On this page

A server snapshot can get a machine back. This guide keeps the application data, Compose files, and database dump you need to prove the app can return.

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.

Shell command
cd /srv/YOUR_APP
sudo docker compose config --services
sudo docker compose config
sudo docker volume ls
sudo docker compose ps

Check the pinned script's capture boundary

Before buying storage, scheduling a job or pausing writers, map every recovery-inventory item to a specific captured artifact or a separately tested recovery procedure. The pinned backup script accepts only an application directory containing compose.yml or docker-compose.yml. Its configuration allowlist is exactly compose.yml, docker-compose.yml, .env and Caddyfile, when present. It archives only the named volumes listed in VOLUME_NAMES, plus the optional configured PostgreSQL dump.

compose.yaml or docker-compose.yaml alone fails its filename check. Bind-mounted data, overrides/includes, additional env_file files, and file-based secrets/configs are not automatically captured. Those dependencies can be missing even when the command reports a successful upload. Resolved Compose output helps identify them; it does not make the script capture them. Keep that output private because it can contain secrets.

Stop this recipe if the layout is unsupported or any required item is unmapped. Do not blindly rename production Compose files or remove overrides to pass the check. Use a separately reviewed and tested recovery procedure for that layout before maintenance. The companion has not been changed to support these cases; its simple named-volume fixture does not certify recovery of other applications.

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.

Back up a Minecraft server to private Spaces storage · Run a private n8n server and test the restore

Use the provider's Spaces key permissions, versioning and lifecycle instructions for these bucket-management steps. Keep configuration credentials off the scheduled backup host.

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.

Install AWS CLI v2 and age from its official installation guide. Record aws --version, age --version and docker compose version in the rehearsal record; these tools are prerequisites, not versions tested by this article. PostgreSQL documents why a logical dump is internally consistent; application files still need their writers paused throughout capture.

Shell command
git clone https://github.com/TylorMayfield/docker-compose-spaces-recovery.git
cd docker-compose-spaces-recovery
git checkout 08fcc40647eef74eb7015fb66c00a96386795dc0
sudo install -m 600 .env.example /etc/compose-recovery.env
sudo nano /etc/compose-recovery.env
# Set APP_DIR, BUCKET, REGION, VOLUME_NAMES, AGE_RECIPIENT, and optional PostgreSQL settings.
sudo ./scripts/backup-compose-app.sh /etc/compose-recovery.env

Disposable recovery fixture

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.

Shell command
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.

Shell command
git clone https://github.com/TylorMayfield/docker-compose-spaces-recovery.git
cd docker-compose-spaces-recovery
git checkout 08fcc40647eef74eb7015fb66c00a96386795dc0
sudo install -m 600 .env.example /etc/compose-recovery-restore.env
sudo nano /etc/compose-recovery-restore.env
# Set RESTORE_ENV=test, the read-only key, one TIMESTAMP, and all four PostgreSQL target values when postgres.dump exists.
sudo ./scripts/restore-compose-app.sh /etc/compose-recovery-restore.env
sudo ./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.

Monitor a website with Uptime Kuma · Self-host a private Stoat chat server

The pinned fixture passes only when restored row 42 still names uploads/record-42.txt and that file contains record-42. Record the backup timestamp, repository SHA, database/image versions, row result, file result and elapsed recovery time. This is an acceptance procedure; no live recovery result is asserted here.

Frequently asked questions

Is a Droplet snapshot enough for a Docker Compose app?

A snapshot can help recover an entire machine, but use an application-level backup as well. It gives you a portable set of Compose files, persistent data, and a database dump that you can test on a different server.

Can I back up a live PostgreSQL Docker volume with tar?

Do not rely on that for a PostgreSQL backup. Use pg_dump or another database-supported method, then restore it into a fresh database during the test.

Why use separate object-storage keys for backup and restore?

The production server needs to write backups. The temporary test server should only read one set. Separate limited keys reduce what either machine can do if its credential leaks.

How often should I test a restore?

Test after the first backup, before material application changes, and on a schedule that matches the risk of the service. Record the result so a future recovery does not start from memory.