Set up the client
The website handles your account, billing, and finding a buddy. The actual backup happens through a small program you run on your own computer, in Docker. Here's how to get it running.
Before you start
You'll need:
-
Docker, with Docker Compose (the
docker composecommand, not the older standalonedocker-compose). Docker Desktop (Mac/Windows/Linux) includes this already. On a Linux server without Docker Desktop, install the Compose plugin alongside Docker. Check you have it with:
That should print a version number. If instead you get "docker: 'compose' is not a docker command", Compose isn't installed yet — follow the link above before continuing.docker compose version - An active buddy pairing — redeem an invite code or request one from Find a buddy first. The client won't have anyone to talk to without one.
- A passphrase only you'll know. Think of it now — it encrypts your data before it leaves your machine, and there's no way to recover it if you lose it. A few random words work well (at least 12 characters).
BACKUP_PASSPHRASE is the only key to your backups — files are encrypted with it on your
own machine, and nobody else ever has it. If you lose it, what your buddy holds for you can never be
decrypted: not by a password reset (that's only your website login), not by support, not by us.
It's not stored anywhere but your .env file, which won't help if the machine itself dies.
Steps
- Get a device token On your dashboard, under Devices, give this machine a label (e.g. "home desktop") and click Add device. You'll see a token — copy it now, it's only shown once.
-
Make a folder and save two files into it
Create a folder for the client (e.g.
backup-buddies-client) and save these two files inside it.docker-compose.ymlpulls the published client image (no build step, no source code on your machine). Change the left side of the three highlighted lines to your own folders, and leave everything after the colon alone:services: client: image: ghcr.io/mspencerl87/backup-buddies/client:latest restart: unless-stopped network_mode: host env_file: .env environment: - BACKUP_DIR=/backup - BUDDY_FILES_DIR=/buddy-files - RESTORE_DIR=/restored volumes: # The client's own config (identity key, bookkeeping). Small; keep it here. - ./config:/data # Your files to back up. Read-only (:ro): the client never writes here. - /mnt/storage/my-files:/backup:ro # What your buddies store with you. Needs the space you pledged. - /mnt/storage/buddy-files:/buddy-files # Where files you restore from a buddy land. - /mnt/storage/restored:/restorednetwork_mode: hostruns the container directly on this machine's network instead of Docker's usual isolated one — it's what lets this device find a buddy on the same local network and connect directly, skipping the relay entirely. Linux only; if you're on Docker Desktop (Mac/Windows), see the troubleshooting section below.Where your files go. The client keeps four folders apart, so nothing of its own ever lands in your data:
/backup— your files. Mounted read-only; the client only reads them. If this machine should only receive (hold a buddy's files, back nothing up), use./empty-backup-dir:/backup:roinstead./buddy-files— buddy files: what your buddies store with you, encrypted (you can't read them). This is what uses the space you pledged. The client makes one subfolder per buddy inside it, so give it a folder of its own on a drive with room./restored— files you restore from a buddy. Separate from your files, so a restore never overwrites anything./data— the client's own config: identity key and small bookkeeping files. Leave it as./config, next todocker-compose.yml.
Two common setups (just the three lines you change):
# A. One data drive, separate folders on it - /mnt/storage/my-files:/backup:ro - /mnt/storage/buddy-files:/buddy-files - /mnt/storage/restored:/restored # B. Two separate mounts (two disks or NAS shares) - /mnt/my-data:/backup:ro - /mnt/buddy-data:/buddy-files - ./restored:/restoredDon't put the buddy-files or restore folder inside your own files' folder — you'd end up backing your buddy's files (or your restores) back up to your buddy. Network shares (NFS/SMB) work: mount them on the host first, then use the mount path here.
.env— your token and passphrase. Only DEVICE_TOKEN and BACKUP_PASSPHRASE need filling in; the rest have sensible defaults:API_URL=https://api.filegarden.net RELAY_URL=https://relay.filegarden.net DEVICE_TOKEN=paste_the_token_from_step_1_here BACKUP_PASSPHRASE=a few random words only you know # Run as your own user, so restored files belong to you instead of root. # Find your numbers with: id -u and id -g PUID=1000 PGID=1000 # The port the local status dashboard (see below) binds directly on this # host. Defaults to 8080 — only set this if 8080 is already taken, or # you're running more than one device's client on the same machine (give # each its own folder and a DIFFERENT DASHBOARD_PORT here — required, # not optional, when sharing a host, since this binds the port directly # rather than Docker mapping it for you). #DASHBOARD_PORT=8080 # The UDP port used for the actual connection to your buddy (not the # dashboard above). Backup Buddies works fine without ever touching this # — it just relays through relay.filegarden.net instead of connecting # directly — but if you can forward a UDP port on your router, forward # this one to this machine for a shot at a faster direct connection. # Defaults to 11235; change it if already taken, easier to forward # differently, or (same as DASHBOARD_PORT) you're running more than one # device's client on this same host. #SYNC_PORT=11235Example, filled in:
DEVICE_TOKEN=bbd_7f3a9c1e2d4b5a6f8e0c1d2b3a4f5e6dandBACKUP_PASSPHRASE=correct horse battery staple woodland— yours will look different; the token comes from step 1, and the passphrase is anything memorable only you know. -
Start it
docker compose pull docker compose up -d docker compose logs -fpullfetches the already-built image — a few seconds, not a build. You should see a line confirming the device registered. Your Devices card on the dashboard will now show this device as connected. -
Confirm it found your buddy
Once your buddy's client is running too, you'll see a cycle line in the logs roughly every 30 seconds: "backup cycle complete" if there was something to send, or "nothing new to back up" if not — both mean the cycle actually ran and reached your buddy. This happens even on a receive-only device (
./empty-backup-diras its backup folder), since it's still running a cycle against an empty folder (so you'll just see "nothing new to back up" every time, which is expected and confirms your two machines can reach each other — directly, or through the relay if a direct connection isn't possible).
Prefer to build it yourself from source instead?
One command downloads the full source and a docker-compose.yml
that builds it locally instead of pulling the published image —
useful if you'd rather compile it yourself than trust a prebuilt
image, or want to inspect the code first:
curl -fsSL https://app.filegarden.net/client/install.sh | bash
cd backup-buddies-client
Fill in the same two .env values as above (this version sets its folders in .env instead — the *_DIR_HOST lines in its .env.example), then
docker compose up -d — the first run builds the
image locally (a few minutes); after that, starting is instant.
What this version actually does
This is a real, working backup client, not just a connectivity demo — your buddy's machine genuinely ends up holding a usable, encrypted copy of your files.
Does
- Scans your folder roughly every 30 seconds and sends only what's new or changed — unchanged files aren't re-sent each cycle
- Mirrors deletions too, so your buddy's copy matches yours rather than only ever growing
- Encrypts everything with your
BACKUP_PASSPHRASEbefore it ever leaves this machine — your buddy's device only ever stores ciphertext - Connects directly to your buddy's device when possible — including finding them automatically on the same local network and connecting over it, never touching the relay — falling back to the relay automatically when a direct connection isn't possible
- Keeps a backstop when a file is overwritten or deleted — the live copy plus its last 2 replaced versions are kept on your buddy's disk, so a bad overwrite or an accidental local delete isn't necessarily final
- A local status dashboard (
http://<this device>:8080) showing per-buddy pledge usage, sent/received totals, last cycle result, a file browser with per-file version history, and one-click restore — restored files are never written back over your live folder; they land in your restore folder under<buddy>/, and the dashboard shows the exact path once a restore finishes - Enforces your buddy's pledge — won't accept more from them than they've pledged to store
Known limits, for now
- Checks for changes on a schedule (every 30 seconds by default; set
SCAN_INTERVAL_SECSin.envto change it) rather than instantly - Backs up the whole folder you point it at — no per-file or pattern-based exclude list yet
- The local status dashboard has no login of its own — see its section below before exposing that port beyond your own network
Useful docker compose commands
Run these from inside the backup-buddies-client folder (where its docker-compose.yml lives):
docker compose up -d— start the client in the background. Safe to run again any time (e.g. after editing.envordocker-compose.yml) — it restarts with the new settings.docker compose pull && docker compose up -d— update to the latest client: fetches the newest published image and restarts with it, keeping your.env,docker-compose.ymland data folders exactly as they are. Nothing updates itself automatically; your dashboard's Devices card shows update available when a newer version is out (the command it shows does the same thing). Never delete the folder and set it up again to update: that throws away this device's identity and the backups it holds for your buddy.docker compose logs -f— follow the client's logs live. Ctrl+C to stop watching (the client keeps running).docker compose ps— check whether it's currently running.docker compose restart— restart it without rebuilding.docker compose down— stop and remove the container. Your device's identity and everything your buddy stores with you are kept (they live in your config and buddy-files folders, outside the container) —docker compose up -dpicks up right where it left off.
Troubleshooting
- No "registered" line in the logs: double-check
DEVICE_TOKENwas copied correctly and the container can reachapi.filegarden.net. A 401 here means that exact token doesn't match any device on your dashboard — it's either stale (its device was removed and re-added since, which issues a new one) or the paste picked up extra whitespace; easiest fix is to remove the device, add it again, and paste the fresh token. - Won't start: "BUDDY_FILES_DIR … has no Backup Buddies marker" or "belongs to a different Backup Buddies setup": the folder mounted for buddy files isn't the one this device has been using. Almost always that's a drive or network share that wasn't mounted yet when the client started — Docker then mounts the empty folder underneath instead, and the client refuses rather than storing your buddy's files on the wrong disk. Mount the drive and run
docker compose up -dagain. If you deliberately moved the buddy files, change the/buddy-filesline indocker-compose.ymlto the new folder. To deliberately start over with a new, empty folder, deletebuddy-files-idfrom your config folder (your buddies will re-send what they had stored with you). - "backup failed: your backup folder looks empty…": the folder with your files is suddenly empty, but your buddy holds files from it. The client won't mirror that as "delete everything" — check the drive is mounted. If you really did delete everything on purpose, add
ALLOW_EMPTY_BACKUP_DIR=trueto.envand rundocker compose up -d. - A file shows "your buddy's client is out of date — files over 512 MB will send once they update": your buddy is on an older client that can only receive files up to 512 MB. Everything smaller still backs up normally; ask your buddy to update (
docker compose pull && docker compose up -d) and the rest goes out on the next cycle. - A file shows "can't read this file on this device — check its permissions": the client runs as the
PUID/PGIDin your.envand can only back up files that user can read. Everything else keeps backing up. Fix the file's permissions (or runid -u/id -gand checkPUID/PGIDmatch the files' owner). While any folder can't be read, deletions are paused so nothing inside it is mistaken for deleted. - Restored files are owned by root / need sudo: set
PUIDandPGIDin.env(see above) and rundocker compose up -d. On its next start the client hands its own folders over to that user once, then runs as it. - After updating, the logs warn that buddy files "are still in the old location inside DATA_DIR": older versions kept your buddies' files inside the config folder (
./data/received). The client keeps using them there, so nothing breaks — but to give them their own folder (on a bigger drive, say), stop the client, move them, and start it again:
Your existingdocker compose down mkdir -p /mnt/storage/buddy-files # pick your folder mv ./data/received/* /mnt/storage/buddy-files/ # then point the buddy-files line in docker-compose.yml at it: # - /mnt/storage/buddy-files:/buddy-files docker compose up -d./dataconfig folder keeps working as it is. If the client instead refuses to start saying buddy files exist in both places, a folder was half-moved — finish moving everything from./data/received/into your buddy-files folder. Old restores in./data/restoredstay where they are; new ones go to your/restoredfolder. - Fails to start with an "address already in use" error, or logs show it exiting immediately: something else on this host — often another device's client — already has that
DASHBOARD_PORTorSYNC_PORT. Give this device its own, different values for both in its.env(see above) and restart. Withnetwork_mode: host, these bind directly on the host, so they must be unique across every client running on the same machine. - On Docker Desktop (Mac/Windows), the dashboard or buddy connections stop working after adding
network_mode: host: host networking isn't fully supported there the way it is on Linux. Remove that line, add back aports:section instead ("${DASHBOARD_PORT:-8080}:8080"and"${SYNC_PORT:-11235}:${SYNC_PORT:-11235}/udp"), and everything still works — you just won't get local-network (mDNS) direct connections to a buddy on the same LAN; relay and direct-over-internet connections are unaffected. - Registered, but no cycle line ("backup cycle complete" or "nothing new to back up"): confirm your buddy pairing shows as "active" on your dashboard, and that your buddy has also completed these steps on their end.
- Lost your device token: remove the device on your dashboard and add it again — tokens can't be retrieved after creation, only replaced.