Move Docker Data or App Storage to a Larger Disk Without Losing Services
When a VPS disk starts filling up, the right fix depends on what is actually full. Learn when to move Docker’s whole data root, when to move one bind mount, and when named volumes need their own migration plan.
How to identify whether your problem is Docker image storage, bind-mounted app data, or named volumes, then move the right thing with verification and rollback in mind.
Small VPS setups, homelabs, and self-hosted apps that outgrew the original disk or need a cleaner storage layout.
People often move the wrong layer of storage first, or copy live data without stopping writes, which creates confusing partial migrations.
Before you begin
- A second disk, larger mounted volume, or new filesystem path already prepared.
- Sudo access on the server.
- A backup path for important databases, uploads, or app state.
- Enough downtime tolerance to stop writes during the actual data copy.
This topic becomes dangerous when people treat all Docker storage as one thing. It is not one thing. You need to identify which of these you are actually moving:
- Docker data-root: Docker’s own storage under paths like
/var/lib/docker, including images, layers, containers, and named volumes. - Bind mounts: Host directories you mapped into containers, such as
./data:/app/dataor/srv/myapp/uploads:/uploads. - Named volumes: Docker-managed volumes declared in Compose, such as
db-data:.
Step 1: Identify what is actually consuming space
Start with a simple audit:
df -h
docker system df
sudo du -sh /var/lib/docker
sudo du -sh /srv /opt /home/* 2>/dev/nullThen inspect your Compose files for bind mounts and named volumes:
docker compose configUse the results to decide:
- If images, container layers, and Docker internals are filling the disk, consider moving the Docker data-root.
- If one app’s uploads or media directory is huge and already lives in a host path, move the bind mount.
- If database data lives in a Docker-managed volume, plan a named volume migration instead of changing the whole engine first.
This decision matters because moving the whole Docker data-root is broader and riskier than moving one app directory.
Step 2: Move Docker’s data-root when the whole engine is cramped
Use this only when the main problem is /var/lib/docker itself and you want Docker’s core storage on a new disk.
First, stop every Docker-managed workload that might still be writing to disk, then stop Docker itself:
docker compose down
sudo systemctl stop docker
sudo systemctl stop containerdIf this host runs more than one Compose project, stop the others too before copying /var/lib/docker. The goal is no active container writes anywhere on the host while the move happens.
Create the target directory on the larger disk, for example:
sudo mkdir -p /mnt/docker-dataCopy the existing data with permissions preserved:
sudo rsync -aHAX --info=progress2 /var/lib/docker/ /mnt/docker-data/Configure Docker to use the new location by editing or creating /etc/docker/daemon.json:
{
"data-root": "/mnt/docker-data"
}Then start Docker again:
sudo systemctl start containerd
sudo systemctl start dockerVerify the new root:
docker info | grep 'Docker Root Dir'Expected outcome: Docker now reports the new root directory, and your containers can start normally from the copied data.
Do not delete the old /var/lib/docker immediately. Rename it or keep it untouched until you finish verification and are confident rollback is not needed.
Step 3: Move bind-mounted app storage when only one app path is the problem
This is often the lowest-risk option. Suppose your Compose file contains:
services:
app:
volumes:
- /srv/myapp/uploads:/app/uploadsIf /srv/myapp/uploads is too large for the current disk, stop the app first:
docker compose stop appCreate the new destination and copy the data:
sudo mkdir -p /mnt/bigdisk/myapp/uploads
sudo rsync -aHAX --info=progress2 /srv/myapp/uploads/ /mnt/bigdisk/myapp/uploads/Then update the Compose file:
services:
app:
volumes:
- /mnt/bigdisk/myapp/uploads:/app/uploadsStart the app and verify it still sees the files:
docker compose up -d app
docker compose logs --tail=50 appThis method avoids touching unrelated images, containers, or volumes.
Step 4: Move named volumes when Docker manages the app data
Named volumes are not edited by changing a host path in Compose. They need a migration plan. Example volume:
volumes:
db-data:Find the volume and inspect it:
docker volume ls
docker volume inspect yourproject_db-dataFor a recovery-first move, create a new volume and copy data between old and new with a temporary helper container after stopping the app that writes to it:
docker compose stop db
docker volume create db-data-new
docker run --rm \
-v yourproject_db-data:/from \
-v db-data-new:/to \
alpine sh -c "cd /from && cp -a . /to"Then change the Compose file to use the new volume name if needed, or adjust the project layout so the new volume is the one mounted.
If your goal is actually to place named volumes on a different disk long-term, moving Docker’s data-root may be the more coherent solution because Docker stores named volumes under its root.
Database note: for Postgres, MySQL, MariaDB, and similar services, take a logical backup before volume migration even if you also copy raw files. That gives you a fallback if the raw copy turns out inconsistent.
Step 5: Verify the result and keep rollback simple
After any storage move, verify more than just “container started”:
docker compose ps
docker compose logs --tail=100
docker exec -it your-container ls /path/you-expect
Check that:
- The app sees expected files in the new path.
- Uploads, media, or database records are present.
- Disk usage now reflects the new storage location.
- No permission or ownership problems appeared during the copy.
Useful verification commands:
df -h
docker info | grep 'Docker Root Dir'
sudo ls -lah /mnt/bigdisk/myapp/uploadsRecovery rules:
- Keep the original data untouched until verification is complete.
- If the app fails after the switch, stop it and point back to the old path or old volume first.
- Only delete the old data after you have tested the app thoroughly and preferably completed one backup cycle from the new location.
Troubleshooting common Docker storage migration problems
The app starts but files are missing.
You may have copied the wrong directory level, or the new mount path points somewhere empty. Compare the old and new paths carefully.
Permissions broke after the move.
Check ownership and mode bits on the new filesystem. Some apps expect a specific UID/GID inside the mounted path.
Docker will not start after changing data-root.
Validate /etc/docker/daemon.json syntax, confirm the target path exists, and inspect journalctl -u docker for startup errors.
The named volume copy completed, but the database is corrupt.
That usually means the copy happened while writes were still in progress, or the database expected a clean shutdown. Restore from the logical backup and retry with the service fully stopped.
I moved everything when only one app was large.
That is a common overreaction. If the issue is isolated, moving a bind mount is often simpler and safer than relocating Docker’s full storage root.
What to do next
Once storage is stable again, the next common source of accidental outages is DNS changes made without a full record inventory. Continue with Set Up DNS Records for a VPS App Without Breaking Email or Existing Traffic.
