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.
Decide 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 psCreate 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.
Run the backup once before scheduling it
The companion script makes a timestamped recovery set. It stops the running application services first and leaves the PostgreSQL service running for its custom-format dump. It captures the selected non-database volumes while those writers remain stopped, then resumes only the services it stopped. Pause external database and file writers for this entire window too. 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 these commands, install AWS CLI and age on the server. Docker and Compose must already run your application. Generate the age identity on your administrator workstation, retain the private key off the server, and put only its public recipient in AGE_RECIPIENT.
For a first rehearsal, use the companion’s fixture/ example on a disposable server. It creates a database record pointing to uploads/record-42.txt in a named app volume. Its README supplies the exact mapping and recovery check, so you do not need to invent an application.
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-recoverygit checkout 08fcc40647eef74eb7015fb66c00a96386795dc0sudo 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. Alert when the command fails or no complete backup arrives by its expected time.
sudo bash -c 'set -a; . /etc/compose-recovery.env; set +a; export AWS_DEFAULT_REGION=us-east-1; aws s3 ls "s3://$BUCKET/$PREFIX/" --endpoint-url "https://$REGION.digitaloceanspaces.com"'Restore 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-recoverygit checkout 08fcc40647eef74eb7015fb66c00a96386795dc0sudo 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.
Check your result
- 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.