Back Up a Docker Compose App and Test the Restore
Back up Docker Compose configuration, named volumes, and PostgreSQL to private object storage, then prove a restore works on an isolated server.
Companion GitHub repositoryCompanion Docker Compose recovery scriptsTylorMayfield/docker-compose-spaces-recoveryView on GitHubDecide what must come back
A Docker Compose service is recoverable only when you can rebuild its configuration, restore the data it owns, and start it without guessing which secret or version it used. A Droplet backup or snapshot can be useful for a whole-machine problem. It does not replace a repeatable application restore test.
Start with one application. Write down its Compose directory, `.env` file, named volumes, bind mounts, image tags, database service, public hostname, and the harmless check that tells you it works. If you cannot name those things, you are not ready to automate a backup.
- Copy configuration only from a protected directory. Never commit a production `.env` file to the companion repository.
- Use a PostgreSQL logical dump for PostgreSQL data. Do not tar live database files from its volume.
- Keep the decryption identity outside both the application server and the backup bucket.
Keep a small recovery inventory
Run these read-only commands in the application directory before changing anything. They show the services, the resolved Compose configuration, and the Docker volumes available on the server. Record the volume names that matter. A volume called `postgres_data` is common, but it is not a rule.
cd /srv/YOUR_APP
sudo docker compose config --services
sudo docker compose config
sudo docker volume ls
sudo docker compose psReplace before use: YOUR_APP
Create private storage and separate keys
Create a private Spaces bucket for this recovery set. Enable versioning and set a lifecycle rule that matches the retention your team has agreed to. Use a full-access key only while you configure versioning or lifecycle rules. Give the scheduled backup job a limited key for this bucket. Create a second limited read key for the restore test, so the test machine never carries the writer credential.
Keep this bucket private. Do not add a CDN, a public policy, or a public listing because a backup is easier to browse that way. Those choices change the risk, not the convenience.
Check the first response
- Expected result
- The test server decrypts one complete set, starts the recovered application under a test hostname, and exposes one known read-only record without contacting production.
- Stop if
- Stop if the target is a production server, the backup set has a missing manifest or mismatched timestamp, the recovered service can send outbound traffic, a production hostname remains, or a secret appears in output.
- Next step
- Record the timestamp, recovery duration, and result. Remove recovered data from the test server and investigate every manual change before scheduling the job.
Run the backup once before scheduling it
The companion script makes a timestamped recovery set. It writes a custom-format PostgreSQL dump through the database container when you configure one. It then stops the Compose stack while it archives selected non-database volumes, so you do not capture an application halfway through a write. It encrypts the set before upload.
That short pause is deliberate. If your application cannot tolerate it, use the database and application vendor's documented online backup method. Do not quietly accept a backup that sometimes restores corrupted data.
Before continuing: Run the first backup by hand and inspect the resulting manifest before scheduling it. Keep the configuration file outside the cloned repository.
git clone https://github.com/TylorMayfield/docker-compose-spaces-recovery.gitcd docker-compose-spaces-recoverysudo install -m 600 .env.example /etc/compose-recovery.envsudo nano /etc/compose-recovery.envsudo ./scripts/backup-compose-app.sh /etc/compose-recovery.env
Check the recovery set before you trust it
The backup command should leave one manifest and the encrypted files under the same timestamp. List the remote prefix after the first run and save the timestamp in your operations notes. A missing manifest, a zero-byte archive, or a database dump without its matching configuration set is a failed backup.
Schedule only the command that you have run and inspected. Send its stderr to the alerting route your team already watches. A cron job that fails quietly is a calendar entry, not a recovery plan.
set -a
. /etc/compose-recovery.env
set +a
aws s3 ls "s3://$BUCKET/$PREFIX/" --endpoint-url "https://$REGION.digitaloceanspaces.com" | tail -8Restore on a different server
Use a fresh Linux Droplet, a test hostname, and a Cloud Firewall that exposes HTTPS only to the recovery administrator. Before the recovered application starts, block outbound traffic in the test firewall or at the host. This matters for chat servers, automations, payment callbacks, and any app that can send a message after it wakes up.
Download one complete timestamped set and its matching manifest using the read-only key. Decrypt it only on the test server. The companion creates new Docker volumes with a recovery prefix and restores their selected archives. If the set contains PostgreSQL, start a fresh test-only container and an empty test database first. In the root-only restore file, provide its container, database, user, and password; the script passes that password only to the restore process. The apply command writes to those new volumes and that test database, never production. Replace production hostnames in the recovered configuration with the test hostname before startup. Do not point a recovery test at the live domain.
Before continuing: The apply command writes recovered data into new recovery volumes and a test-only database. Run it only on the isolated test server after blocking outbound traffic and confirming RESTORE_ENV=test.
git clone https://github.com/TylorMayfield/docker-compose-spaces-recovery.gitcd docker-compose-spaces-recoverysudo install -m 600 .env.example /etc/compose-recovery-restore.envsudo nano /etc/compose-recovery-restore.envsudo ./scripts/restore-compose-app.sh /etc/compose-recovery-restore.envsudo ./scripts/restore-compose-app.sh --apply /etc/compose-recovery-restore.env
Prove the application, then clean up
A green container list is not enough. Sign in to the test hostname, find one known record, and run the harmless health check you chose at the start. For a web app, that may mean opening a read-only page. For a worker, it may mean checking that the process starts while outbound work stays blocked.
Record the recovery-set timestamp, elapsed time, result, and anything you had to change. Rotate any credential that appeared outside its intended protected location. Destroy the test server or wipe the recovered data once the test ends.