Skip to main content

Migrating Docker Containers Between Servers

Migration · 03.02.2025 · 5 min read
Illustration for “Migrating Docker Containers Between Servers”

Moving Docker containers between servers comes down to three things: images, data volumes and environment configuration. A container itself holds no state — everything that matters lives in a volume and in .env variables, so "moving a container" in practice means moving the image and the data, then starting it again on the new host. If Docker is not installed on the new server yet, start with the basic Docker on a VPS setup, and for multi-container projects the Docker Compose article is useful too.

Method 1: docker save + docker load

This fits when there is no shared Registry between the servers and you need to hand over the image as a single file — for example, during a one-time move to a new VPS.

# On the old server -- save the image with all layers
docker save myapp:latest | gzip > myapp.tar.gz

# Transfer to the new server
scp myapp.tar.gz root@NEW_SERVER:/tmp/

# On the new server -- load the image
docker load < /tmp/myapp.tar.gz

# Confirm the image is present with the same ID
docker images | grep myapp

The downside is file size: an image with a heavy base layer (a full Ubuntu instead of Alpine, for instance) easily runs several gigabytes, and transferring it over a slow link takes hours.

A Registry transfers only the missing layers, supports rolling back to a previous tag, and does not require keeping an image file on disk between servers.

# Tag and push to the Registry
docker tag myapp:latest registry.example.com/myapp:latest
docker push registry.example.com/myapp:latest

# On the new server
docker login registry.example.com
docker pull registry.example.com/myapp:latest

If you use docker-compose, it is enough to copy docker-compose.yml and the data volumes for the move — the images will be rebuilt locally or pulled from the Registry by the docker compose up -d command.

Common errors during the move

  • Error response from daemon: conflict: unable to delete ... (must be forced) — the image is used by another tag or a running container; stop the containers using the image first, or add the -f flag to docker rmi.
  • open /tmp/myapp.tar.gz: no such file or directory — scp was interrupted mid-transfer or the path is wrong; check the file size on both sides with ls -la and compare a sha256sum checksum.
  • unauthorized: authentication required — docker login to the private Registry was not run on the new server, or the access token has expired.
  • Error: No such image: myapp:latest — a typo in the tag during docker tag, or the image was never pushed before the pull attempt on the other server.
  • toomanyrequests: You have reached your pull rate limit — anonymous requests to Docker Hub are rate-limited per IP; run docker login with an account or use your own Registry.

Moving volumes

The image carries only the application code and its dependencies. The database, uploaded files and logs live in a volume and are moved separately.

# Export the volume into a temporary archive
docker run --rm -v myapp_data:/data -v $(pwd):/backup \
  alpine tar czf /backup/volume_backup.tar.gz -C /data .

# Import on the new server into a volume created in advance
docker volume create myapp_data
docker run --rm -v myapp_data:/data -v $(pwd):/backup \
  alpine tar xzf /backup/volume_backup.tar.gz -C /data

Comparing the two methods

Criteriondocker save/loadDocker Registry
What is transferredThe full image archiveOnly the new layers
Requires a separate serviceNoYes, a Registry
Rolling back to a previous versionYou must keep old archives manuallyJust switch the tag
Best suited forA one-time move of a single serverRegular deploys and several servers

When these methods do not fit

  • The container holds a stateful network connection open (a live WebSocket session, for example) — a move always breaks such connections; you need a graceful shutdown route at the application level.
  • The container is tied to a host device through --device (a GPU, a USB token) — the image will move, but part of the functionality will not work without the same device on the new server.
  • Data keeps changing during the volume export — if the application is not stopped, running tar against a live database volume can capture the file in an inconsistent state; for a database engine, move a dump produced by the database itself instead of the raw volume files.

Docker migration checklist

StepStatus
Install Docker on the new server⬜
Move docker-compose.yml⬜
Move the images or set up a pull from the Registry⬜
Move the data volumes⬜
Update the .env files⬜
Verify on the new server⬜
Switch DNS⬜

Verifying the result

Before switching DNS, confirm the stack behaves on the new server the same way it did on the old one.

# All containers are up without restarts
docker compose ps

# No errors in the logs over the last 5 minutes
docker compose logs --since=5m

# The application responds on the internal port
curl -I http://127.0.0.1:8080/health

# Data matches: compare the row count in a key table
docker exec -it myapp_db psql -U app -c "SELECT count(*) FROM orders;"

If the counters and logs match the old server, switch DNS and only then stop the containers on the old machine.

Before stopping the containers on the old server, make sure all data is synchronized — especially with persistent volumes holding a database: any write made after the dump was taken is missing on the new server.

Was this article helpful?
← Back to Knowledge Base Ask Support