Contenido

Tutorial de volúmenes en Docker: persistir datos con volúmenes y bind mounts

Los archivos escritos dentro de un contenedor se guardan en su capa escribible. Cuando eliminas el contenedor, también desaparecen los datos. Los datos que necesitas conservar, como los de una base de datos o los archivos subidos por los usuarios, deben almacenarse fuera del contenedor. Docker ofrece tres formas de montaje:

TipoDónde se almacenan los datosAdecuado para
VolumeUn área administrada por DockerBases de datos y datos persistentes de producción
Bind mountUna carpeta del host que tú especificasSincronizar código durante el desarrollo y leer archivos de configuración del host
tmpfsMemoriaDatos temporales que no quieres escribir en el disco

Volume: elimina el contenedor, los datos siguen ahí

Compruébalo con PostgreSQL. Inicia la base de datos y almacena los datos en un volumen llamado pgdata:

docker run -d --name db -e POSTGRES_PASSWORD=secret -v pgdata:/var/lib/postgresql postgres:18

-v pgdata:/var/lib/postgresql significa que se monta el volumen llamado pgdata en /var/lib/postgresql dentro del contenedor. Si el volumen no existe, Docker lo crea automáticamente.

Crea una tabla, inserta un registro y luego elimina el contenedor. Después, inicia un contenedor nuevo con el mismo volumen:

$ docker run -d --name db -e POSTGRES_PASSWORD=secret -v pgdata:/var/lib/postgresql postgres:18
119ed37ba5c6dc12a773a32d4ef296523529552f94a2ab1ee56113c1b990d7ae

$ docker ps --filter name=db
CONTAINER ID   IMAGE         COMMAND                  CREATED         STATUS         PORTS      NAMES
119ed37ba5c6   postgres:18   "docker-entrypoint.s…"   8 seconds ago   Up 8 seconds   5432/tcp   db

$ docker exec db psql -U postgres -c "CREATE TABLE notes (id serial, body text); INSERT INTO notes (body) VALUES ('hello volume');"
CREATE TABLE
INSERT 0 1

$ docker exec db psql -U postgres -c "SELECT * FROM notes;"
 id |     body
----+--------------
  1 | hello volume
(1 row)


$ docker rm -f db
db

$ docker run -d --name db -e POSTGRES_PASSWORD=secret -v pgdata:/var/lib/postgresql postgres:18
d94fb2966b3dc5790239b4a48d180aedd193f6fc2c846d6878a7c5fcc450f7cd

$ docker exec db psql -U postgres -c "SELECT * FROM notes;"
 id |     body
----+--------------
  1 | hello volume
(1 row)

El contenedor nuevo leyó los datos que había escrito el anterior: están almacenados en el volumen y no dependen del ciclo de vida del contenedor.

Directorio de datos de PostgreSQL 18

Muchas guías antiguas indican -v pgdata:/var/lib/postgresql/data. Esa era la forma de hacerlo hasta PostgreSQL 17, pero hace que postgres:18 no pueda iniciarse:

$ docker run -d --name db -e POSTGRES_PASSWORD=secret -v pgdata:/var/lib/postgresql/data postgres:18
c1cec1074a4d440c1f6d106d994171d290cd8ea9da3266d798c0c81662bd8281

$ docker ps -a --filter name=db
CONTAINER ID   IMAGE         COMMAND                  CREATED         STATUS                     PORTS     NAMES
c1cec1074a4d   postgres:18   "docker-entrypoint.s…"   8 seconds ago   Exited (1) 7 seconds ago             db

$ docker logs db
Error: in 18+, these Docker images are configured to store database data in a
       format which is compatible with "pg_ctlcluster" (specifically, using
       major-version-specific directory names).  This better reflects how
       PostgreSQL itself works, and how upgrades are to be performed.

       See also https://github.com/docker-library/postgres/pull/1259

       Counter to that, there appears to be PostgreSQL data in:
         /var/lib/postgresql/data (unused mount/volume)

       This is usually the result of upgrading the Docker image without
       upgrading the underlying database using "pg_upgrade" (which requires both
       versions).

       The suggested container configuration for 18+ is to place a single mount
       at /var/lib/postgresql which will then place PostgreSQL data in a
       subdirectory, allowing usage of "pg_upgrade --link" without mount point
       boundary issues.

       See https://github.com/docker-library/postgres/issues/37 for a (long)
       discussion around this process, and suggestions for how to do so.

$ docker rm -f db
db

$ docker volume rm pgdata
pgdata

