Tutorial: Move large Docker volumes to another disk on the same host with minimal downtime (rsync + bind‑mount + Docker Compose)
Docker & Containers · By Kevin Marcondes · Published on · 8 min read
Need a safe how-to to move large Docker volumes to another disk on the same host with minimal downtime? This guide shows a hot initial copy with rsync, a short stop for final sync with --delete, and how to update Docker Compose to use bind‑mounts on the new disk.
Prerequisites, warnings and scenario
You will copy data from existing Docker volumes to a locally mounted new disk and reconfigure Docker Compose to use bind‑mounts. Back up before you start and schedule a short maintenance window. Commands require root or sudo access.
If you need a backup plan with snapshots and incremental uploads, see LVM snapshots + Restic to S3. If you plan to migrate to another VPS instead, follow this guide: Migrate a Docker host to another VPS.
- New disk already mounted, e.g. at /mnt/docker-data
- Docker and Docker Compose v2 available (command: docker compose)
- Short maintenance window for the final synchronization
# Verifique espaço e pontos de montagem
sudo df -hT | sort -k6
sudo lsblk -f
# Confirme que o novo disco está montado (ex.: /mnt/docker-data)
mount | grep /mnt/docker-data || echo "Monte o novo disco antes de prosseguir"
1) Map current volumes and paths
Find which volumes the project uses and where they are on the host. Set PROJECT to your compose prefix — usually the folder name.
Note the Mountpoint for each volume. That's the source path that will be copied to the new disk.
# Defina o prefixo do projeto (veja com `docker compose ps`)
PROJECT=myapp
# Liste volumes do projeto
vols=$(docker volume ls --format '{{.Name}}' | grep "^${PROJECT}_")
echo "$vols" | sed 's/^/volume: /'
# Veja o caminho (Mountpoint) de cada volume
for v in $vols; do
mp=$(docker volume inspect -f '{{.Mountpoint}}' "$v")
printf '%s -> %s\n' "$v" "$mp"
done
2) Prepare the destination on the new disk
Create the directory structure on the new disk. We use DEST as the base path. rsync will preserve owners, ACLs and links.
You can adjust names or group volumes by project to keep things organized.
# Caminho base no novo disco
echo "Defina onde o novo disco está montado (ex.: /mnt/docker-data)"
DEST=/mnt/docker-data
sudo mkdir -p "$DEST"
# Crie diretórios correspondentes aos volumes
for v in $vols; do
sudo mkdir -p "$DEST/$v"
done
# Permissões serão preservadas pelo rsync (-aHAX --numeric-ids)
3) First hot sync with rsync
Do the initial copy while containers are running to transfer most of the data. Changes made during this step will be reconciled in the final sync when services are stopped.
- -a: preserves attributes
- -HAX: preserves hardlinks, ACLs and xattrs (if present)
- --numeric-ids: keeps exact UIDs and GIDs
- --info=progress2: show continuous progress
for v in $vols; do
src=$(docker volume inspect -f '{{.Mountpoint}}' "$v")
dst="$DEST/$v"
echo "Sync: $src -> $dst"
sudo rsync -aHAX --numeric-ids --info=progress2 "$src/" "$dst/"
done
4) Short maintenance: stop services and final sync
Now perform a brief stop for consistency and a final sync with --delete. Databases and other write‑heavy services must be stopped to guarantee integrity.
You may stop only the affected services or the whole project depending on dependencies.
- Use docker compose stop for a short downtime
- --delete removes files deleted on the source
- Keep the old volume data intact for rollback until you're confident
# Pare os serviços afetados (ou todo o projeto)
# No diretório do compose:
docker compose stop
# Segunda sincronização (curta), agora com os containers parados
for v in $vols; do
src=$(docker volume inspect -f '{{.Mountpoint}}' "$v")
dst="$DEST/$v"
echo "Final sync: $src -> $dst"
sudo rsync -aHAX --numeric-ids --delete --info=progress2 "$src/" "$dst/"
done
5) Update Docker Compose to use bind‑mounts
To keep services unchanged, redefine the named volumes with driver local and bind options that point to the new path. That way, a named volume like myapp_xxx-data will mount the directory on the new disk.
Alternative: replace the volume in the service with a direct bind (source: /mnt/... target: /path). If you use SELinux, consider labeling the path or using compatible bind options.
# docker-compose.yml (trecho)
services:
app:
image: your/image
volumes:
- myapp_app-data:/var/www/html/storage
db:
image: postgres:16
volumes:
- myapp_db-data:/var/lib/postgresql/data
volumes:
myapp_app-data:
driver: local
driver_opts:
type: none
o: bind
device: /mnt/docker-data/myapp_app-data
myapp_db-data:
driver: local
driver_opts:
type: none
o: bind
device: /mnt/docker-data/myapp_db-data
6) Bring services up and validate mounts on the new disk
Start the services and confirm mounts point to the new disk. Then validate that apps and databases start correctly.
If you prefer a GUI for inspection, see how to use Portainer.
# No diretório do compose
docker compose up -d
# Verifique que os mounts apontam para o novo caminho
# Substitua "app" e "db" pelos nomes reais dos containers
for c in app db; do
docker inspect "$c" -f '{{range .Mounts}}{{.Destination}} -> {{.Source}} ({{.Type}}){{println}}{{end}}'
done
# Confira espaço e uso no novo disco
sudo df -hT /mnt/docker-data
sudo du -sh /mnt/docker-data/* | sort -h
7) How to confirm it worked and common fixes
Try a write test in a mounted directory and check the file appears on the new disk. Below are common issues and how to fix them.
For general server safety, review this VPS security checklist.
- Permission denied: find the container's expected UID:GID and chown the destination accordingly.
- Service restarting in a loop: inspect logs with docker compose logs -f and check configured paths.
- Docker rootless: volume roots differ from /var/lib/docker — use docker info to locate and adapt rsync targets.
- Large volumes and slowness: run the initial copy during low load and consider limiting rsync I/O.
- Databases: do the final sync with the service stopped and validate DB integrity afterwards.
# Teste de escrita (ex.: diretório de uploads do app)
docker exec app sh -c 'echo ok > /var/www/html/storage/.rw-test && ls -l /var/www/html/storage/.rw-test'
# Ajustar donos se necessário (ex.: UID 1000:1000)
sudo chown -R 1000:1000 /mnt/docker-data/myapp_app-data
# Logs para diagnóstico
docker compose logs -f --tail=200
8) Cleanup and safe rollback
Before removing the old content under /var/lib/docker/volumes, run several days in production and monitor. Deleting data is permanent.
To roll back, revert the compose file to the original volumes and bring services up. Keeping a tarball of the old content gives you a simple fallback.
- Keep a backup of the old state for an agreed retention period.
- Remove old data only after validating integrations and reports.
- Rollback: restore the original compose and start the services.
# Backup dos volumes antigos (ajuste o prefixo)
cd /var/lib/docker/volumes
sudo tar -czf /root/backup-old-volumes-$(date +%F).tgz myapp_*
# Remoção definitiva (após validação)
# sudo rm -rf /var/lib/docker/volumes/myapp_*
Frequently asked questions
Can I move Docker volumes with zero downtime?
You can minimize downtime by doing an initial hot copy and a short final sync while services are stopped. Absolute zero downtime is unrealistic for actively writing services such as databases.
Is this safe for databases like PostgreSQL and MySQL?
Yes, provided the final synchronization runs while the database service is stopped. The initial copy speeds up transfer; the final rsync --delete with the service down ensures consistency. After startup, validate DB integrity and run your usual checks.
Do I need to change all services in docker‑compose.yml?
Not necessarily. You can redefine named volumes with driver local and driver_opts (type=none, o=bind, device=/mnt/...) so services keep the same volume names while pointing to the new disk.
How do I discover which volumes to move first?
List project volumes with docker volume ls and filter by your compose prefix. Use docker volume inspect to get each Mountpoint. Prioritize large consumers like uploads, persistent caches and database data.
I use Docker rootless. Does the process change?
The physical volume paths differ from /var/lib/docker. Use docker info to find the root for rootless Docker and adjust your inspect and rsync targets. The overall flow with hot copy, final sync and bind‑mounts remains the same.
Can I keep named volumes while using another disk?
Yes. Define the volumes with driver local and driver_opts (type=none, o=bind, device=/mnt/...) so services continue referencing the same names while the storage resides on the new path.
Want us to review your plan before running it in production?
Send your list of services and volumes. We can review risks, recommend a downtime window and provide a validation checklist for your environment.
Discuss the volume 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