Migrating a Docker named volume to a host path moves persistent container data into a bind mount that can be inspected, backed up, and managed with normal host tools. The migration is complete only when the workload starts again from that host directory instead of the original volume.
The Docker-side workflow is to confirm the current mount target, stop the writer, copy the named volume into a host directory from a short-lived helper container, and recreate the workload with a bind mount that uses the same container path. Keeping the container path unchanged lets the application find the migrated files where it already expects them.
Unlike a named volume, a bind mount does not seed an empty target path with files from the image, and mounting over a populated container directory hides the image-side files underneath it. Copy the data before switching the mount, use a new or intentionally empty host directory, and keep the original named volume until the restarted workload passes its own health check.
$ docker container inspect --format '{{range .Mounts}}{{.Type}} {{if .Name}}{{.Name}}{{else}}{{.Source}}{{end}} {{.Destination}}{{println}}{{end}}' notes-app volume notes-data /var/lib/notes
The last field is the container path that the replacement bind mount must keep. Related: How to inspect container details in Docker
$ docker container stop notes-app notes-app
Do not remove the original volume yet. Keep it available until the recreated workload starts cleanly and the application-level check succeeds.
$ sudo mkdir -p /srv/notes-data
The path must exist on the machine that runs the Docker daemon. On Docker Desktop, use a local path that is shared with Docker.
Use a new or intentionally empty directory, because copying into a populated host path can leave old files mixed with the migrated data.
$ docker run --rm --mount source=notes-data,target=/from,readonly --mount type=bind,src=/srv/notes-data,dst=/to alpine cp -a /from/. /to
Docker currently prefers --mount over -v because it is more explicit and supports the full set of mount options.
$ ls -al /srv/notes-data total 16 drwxr-xr-x 3 root root 4096 Apr 16 09:07 . drwxr-xr-x 3 root root 4096 Apr 16 09:06 .. -rw-r--r-- 1 root root 16 Apr 16 09:07 app.env drwxr-xr-x 2 root root 4096 Apr 16 09:07 uploads
If ownership must match a non-root container user, correct it on the host before the container starts again.
$ docker container rm notes-app
notes-app
Mount definitions are fixed when the container is created, so switching from a named volume to a bind mount requires recreation.
$ docker run -d --name notes-app --restart unless-stopped --mount type=bind,src=/srv/notes-data,dst=/var/lib/notes <original-image>
Reuse the original ports, environment variables, networks, command, and restart policy here. Only the storage definition should change.
The bind mount hides any files baked into the image at /var/lib/notes, so copy the original volume contents first and keep the same destination path.
$ docker container inspect --format '{{range .Mounts}}{{.Type}} {{if .Name}}{{.Name}}{{else}}{{.Source}}{{end}} {{.Destination}}{{println}}{{end}}' notes-app bind /srv/notes-data /var/lib/notes
$ docker exec notes-app ls -al /var/lib/notes total 8 drwxr-xr-x 4 root root 128 Apr 16 09:07 . drwxr-xr-x 1 root root 4096 Apr 16 09:09 .. -rw-r--r-- 1 root root 16 Apr 16 09:07 app.env drwxr-xr-x 3 root root 96 Apr 16 09:07 uploads
After this Docker-level check succeeds, run the application's own health endpoint, login flow, or smoke test before removing the original volume.