Docker gives you several ways to keep data around when containers are recreated. Two of the most common are
named Docker volumes and
bind mounts.
A named volume is managed by Docker, which in your
docker-compose.yml could look like this:
volumes:
- postgres-data:/var/lib/postgresql/data
A bind mount maps an ordinary directory on the host into the container:
volumes:
- ./postgres-data:/var/lib/postgresql/data
Both approaches have their advantages. For self-hosted applications, however, there are situations where you may want to move from Docker-managed volumes to directories you control directly.
This guide covers that migration.
Docker Volumes vs Bind Mounts
Before migrating anything, it is worth understanding what actually changes.
Docker named volumes
With a named volume, Docker manages the storage location.
Small
docker-compose.yml example below:
services:
postgres:
image: postgres:16-alpine
volumes:
- postgres-data:/var/lib/postgresql/data
volumes:
postgres-data:
The application sees
/var/lib/postgresql/data, but the actual files are stored in Docker's volume storage.
Bind mounts
With a bind mount, you explicitly choose the host directory:
services:
postgres:
image: postgres:16-alpine
volumes:
- ./postgres-data:/var/lib/postgresql/data
If your Compose project is located at:
/var/lib/docker/dockerProject
then the PostgreSQL data will be stored at:
/var/lib/docker/dockerProject/postgres-data
That difference is the main reason someone might want to migrate.
Docker Volumes - Pros and Cons
| Pros |
Cons |
| Managed by Docker |
Data is hidden away in Docker's storage |
| Simple to configure |
Less convenient to inspect manually |
| Good container isolation |
Backups need to account for Docker volumes |
| Portable Compose configuration |
Moving data to another filesystem requires additional steps |
| No need to manage host directories |
Less obvious where application data physically lives |
Bind Mounts - Pros and Cons
| Pros |
Cons |
| Data lives exactly where you specify |
Host filesystem permissions matter |
| Easy to inspect with normal Linux tools |
Less portable between hosts |
| Easy to include in filesystem backups |
Host users can directly modify the files |
| Easy to move to another disk |
You are responsible for managing the directories |
| Project and persistent data can live together |
Incorrect manual changes can damage application data |
Why Move From a Docker Volume to a Bind Mount?
There is no technical requirement to make this change. If Docker-managed volumes work well for you, there is nothing wrong with keeping them.
A bind mount can nevertheless make sense if you want more direct control over your data.
For example, I prefer self-hosted projects to look something like this:
dockerProject/
-rw-rw-r-- 1 2645 Sep 19 22:32 docker-compose.yml
drwxr-xr-x 4 4096 Sep 8 23:20 minio-data
drwx------ 19 4096 Sep 20 08:10 postgres-data
drwxr-xr-x 2 4096 Sep 19 22:33 redis-data
.. Or even a dedicated "data" folder than contains all of the
*-data directories above.
When I need to back up, inspect or move the project, I know exactly where the persistent data is.
This can also fit nicely into an existing rsync-based backup system because the data is simply part of the normal filesystem.
Important: Don't Just Change the Compose File
This is the most important part of the migration, really.
Suppose you currently have:
volumes:
- postgres-data:/var/lib/postgresql/data
and change it to:
volumes:
- ./postgres-data:/var/lib/postgresql/data
Docker will not automatically copy the existing database into
./postgres-data.
The old Docker volume still contains your database.
The new directory is initially empty.
You therefore need to copy the contents of the existing volume into the new directory before starting the application with the new configuration.
1. Find the Existing Volume
Start by finding the Docker volumes:
docker volume ls
If your project has a recognizable name, you can narrow the result:
docker volume ls | grep dockerProject
For example:
local dockerProject_postgres-data
local dockerProject_redis-data
local dockerProject_minio-data
Use the actual names returned by Docker. Do not assume that your volume names will be identical to these examples.
2. Stop the Project
Stop the Compose project before copying database data:
docker compose down
This removes the containers but keeps the named volumes.
Do not use docker compose down -v.
The
-v option removes the volumes, which would defeat the purpose of this migration.
3. Create the New Directories
Create the directories that will become your bind mounts - from the path where you want to store, in this example we use the directory of
docker-compose.yml:
mkdir -p postgres-data redis-data minio-data
Because these paths begin with
./, they are relative to the directory containing your Compose file.
4. Copy the Docker Volume to the New Directory
A convenient way to copy a Docker volume is to temporarily mount it into a small Alpine Linux container.
For PostgreSQL:
docker run --rm \
-v dockerProject_postgres-data:/from:ro \
-v "$PWD/postgres-data:/to" \
alpine \
sh -c 'cp -a /from/. /to/'
Redis:
docker run --rm \
-v dockerProject_redis-data:/from:ro \
-v "$PWD/redis-data:/to" \
alpine \
sh -c 'cp -a /from/. /to/'
MinIO:
docker run --rm \
-v dockerProject_minio-data:/from:ro \
-v "$PWD/minio-data:/to" \
alpine \
sh -c 'cp -a /from/. /to/'
The
:ro flag makes the old volume read-only inside the temporary container.
That means the migration command can read from the original volume but cannot modify it.
5. Check the Migrated Data
Check the size of the new directories:
du -sh postgres-data redis-data minio-data
You may need
sudo for PostgreSQL:
sudo du -sh postgres-data redis-data minio-data
You can also inspect the contents:
sudo ls -la postgres-data
ls -la redis-data
find minio-data -maxdepth 2 -type f | head
A PostgreSQL data directory should contain things such as:
PG_VERSION
base/
global/
pg_wal/
postgresql.auto.conf
The exact contents depend on your PostgreSQL version and configuration.
6. Change Docker Compose
Once the data has been copied, change the Compose configuration.
Before:
services:
postgres:
image: postgres:16-alpine
volumes:
- postgres-data:/var/lib/postgresql/data
volumes:
postgres-data:
After:
services:
postgres:
image: postgres:16-alpine
volumes:
- ./postgres-data:/var/lib/postgresql/data
In this example we also have Redis and MinIO - do the same thing:
services:
redis:
image: redis:7-alpine
volumes:
- ./redis-data:/data
minio:
image: minio/minio:latest
volumes:
- ./minio-data:/data
The top-level
volumes: section is no longer required for these mounts, so it can be removed.
7. Start the Project
Start the project again:
docker compose up -d
Check the containers:
docker compose ps
For PostgreSQL, check the logs if necessary:
docker compose logs postgres --tail=50
You want to see PostgreSQL successfully starting and accepting connections.
8. Verify the Application
Don't stop at checking that the containers are running.
Actually use the application - ensure everything is as it should be. We haven't
deleted any data, so it's still possible to revert in the
docker-compose.yml file.
This confirms that Docker is actually using the copied bind-mounted data rather than accidentally starting with an empty directory.
9. Keep the Old Volume for a While
Do not immediately delete the old volume.
This gives you a fallback if you discover something went wrong with the migration.
You can see the old volumes with:
docker volume ls
Once you have verified that everything works correctly from the new directories, the old volumes can eventually be removed:
docker volume rm dockerProject_postgres-data
docker volume rm dockerProject_redis-data
docker volume rm dockerProject_minio-data
Only remove them once you are certain you no longer need the old copies.
What About Backups?
One of the main reasons I prefer bind mounts is that they fit naturally into a normal filesystem backup strategy.
Instead of having to specifically handle a Docker volume, the backup system can simply include the project directory:
/var/lib/docker/dockerProject/
That means the persistent directories are ordinary filesystem data.
However, there is an important distinction for databases.
A filesystem copy of a running PostgreSQL data directory should not automatically be treated as equivalent to a proper PostgreSQL backup. For important databases, consider using a database-aware backup such as
pg_dump in addition to your filesystem backup.
Conclusion
Moving from Docker named volumes to bind mounts is relatively straightforward once you understand that changing the Compose file does not migrate the data for you.
The basic process is:
- Find the existing Docker volume.
- Stop the application.
- Create the new host directory.
- Copy the volume contents into it.
- Change the Compose file to use the bind mount.
- Start the application.
- Verify everything works.
- Keep the original volume until you're satisfied with the migration.
The end result is a Docker project where the persistent data is stored directly alongside the project, making it easier to inspect, move and include in filesystem-level backups.
There is nothing inherently wrong with Docker volumes, though. They remain an excellent option when you prefer Docker to manage persistent storage. Bind mounts simply give you a different trade-off: more direct control over the host filesystem in exchange for more responsibility.
Comments (0)
No comments yet. Be the first to comment!
Leave a Comment