6.8 KiB
Zorruno Docker Backup Scripts
Modular backup scripts for Docker Compose servers.
Each server has its own untracked .env and containers.local.txt, while all script
logic remains common and can be updated through Git.
Pipeline
backup.sh runs these stages in order:
stages/10-containers.sh— stop, archive, pull, start and verify projects.stages/20-rclone.sh— copy archives to an rclone destination when enabled.stages/30-smb.sh— copy archives to a mounted SMB share when enabled.stages/90-prune.sh— retain the newest configured number of.zstarchives locally, including legacy.zstand current.tar.zstnames.
Every stage can also be run directly. The old docker-backup.sh name invokes
the container archive stage, while dockerbackup_prune.sh invokes the local
prune stage. Use the new backup.sh for the complete pipeline.
Requirements
- Bash 4 or later
- Docker with
docker compose, or legacydocker-compose - GNU
tarwith zstd support flockrcloneonly when the rclone method is enabledrsyncandmountpointonly when the SMB method is enabled
The account running the scripts must be allowed to control Docker and read all
files under /dockervolumes.
Installation on a server
sudo git clone https://home.fox.co.nz/gitea/zorruno/zorruno-backupdockerscript.git /backupdockerscripts
cd /backupdockerscripts
sudo cp .env.example .env
sudo cp containers.txt containers.local.txt
sudo chmod 600 .env
sudo chmod +x backup.sh stages/*.sh
Edit .env. A typical Docker source and example host name are represented by:
SOURCE_BASE_DIR="/dockervolumes"
BACKUP_HOSTNAME="examplehostname"
Choose and create a local staging path for BACKUP_ROOT. /dockerbackups is
the default, but it should be on a filesystem with enough free space to hold
at least KEEP_COUNT copies of every listed project.
Edit the server's untracked containers.local.txt. Each active line can be either a running
container name or a directory name beneath /dockervolumes. Blank lines and
text after # are ignored. If several entries resolve to the same Compose
project directory, that project is backed up only once. Keeping this host-local
file out of Git prevents a pull on any server from replacing its local list.
Configuration
Copy .env.example to .env; .env is ignored by Git. Important values:
| Variable | Purpose |
|---|---|
SOURCE_BASE_DIR |
Parent of Compose project directories |
BACKUP_ROOT |
Local archive staging and retention tree |
CONTAINERS_FILE |
Absolute path, or path relative to this repository |
BACKUP_HOSTNAME |
Host folder/name embedded in archive filenames |
BACKUP_METHODS |
Ordered method names, default rclone smb |
ZSTD_EXTRA_OPTS |
Options passed to zstd, default -T0 -12 |
KEEP_COUNT |
Local archives retained per Compose project |
PULL_IMAGES |
Pull images before restarting (1 or 0) |
Rclone
RCLONE_ENABLED=1
RCLONE_REMOTE="gdrive:dockerbackups"
Archives are copied beneath <remote>/<BACKUP_HOSTNAME>/. The method uses
rclone copy, so it does not delete older destination files. Set
RCLONE_HOST_SUBDIR=0 when RCLONE_REMOTE already names the final host-specific
destination; otherwise the host name is appended automatically.
SMB
Mount the SMB share through /etc/fstab or a systemd mount unit; do not put SMB
credentials in this repository. Then configure:
SMB_ENABLED=1
SMB_MOUNT="/mnt/dockerbackups"
SMB_HOST_SUBDIR=1
The script refuses to copy unless SMB_MOUNT is an actual mount point. This
prevents a failed network mount from silently filling the server's root disk.
Running
Run the complete pipeline:
sudo /backupdockerscripts/backup.sh
Preview all stages without stopping containers, copying, or deleting:
sudo /backupdockerscripts/backup.sh --dry-run
Select a different container list for one complete pipeline run:
sudo /backupdockerscripts/backup.sh --containers containers-special.txt
Relative container-list paths are resolved from the repository directory; absolute paths are also accepted. The selected list affects only the container archive stage, after which configured transfer methods and pruning continue.
Run individual parts:
sudo stages/10-containers.sh --dry-run
sudo stages/20-rclone.sh --dry-run --force
sudo stages/30-smb.sh --dry-run --force
sudo stages/90-prune.sh --dry-run
--force runs a transfer method even when its *_ENABLED value is false. It
still requires the method's destination to be configured and available.
MQTT reporting and Home Assistant trigger
Enable retained run results in the host's .env:
MQTT_REPORT_ENABLED=1
MQTT_HOST="examplehostname"
MQTT_PORT=1883
MQTT_USERNAME="dockerbackup"
MQTT_PASSWORD_FILE="/etc/zorruno-docker-backup/mqtt-password"
MQTT_STATUS_TOPIC="viewroad-status/dockerbackup/examplehostname"
MQTT_COMMAND_TOPIC="viewroad-commands/dockerbackup/examplehostname/run"
The retained status JSON includes overall status, timestamps, duration, exit code, container totals, successful backups, dry-run counts, and error messages. Final reporting runs from an exit trap, so failures in any earlier stage are reported when MQTT remains available.
Install the optional root-run command listener with:
cp systemd/zorruno-docker-backup-mqtt.service /etc/systemd/system/
systemctl daemon-reload
systemctl enable --now zorruno-docker-backup-mqtt.service
It accepts only JSON containing the exact action run on the configured
command topic:
{"action":"run"}
Publish commands without MQTT retain. The listener also ignores retained messages, preventing an old command from starting another backup after a listener or broker restart.
Restrict publish access to the command topic with broker ACLs. The listener runs as root because the backup stage controls Docker containers. The existing pipeline lock prevents concurrent runs.
Adding another backup method
Add an executable script named stages/NN-name.sh, for example
stages/40-restic.sh. It should load lib/common.sh, accept --dry-run, and
return a non-zero status on failure. Add its short name to .env:
BACKUP_METHODS="rclone smb restic"
The main pipeline discovers the numbered file from the short name and runs it after the earlier methods. A failed method stops the pipeline, so pruning will not run after a failed transfer.
Archive and restore format
Archives are stored as:
<BACKUP_ROOT>/<project>/<host>.<project>.<date_time>.tar.zst
Each archive contains the project's top-level directory rather than absolute paths. Example restore into an empty temporary directory:
mkdir -p /tmp/docker-restore
tar --zstd -xf backup.tar.zst -C /tmp/docker-restore
Inspect and verify restored content before replacing a live project.