Move or restore a Docker Compose app onto a new machine
Back up a Compose stack (compose files, .env, named volumes and bind-mounted folders) and bring it back on a blank machine with its data intact, without the 'it started fresh and empty' surprise.
A Compose app is more than its compose.yaml. Copy only that file to a new machine, run docker compose up, and you get a brand-new, empty app: no users, no database, no settings. The data lives in places the compose file only points at.
This guide moves everything across and proves the restore worked before you touch the old machine. The examples use a project called myapp in ~/myapp.
Stop if: you’re about to run
docker compose down -v. The-vdeletes named volumes, which are your data. Also avoid plaindownuntil the end: Docker’s docs say it removes the containers, so anything an app wrote inside a container (not to a volume) is gone with them.
Step 1: Inventory on the old machine (before stopping anything)
Record exactly what’s running, so nothing is left behind and the new machine runs the same thing.
cd ~/myapp
umask 077 # backups contain passwords: keep them private
mkdir -p ~/migrate
docker compose ls # the real PROJECT NAME (may not match the folder)
docker compose config > ~/migrate/resolved-compose.yaml # the fully merged config actually in use
docker compose images > ~/migrate/images.txt # versions you're running now
docker compose ps -q | xargs docker inspect \
--format '{{.Name}}{{range .Mounts}}{{println}} {{.Type}} {{if .Name}}{{.Name}}{{else}}{{.Source}}{{end}} -> {{.Destination}}{{end}}' \
| tee ~/migrate/mounts.txt
Read mounts.txt carefully. It’s your checklist:
volumelines are named volumes (myapp_db-data) or, if the name is a long random string, anonymous volumes. Those are easy to lose, so back them up by name like the rest.bindlines are host folders. Some may be outside~/myapp(e.g./srv/photos). Each one needs copying.- An important path inside a container with no mount (for example an app that saves to
/app/databut you never mapped it) lives only in that container. Copy it out now withdocker cp <container>:/app/data ~/migrate/. Better still, add a volume for it on the new machine.
Also note how you start it: extra -f files, --profiles, -p or COMPOSE_PROJECT_NAME. The new machine must use the same ones.
Pin versions: in images.txt, note the exact tag (or digest) of each image. If the compose file says :latest, change it to that exact version before the first start on the new machine. Otherwise you may get a newer database that upgrades your data on first launch.
Step 2: Make app-level dumps, then stop
If there’s a database, take its own dump while it’s still running. This is the most portable backup there is:
docker compose exec -T db pg_dump -U myuser mydb > ~/migrate/mydb.sql # PostgreSQL
# docker compose exec -T db mysqldump -u root -p mydb > ~/migrate/mydb.sql # MySQL/MariaDB
Then stop the app but keep its containers:
docker compose stop -t 60 # give databases time to shut down cleanly
docker compose ps -a # all should say "Exited (0)"; investigate anything else
If something else also writes to the same folders or volumes (another stack, a backup job), stop that too.
Step 3: Back up every volume and folder on your list
Named and anonymous volumes. Docker’s documented pattern is a throwaway container that tars the volume. Edit VOLUMES to match mounts.txt exactly.
( set -euo pipefail # the ( ) keeps a failure from closing your terminal
cd ~/migrate
VOLUMES="myapp_db-data myapp_uploads"
: > volumes.list
for v in $VOLUMES; do
docker run --rm -v "$v":/data:ro -v "$PWD":/backup alpine \
tar czf "/backup/vol-$v.tgz" -C /data .
echo "$v" >> volumes.list
done ) && echo "ALL VOLUMES BACKED UP"
If you don’t see ALL VOLUMES BACKED UP, one failed. Fix it before going on.
Bind-mounted folders, including any outside the project, and the project folder itself (compose files, .env, env_files, secrets):
sudo tar --numeric-owner --acls --xattrs -czpf ~/migrate/project.tgz -C ~ myapp
sudo tar --numeric-owner --acls --xattrs -czpf ~/migrate/bind-srv-photos.tgz -C / srv/photos # one per outside folder
sudo chown -R "$USER" ~/migrate
--numeric-owner keeps the exact user and group IDs that apps inside containers depend on.
Finally, record checksums and confirm every archive is readable:
cd ~/migrate && sha256sum *.tgz *.sql > SHA256SUMS
for f in *.tgz; do tar tzf "$f" > /dev/null && echo "OK $f"; done
Step 4: Move it and verify
Copy ~/migrate by any private route: scp -r ~/migrate newhost:~/, an external drive, or the NAS. These files contain passwords and databases, so don’t leave them on shared storage longer than needed. On the new machine:
cd ~/migrate && sha256sum -c SHA256SUMS # every line must say OK
Step 5: Restore on the new machine
- Install Docker and the Compose plugin (
docker compose versionshould work). If the new machine has a different CPU type (Intel → Raspberry Pi or Apple Silicon), check every image is published for it. - Restore the project and bind folders to the same paths:
cd ~ && sudo tar --numeric-owner --acls --xattrs -xzpf ~/migrate/project.tgz sudo tar --numeric-owner --acls --xattrs -xzpf ~/migrate/bind-srv-photos.tgz -C / - Make sure the project name matches. Compose names a volume
<project>_<volume>, and the project defaults to the folder name. Restore into a different folder and Compose quietly makes new, empty volumes. Addingname: myappat the top of the compose file pins it. Note that-porCOMPOSE_PROJECT_NAMEstill override it, so use the same ones you noted in Step 1. - Pin image versions (Step 1), then let Compose create the volumes without starting anything. Compose labels the volumes it creates, and it warns about volumes it didn’t create (a Docker forum answer covers this). Volumes marked
external: truearen’t created by Compose: make those yourself first withdocker volume create <name>.cd ~/myapp docker compose create - Fill each volume. It’s emptied first, because creating a container can copy files from the image into a new volume, and you want exactly your data, not a mix:
( set -euo pipefail while read -r v; do docker volume inspect "$v" > /dev/null # stops here if the name doesn't exist docker run --rm -v "$v":/data -v ~/migrate:/backup alpine \ sh -c "find /data -mindepth 1 -delete && tar xzf /backup/vol-$v.tgz -C /data" done < ~/migrate/volumes.list ) && echo "ALL VOLUMES RESTORED" - Start it and watch the logs:
docker compose up -d docker compose logs -f --tail=50
Step 6: Prove it’s really your data
- Log in with an existing account. A first-run “create admin” screen means it’s reading empty volumes: check
docker volume lsand the project name. - Check each data store, not just one. Open something recent from every app or service in the stack (the latest photo, the newest record, recent settings).
- Bind folders kept their owners:
ls -ln /srv/photosshows the same numbers as on the old machine. Apps running as a specific user ID fail with “permission denied” if they changed. - Run the app’s own health or integrity check if it has one (many self-hosted apps have a maintenance or “check database” option).
Only when everything passes should you retire the old machine. Now docker compose down there is fine. Keep ~/migrate (somewhere private) for a few weeks.
Common snags
- The database got upgraded by accident because
:latestpulled a newer version: don’t just switch the tag back. The data may already be converted to the new format. Restore the volume again from your.tgz(or the.sqldump), pin the old version, then upgrade properly following the app’s instructions. - Ports already in use on the new machine:
sudo ss -ltnp(Linux) shows what’s holding them. - Rootless Docker or user-namespace remapping on one machine but not the other changes how file owners map. Use the same setup on both, or fix ownership after restoring.
- Copying
/var/lib/dockerwholesale seems quicker, but a Docker forum thread notes it only works when both machines use the same storage driver. The volume-by-volume method above is the portable one.
Sources
- Volumes: Back up, restore, or migrate data volumes — by Docker Docs
- Compose file reference: Volumes top-level element — by Docker Docs
- Specify a project name — by Docker Docs
- docker compose down — by Docker Docs
- How to restore Docker volumes on a different host? — by Docker Community Forums