La imagen oficial de PostgreSQL 18 ahora almacena los datos en un subdirectorio separado por versión principal, como /var/lib/postgresql/18/docker, para facilitar futuras actualizaciones con pg_upgrade. La nueva forma consiste en montar el directorio superior /var/lib/postgresql:

$ docker volume ls
DRIVER    VOLUME NAME
local     5775c1d90defc3dc23a92cdde10cd62b182d9c774ae43f0a40595fe62131a63c
local     pgdata

$ docker volume inspect pgdata
[
    {
        "CreatedAt": "2026-10-13T00:09:44Z",
        "Driver": "local",
        "Labels": null,
        "Mountpoint": "/var/lib/docker/volumes/pgdata/_data",
        "Name": "pgdata",
        "Options": null,
        "Scope": "local"
    }
]

$ docker exec db sh -c 'echo $PGDATA; ls /var/lib/postgresql'
/var/lib/postgresql/18/docker
18
  • PGDATA ahora es /var/lib/postgresql/18/docker.
  • Mountpoint en docker volume inspect es la ubicación real del volumen en el host Docker. En Docker Desktop está dentro de la máquina virtual Linux de Docker, por lo que no se puede ver directamente desde el Explorador de archivos de Windows ni desde Finder en macOS. Para hacer una copia de seguridad, usa el método que se explica a continuación.

Administrar volúmenes

docker volume ls                 # Lista todos los volúmenes
docker volume inspect pgdata     # Consulta los detalles
docker volume rm pgdata          # Elimina (solo si ningún contenedor lo usa)
docker volume prune              # Elimina volúmenes anónimos sin usar
docker volume prune -a           # También elimina volúmenes con nombre (¡cuidado!)

docker volume prune -a y docker system prune --volumes eliminan los datos de la base de datos. Comprueba antes de ejecutarlos o haz primero una copia de seguridad.

De dónde salen los volúmenes anónimos

Aunque no uses -v, muchas imágenes de bases de datos siguen creando volúmenes porque sus Dockerfile incluyen la instrucción VOLUME:

$ docker volume ls
DRIVER    VOLUME NAME

$ docker run -d --name anon -e POSTGRES_PASSWORD=secret postgres:18
1db583d1b34d0482cfa458a5fe4a7fc7c1320633bde0339407a0c24fe62ad630

$ docker volume ls
DRIVER    VOLUME NAME
local     0bbe1b19b9c02afcc2619b031ceddb1af1b4b75c3068f28eb64642973b3fd6e8

$ docker inspect --format '{{range .Mounts}}{{.Type}} {{.Name}} -> {{.Destination}}{{println}}{{end}}' anon
volume 0bbe1b19b9c02afcc2619b031ceddb1af1b4b75c3068f28eb64642973b3fd6e8 -> /var/lib/postgresql

$ docker rm -f anon
anon

$ docker volume ls
DRIVER    VOLUME NAME
local     0bbe1b19b9c02afcc2619b031ceddb1af1b4b75c3068f28eb64642973b3fd6e8

$ docker volume prune -f
Deleted Volumes:
0bbe1b19b9c02afcc2619b031ceddb1af1b4b75c3068f28eb64642973b3fd6e8

Total reclaimed space: 40.49MB
  • Solo ejecutaste docker run y docker volume ls ya muestra un volumen anónimo con un nombre formado por una larga cadena aleatoria, montado en /var/lib/postgresql.
  • Después de que docker rm -f elimina el contenedor, el volumen anónimo sigue ahí y ocupa 40 MB. Esta es una de las razones por las que las instalaciones de Docker de muchas personas ocupan cada vez más espacio: estos volúmenes anónimos olvidados.
  • docker volume prune elimina los volúmenes anónimos que no se están usando. Si añades -v al eliminar un contenedor (docker rm -fv anon), también se elimina su volumen anónimo.

Se recomienda usar siempre volúmenes con nombre; así sabrás de un vistazo para qué sirven.

Bind mount: monta una carpeta del host en el contenedor

docker run -d --name site -p 8080:80 -v "$(pwd)/site:/usr/share/nginx/html:ro" nginx:1.30
docker run -d --name site -p 8080:80 -v "${PWD}/site:/usr/share/nginx/html:ro" nginx:1.30
docker run -d --name site -p 8080:80 -v "%cd%/site:/usr/share/nginx/html:ro" nginx:1.30
Página web antes de modificarla
nginx muestra el contenido de site/index.html en el host

Modifica el título de site/index.html en el host con tu editor y guarda los cambios. Luego, actualiza el navegador:

