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:
| Tipo | Dónde se almacenan los datos | Adecuado para |
|---|---|---|
| Volume | Un área administrada por Docker | Bases de datos y datos persistentes de producción |
| Bind mount | Una carpeta del host que tú especificas | Sincronizar código durante el desarrollo y leer archivos de configuración del host |
| tmpfs | Memoria | Datos 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 quepostgres:18no 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
PGDATAahora es/var/lib/postgresql/18/docker.Mountpointendocker volume inspectes 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 -aydocker system prune --volumeseliminan 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 runydocker volume lsya 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 -felimina 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 pruneelimina los volúmenes anónimos que no se están usando. Si añades-val 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

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

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
:ropara 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
-vencuentra 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=bindencuentra 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
pgdataen/dataen modo de solo lectura y la carpeta actual en/backup; usa tar para empaquetar/datacomopgdata.tar.gzy 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_dumpomysqldump. 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,
-vcrea una carpeta vacía;--mountdevuelve 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