Tutorial: move a production Docker host to another VPS with minimal downtime using rsync, volumes and Docker Compose
Deploy & DevOps · By Kevin Marcondes · Published on · 9 min read
Step-by-step tutorial to move a production Docker host to another VPS with minimal downtime using rsync, volumes and Docker Compose. You will perform a two-pass sync, validate the destination and cut traffic by DNS or proxy. The goal is to minimize downtime while keeping data integrity intact.
Before you begin: prerequisites and cutover window
Plan a short maintenance window for the final cutover. Lower the DNS TTL hours beforehand. Back up anything critical before touching data. Ensure you have sudo/root access on both VPSes and SSH key authentication.
Check available disk space on the destination and verify image architecture compatibility (amd64/arm64). Prepare a list of what will be migrated: Compose files, environment variables, secrets and persistent volumes.
If you need to harden the destination server, see the VPS Linux security checklist.
- SSH key access to both VPSes
- Sudo/root to manipulate volumes under /var/lib/docker/volumes
- Pre-reduced DNS TTL
- Recent backup of critical data
- Same architecture or multi-arch images
Map services, volumes and ports on the current host
Inventory what’s running today. Note service names, exposed ports, networks and volumes. This avoids surprises during cutover. The commands below help quickly extract the current environment view.
- Confirm which volumes are stateful (database, uploads, queues)
- Review healthchecks and service dependencies
- List sensitive variables not in .env (e.g. external secrets)
# No host de origem
# Projetos/stack e containers em execução
docker ps --format 'table {{.Names}}\t{{.Image}}\t{{.Ports}}'
# Volumes Docker
docker volume ls
# Mapas de montagem por container (mostra bind mounts e volumes)
docker inspect -f '{{ .Name }} -> {{range .Mounts}}{{.Type}}:{{.Source}}:{{.Destination}} {{end}}' $(docker ps -q)
Prepare the destination VPS (user, firewall, Docker/Compose)
Create a deploy user, enable the firewall and install Docker + the Compose plugin. Reapply any sysctls or limits used on the current host. If you prefer a dedicated installation guide, see how to install Docker and Docker Compose on Linux.
- Reserve a projects directory (e.g. /opt/projects)
- Enable UFW only for required ports
- Ensure a compatible minimum Compose version
# Na VPS de destino (Debian/Ubuntu)
sudo apt-get update
sudo apt-get install -y docker.io docker-compose-plugin rsync ufw
# Usuário de deploy e acesso ao Docker
sudo useradd -m -s /bin/bash deploy || true
sudo usermod -aG docker deploy
# Firewall básico
sudo ufw allow OpenSSH
# Abra apenas o que seu stack realmente usa (exemplo: HTTP/HTTPS)
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
sudo ufw enable
Sync Compose, variables and configuration files
Copy the project repository, docker-compose*.yml files, .env and any overrides to the destination. Do not copy docker.sock or temporary files. Keep the same directory structure to avoid unnecessary path adjustments.
- Use --exclude to skip large directories (e.g. node_modules)
- Check permissions for .env and secrets on the destination
- Avoid hardcoded credentials; keep .env separate
# Do host de origem para o destino (ajuste caminhos/host)
DEST="deploy@IP_OU_HOST_DESTINO"
PROJ="/opt/meu-stack"
rsync -avz --progress -e ssh \
--exclude '.git' --exclude 'node_modules' \
${PROJ}/ ${DEST}:${PROJ}/
# Se .env não estiver no repo
rsync -avz -e ssh ${PROJ}/.env ${DEST}:${PROJ}/.env
First sync of volumes (system still running)
We perform two rsync passes: a warm sync while the app is still serving, and a quick final sync during the cutover window. For databases prefer replication or dump/restore; rsync-ing the _data directory while the DB is writing can corrupt it. For uploads and logs, rsync works well.
- Create the destination volumes before copying
- Use --numeric-ids to preserve UIDs/GIDs
- Avoid syncing sockets and files in use; focus on _data
| Scenario | Recommended strategy | Inconsistency risk | Notes |
|---|---|---|---|
| Uploads/static files | Two-pass rsync | Low | Stop the app only for the final sync |
| Relational DB (PostgreSQL/MySQL) | Replica or backup+restore | High if using rsync | Use dump/replication; rsync only with service stopped |
| Caches/queues (Redis/RabbitMQ) | Recreate empty | Low | Acceptable to lose cache; drain queues beforehand |
| Bind-mounted configs | Simple rsync | Low | Ensure identical paths in Compose |
# Do host de origem: crie e prepare volumes no destino, então rsync "quente"
DEST="deploy@IP_OU_HOST_DESTINO"
VOLS=(app_uploads db_data redis_data)
ssh -o StrictHostKeyChecking=no ${DEST} 'for v in app_uploads db_data redis_data; do sudo docker volume create $v >/dev/null; sudo mkdir -p /var/lib/docker/volumes/$v/_data; done'
for v in "${VOLS[@]}"; do
sudo rsync -aHAX --numeric-ids --info=progress2 \
-e ssh /var/lib/docker/volumes/${v}/_data/ \
${DEST}:/var/lib/docker/volumes/${v}/_data/
done
Test the destination without exposing public traffic
Start the stack on the destination and validate with curl and healthchecks on localhost or via an SSH tunnel. Do not point DNS yet. If you use standard ports, keep the firewall closed to the public while testing. Adjust environment-specific variables in the destination .env.
- Test dependencies: DB, queues and external storage connectivity
- Verify Compose networks and internal hostnames
- Ensure the correct images were pulled
# Na VPS de destino, no diretório do projeto
cd /opt/meu-stack
sudo docker compose --env-file .env pull
sudo docker compose --env-file .env up -d
sudo docker compose ps
# Testes locais
curl -fsS http://127.0.0.1:80/health || true
sudo docker compose logs --no-color --tail=50
Cutover window: stop writes, final rsync and swap DNS
Now perform the cutover. This is the only planned downtime. Stop services on the source, run a final rsync with --delete and start services on the destination. Be careful: --delete will remove files on the destination that don't exist on the source. Then update DNS and wait for propagation according to your TTL.
- Notify users and monitor real-time metrics
- Have a simple rollback: keep the source intact until validation
- Update tokens and allowlists that rely on IP addresses
# 1) Parar serviços na ORIGEM
cd /opt/meu-stack
sudo docker compose --env-file .env down
# 2) Sincronização FINAL dos volumes (com --delete)
DEST="deploy@IP_OU_HOST_DESTINO"
for v in app_uploads db_data redis_data; do
sudo rsync -aHAX --numeric-ids --delete --info=progress2 \
-e ssh /var/lib/docker/volumes/${v}/_data/ \
${DEST}:/var/lib/docker/volumes/${v}/_data/
done
# 3) Subir no DESTINO
ssh ${DEST} 'cd /opt/meu-stack && sudo docker compose --env-file .env up -d && sudo docker compose ps'
# 4) Trocar DNS (exemplo: verifique resolução)
# Altere o A/AAAA no provedor e valide depois
# (ajuste seu domínio)
dig +short A exemplo.com
How to confirm it worked (post-cut checks)
Validate the application, metrics and logs. Check that no container is restarting in a loop. Monitor 4xx and 5xx errors. Do not decommission the old host until you finish validations and keep a post-migration backup.
- Open the app over HTTPS and verify login and critical flows
- Confirm integrity of uploads and file permissions
- Check CPU/RAM metrics, queues and scheduled jobs
# Na VPS de destino
sudo docker compose ps
sudo docker compose logs --no-color --tail=100 | sed -n '1,100p'
# Endpoints de saúde e páginas críticas (ajuste URLs)
curl -I https://exemplo.com/health
curl -I https://exemplo.com/
Advanced options to reduce downtime further
If you need an almost-instant cutover, put a proxy in front routing to origin and destination and switch traffic based on healthchecks. See the HAProxy + Let's Encrypt and healthchecks guide.
To orchestrate parallel releases per stack, use the blue-green deployment strategy with Docker Compose. To speed image downloads during cutover, a private Docker registry or mirror helps a lot.
- Proxy with healthchecks for fast cutovers
- Blue/green in Compose for safe switchovers
- Registry/mirror to pull images locally
Common troubleshooting
Some issues commonly appear after migrations. The list below shows symptoms and typical fixes. Pay attention to permissions: differing UIDs/GIDs between hosts often break file access for mounted volumes inside containers.
- Permission denied on uploads: chown the _data directory to match the container process UID.
- Containers restarting: check required environment variables in .env and service dependency order.
- Different architecture (amd64/arm64): ensure images are multi-arch and recreate containers on the destination.
- Closed ports: review UFW/security group rules and confirm inbound rules.
- Slow pulls during cutover: configure a local registry mirror.
- Database corrupted after rsync: restore from backup or repeat using dump/replication with the service stopped.
Frequently asked questions
How do I migrate a Docker database safely?
For relational databases use temporary replication or dump and restore. Avoid rsync of the _data directory while the DB is writing. Perform the initial copy with the instance read-only or stopped and validate integrity before the final cutover.
Can I migrate with zero downtime?
Without replication or a proxy there will be a short window. To reduce downtime nearly to zero, use a proxy with healthchecks, prepare the destination, sync data, freeze writes during cutover and atomically switch traffic. Still plan a rollback.
What if the new VPS has a different architecture (arm64 vs amd64)?
Ensure images publish multi-architecture manifests. Recreate containers on the destination. Data volumes are architecture-agnostic, but binaries inside images depend on architecture. Test on the destination before cutover.
How should I handle environment variables and secrets?
Keep .env out of the repo or encrypted. Sync it separately and verify permissions. External secrets (tokens, keys) should be reviewed after IP/DNS changes to avoid allowlist blocks.
Do I have to download all images again?
Not necessarily. A private registry or mirror speeds pulls on the destination. You can also use docker save and docker load, but usually pulling the versioned images via Compose on the new host is simpler.
Want a pre-cutover review?
If you want a second opinion on the cutover plan, critical volumes and rollback strategy, contact me for a tailored checklist for your stack.
Discuss the migrationSee also
- Tutorial: set up mTLS between Traefik and Docker Compose services using smallstep CA and automatic certificates — step by step
- Tutorial: integrate Sentry into your GitHub Actions deploy pipeline — automatic source map upload & issue creation (PHP & JavaScript)
- Configure retention and compression for Docker container logs with journald + logrotate + rsyslog on a low-disk VPS
- Tutorial: Configure GitHub Actions for integration tests with PostgreSQL in ephemeral containers and automatic migration rollback — a practical guide