Página web después de modificarla
Sin reconstruir la imagen ni reiniciar el contenedor, actualiza la página y verás los cambios

Este es el uso más habitual durante el desarrollo: editas el código en el host con el editor que prefieras y el contenedor se encarga de ejecutarlo.

Ten en cuenta lo siguiente:

  • La ruta del host debe ser una ruta absoluta. $(pwd) se sustituye por el directorio actual (en Windows PowerShell, usa ${PWD}).
  • Si escribes un nombre en lugar de una ruta (por ejemplo, site:/data), Docker lo interpreta como el nombre de un volumen.
  • En Linux, los archivos que el contenedor escribe como root también pertenecen a root en el host. Puedes usar -u $(id -u):$(id -g) para ejecutar el contenedor con tu identidad.
  • Añade :ro para montar en modo de solo lectura.

Rutas inexistentes con -v y --mount

$ ls -d nothere
ls: nothere: No such file or directory

$ docker run --rm -v "$(pwd)/nothere:/data" alpine:3.23 ls -la /data
total 8
drwxr-xr-x    2 root     root          4096 Oct 13 00:09 .
drwxr-xr-x    1 root     root          4096 Oct 13 00:09 ..

$ ls -ld nothere
drwxr-xr-x    2 root     root          4096 Oct 13 00:09 nothere

$ docker run --rm --mount type=bind,src="$(pwd)/nothere2",dst=/data alpine:3.23 ls /data
docker: Error response from daemon: invalid mount config for type "bind": bind source path does not exist: /work/nothere2

Run 'docker run --help' for more information

Comandos equivalentes en Windows (selecciona una pestaña para cambiar):

docker run --rm -v "$(pwd)/nothere:/data" alpine:3.23 ls -la /data
docker run --rm --mount type=bind,src="$(pwd)/nothere2",dst=/data alpine:3.23 ls /data
docker run --rm -v "${PWD}/nothere:/data" alpine:3.23 ls -la /data
docker run --rm --mount type=bind,src="${PWD}/nothere2",dst=/data alpine:3.23 ls /data
docker run --rm -v "%cd%/nothere:/data" alpine:3.23 ls -la /data
docker run --rm --mount type=bind,src="%cd%/nothere2",dst=/data alpine:3.23 ls /data
  • Si -v encuentra una ruta del host que no existe, crea automáticamente una carpeta vacía (que además pertenece a root). El contenedor se inicia correctamente, pero la carpeta montada está vacía, lo que puede hacerte pensar que «la montaste, pero no hay archivos».
  • Si --mount type=bind encuentra una ruta inexistente, muestra un error directamente, así es más fácil detectar que la ruta está mal escrita.

La sintaxis de --mount es más larga, pero más explícita:

docker run -d \
  --mount type=volume,src=pgdata,dst=/var/lib/postgresql \
  --mount type=bind,src="$(pwd)/init.sql",dst=/docker-entrypoint-initdb.d/init.sql,readonly \
  -e POSTGRES_PASSWORD=secret postgres:18
