This repository serves as a reusable template for streamlining backups. Copy the example files and populate them with your own values.
- Provides safe “cold” backups by stopping only the relevant compose stacks, then restarting them after the snapshot.
- Makes restores fast & repeatable: stage a snapshot into a working folder, then apply it to service. It will recreate the respective folders & volumes.
- Avoids manual volume lists maintenance by scanning compose files in the services dir and generating the backup volume mounts automatically. (Yes, volume access is mount based to keep it more safe, portable and system agnostic.)
- Adds scheduled remote replication (
restic copyto SFTP or mounted NAS) without re-running a hot backup.
- Keep only template files in Git (
.env.example,remotes/remote.env.example,services/*/docker-compose.env.example). - Keep real runtime secrets out of Git (
restic-backup/.env,restic-backup/remotes/<name>/remote.env,services/*/docker-compose.env). - Do not store SSH private keys inside this repository tree.
- Keep backup and staging data out of Git (
restic-repository/,restic-backup/tmp-for-restore/). restic-backup/docker-compose.ymlmounts/var/run/docker.sock; use only trusted images and scripts in this stack.- If any real secret or private key was ever committed or pushed, rotate it.
At a high level there are two backup types: local and remote.
A local copy is kept for fast backup/restore operations if a service breaks or experiences data loss.
One or more remote copies are kept in case the whole server is lost and you need to rebuild on a new VM or host. Remote copies are created via restic copy instead of restic backup. This means you only need one cold backup (local), which minimizes downtime while preserving data integrity.
Check available make commands first: run make inside restic-backup/ (see restic-backup/Makefile).
If you want to spin up restic do not use docker compose up,
use the make command: make restic-up because before spinning up the container it generates a yml override with all the available volumes declared in the compose files within the services folder. Hence, if you add or remove services running in the homelab, you don’t have to manually declare the volumes for each service you want to back up.
All commands below assume you are in the restic-backup/ folder:
cd restic-backup
- Create your local secrets file:
cp .env.example .env
Edit .env and set RESTIC_PASSWORD.
- Start the restic stack (dynamic volume mounts):
make restic-up
This starts only local services (backup/prune/check).
- Useful local commands:
make snapshot creates a restic snapshot (manual test run; cron handles the scheduled ones).
make snapshot-list lists all available snapshots.
make snapshot-stage SNAPSHOT_ID=<id> stages a snapshot into /tmp-for-restore. Restic stores data incrementally, but the restore produces the full filesystem state for that snapshot date, ready for recovery.
make stage-apply SERVICES="paperless" restores the service folder and its Docker volumes
from the staged snapshot data (the output of snapshot-stage). It stops only the
containers in that compose stack, restores, then starts them again.
This avoids hot backups by copying snapshots from the local repo to a remote repo.
Remote config lives under remotes/<name>/:
remote.env(repo + schedule)ssh/(key, config, known_hosts)
Example structure:
remotes/sftp/ssh
├── config
├── known_hosts
└── sftpbackup
This creates remotes/<name>/remote.env and remotes/<name>/ssh/*:
make remote-add TYPE=sftp
Manual alternative:
mkdir -p remotes/my-remote
cp remotes/remote.env.example remotes/my-remote/remote.env
Edit remotes/my-remote/remote.env:
RESTIC_REPOSITORY(remote SFTP repo, e.g.sftp:user@host:/path/to/repo)RESTIC_PASSWORD(remote repo password)REMOTE_COPY_CRON(default: 1h after local backups)
If you keep /restic mounted read-only in the copy container, keep:
RESTIC_COPY_ARGS=--no-lock
and ensure copy runs when no other restic job is active.
If you used make remote-add TYPE=sftp, this is already done (you can skip this section).
Create the SSH folder and drop your private key there: (it is recommended to create a separate sftp user on your remote target, without login shell capabilities, and limited access only to a certain folder)
mkdir -p ./remotes/my-remote/ssh
cp /path/to/your/key ./remotes/my-remote/ssh/sftpbackup
chmod 700 ./remotes/my-remote/ssh
chmod 600 ./remotes/my-remote/ssh/sftpbackup
Add a minimal SSH config (recommended):
cat > ./remotes/my-remote/ssh/config <<'EOF'
Host your-host-name
HostName your-host-name
User sftpbackup-user
IdentityFile /root/.ssh/sftpbackup
IdentitiesOnly yes
EOF
chmod 600 ./remotes/my-remote/ssh/config
Add known_hosts (required because /root/.ssh is mounted read-only):
ssh-keyscan -H your-host-name >> ./remotes/my-remote/ssh/known_hosts
chmod 600 ./remotes/my-remote/ssh/known_hosts
Env and SSH changes require a recreate:
make remote-up REMOTE=my-remote
make remote-up REMOTE=my-remote FORCE_RECREATE=1
make remote-init REMOTE=my-remote
make snapshot-remote REMOTE=my-remote
make snapshot-list-remote REMOTE=my-remote
make snapshot-stage-remote REMOTE=my-remote SNAPSHOT_ID=<id>
Recommended: run the bootstrap again and choose a new remote name:
make remote-add TYPE=sftp
Manual alternative:
- Copy an existing remote folder, e.g.
remotes/my-remote→remotes/sftp-pi. - Edit
remotes/sftp-pi/remote.envandssh/in that folder. - Start the copy container and use it:
make remote-up REMOTE=sftp-pi
make snapshot-remote REMOTE=sftp-pi
- Create a new folder, e.g.
remotes/nas-homelab. - In
remotes/nas-homelab/remote.env, set:
RESTIC_REPOSITORY=/mnt/nas/restic
- You can omit
ssh/for this type. - Start and use:
make remote-up REMOTE=nas-homelab
make snapshot-remote REMOTE=nas-homelab
services/paperlessis kept as an example stack (compose + env) for learning.- The
services/folder is scanned to generate dynamic volume mounts. docker-compose.volumes.generated.ymlis generated and not meant to be committed.- Remote config now lives under
remotes/<name>/(env + ssh).
You can keep your services or restic repository outside this repo and symlink them in. This works as long as Docker can access the target paths.
Example:
ln -s /Users/you/my-homelab-services ./services
ln -s /Users/you/my-homelab-restic-repo ../restic-repository
Quick access check (must succeed):
docker run --rm -v /Users/you/my-homelab-services:/mnt alpine ls /mnt
If the test fails, add the target path to Docker’s shared folders (Docker Desktop
Settings → Resources → File Sharing), or move the target under /Users/....
For OrbStack, paths under /Users/... are typically available by default; if a path
is outside that tree, move it under /Users/... or add a bind mount to a shared path.