docker run -d `
  --mount type=volume,src=pgdata,dst=/var/lib/postgresql `
  --mount type=bind,src="${PWD}/init.sql",dst=/docker-entrypoint-initdb.d/init.sql,readonly `
  -e POSTGRES_PASSWORD=secret postgres:18
docker run -d ^
  --mount type=volume,src=pgdata,dst=/var/lib/postgresql ^
  --mount type=bind,src="%cd%/init.sql",dst=/docker-entrypoint-initdb.d/init.sql,readonly ^
  -e POSTGRES_PASSWORD=secret postgres:18

tmpfs: datos en memoria

$ docker run --rm --tmpfs /cache:size=64m alpine:3.23 df -h /cache
Filesystem                Size      Used Available Use% Mounted on
tmpfs                    64.0M         0     64.0M   0% /cache

Los datos solo existen en la memoria y desaparecen cuando se detiene el contenedor. Es adecuado para archivos temporales (solo en contenedores Linux).

Copia de seguridad y restauración de volúmenes

Docker administra la ubicación de los volúmenes. La forma más general de hacer una copia de seguridad es iniciar un contenedor temporal y empaquetar el volumen en un archivo tar:

$ docker run --rm -v pgdata:/data:ro -v "$(pwd):/backup" alpine:3.23 tar czf /backup/pgdata.tar.gz -C /data .

$ ls -lh pgdata.tar.gz
-rw-r--r--    1 root     root        4.6M Oct 13 00:10 pgdata.tar.gz

$ docker volume create pgdata-restore
pgdata-restore

$ docker run --rm -v pgdata-restore:/data -v "$(pwd):/backup" alpine:3.23 tar xzf /backup/pgdata.tar.gz -C /data

$ docker run --rm -v pgdata-restore:/data alpine:3.23 ls /data/18/docker
PG_VERSION
base
global
pg_commit_ts
pg_dynshmem
… (salida omitida)

$ rm pgdata.tar.gz

Equivalente en Windows (selecciona una pestaña para cambiar):

docker run --rm -v pgdata:/data:ro -v "$(pwd):/backup" alpine:3.23 tar czf /backup/pgdata.tar.gz -C /data .
docker run --rm -v pgdata-restore:/data -v "$(pwd):/backup" alpine:3.23 tar xzf /backup/pgdata.tar.gz -C /data
docker run --rm -v pgdata:/data:ro -v "${PWD}:/backup" alpine:3.23 tar czf /backup/pgdata.tar.gz -C /data .
docker run --rm -v pgdata-restore:/data -v "${PWD}:/backup" alpine:3.23 tar xzf /backup/pgdata.tar.gz -C /data
docker run --rm -v pgdata:/data:ro -v "%cd%:/backup" alpine:3.23 tar czf /backup/pgdata.tar.gz -C /data .
docker run --rm -v pgdata-restore:/data -v "%cd%:/backup" alpine:3.23 tar xzf /backup/pgdata.tar.gz -C /data
  • Primer comando: monta pgdata en /data en modo de solo lectura y la carpeta actual en /backup; usa tar para empaquetar /data como pgdata.tar.gz y guardarlo en la carpeta actual del host.
  • Para restaurarlo, crea un volumen nuevo y descomprímelo con el mismo método. En el volumen restaurado puedes ver los archivos de datos de PostgreSQL dentro de 18/docker.

Empaquetar directamente los archivos de una base de datos en ejecución puede producir datos incoherentes. En el ejemplo anterior, la copia se hace con la base de datos detenida. Para las bases de datos, se recomienda usar sus propias herramientas de exportación, como pg_dump o mysqldump. Consulta Ejecutar PostgreSQL con Docker.

¿Cuál conviene usar?

  • Bases de datos y datos que deban conservarse a largo plazo → Volume
  • Para ver los cambios en el código al instante durante el desarrollo → Bind mount
  • Para que un contenedor lea archivos de configuración del host → Bind mount (solo lectura)
  • Para datos temporales que no necesitas conservar → tmpfs o escribirlos directamente en el contenedor

Resumen

  • Usa volúmenes con nombre para los datos que deban conservarse, como los de una base de datos.
  • Durante el desarrollo, usa Bind mount para que el contenedor utilice directamente el código del host.
  • Si la ruta del host no existe, -v crea una carpeta vacía; --mount devuelve un error directamente.
  • Para hacer una copia de seguridad de un volumen, monta el volumen y una carpeta del host a la vez en un contenedor temporal y empaquétalos con tar.

Ejercicios

Ejercicio 1: Crea el volumen notes, escribe un archivo con un contenedor, elimina el contenedor y léelo con otro.

Respuesta
docker volume create notes
docker run --rm -v notes:/data alpine:3.23 sh -c "echo hello > /data/a.txt"
docker run --rm -v notes:/data alpine:3.23 cat /data/a.txt

La salida es hello. Consulta los comandos de administración de volúmenes en docker volume.

Ejercicio 2: ¿Cuál es la diferencia entre docker run -v data:/app y docker run -v ./data:/app?

Respuesta

data no contiene una barra, por lo que se interpreta como un nombre de volumen. ./data es una ruta y se interpreta como Bind mount; monta data, que está dentro de la carpeta actual del host.

Preguntas frecuentes

¿Dónde se almacenan los datos de un volumen?

En Linux, en /var/lib/docker/volumes/nombre/_data. En Docker Desktop, los volúmenes están en su máquina virtual Linux y no se pueden ver en el Explorador de archivos de Windows ni en Finder. Puedes consultarlos en la pantalla Volumes de Docker Desktop.

¿Cómo se escribe la ruta del host para -v en Windows?

En PowerShell, usa "${PWD}/site:/app"; en CMD, usa "%cd%/site:/app". Los comandos de este capítulo incluyen pestañas con las tres formas.

¿Los permisos de los archivos de un bind mount son incorrectos?

En Linux, los archivos que el contenedor escribe como root también pertenecen a root en el host. Usa -u $(id -u):$(id -g) para ejecutar el contenedor con tu identidad.

Actualizado 14 de octubre de 2026