Todo lo que un contenedor escribe en su sistema de archivos desaparece cuando el contenedor se elimina, algo que ocurre cada vez que actualizas su imagen. Para bases de datos, archivos subidos o cualquier dato que deba sobrevivir, Docker ofrece volúmenes, bind mounts y montajes tmpfs. En este tutorial usarás una base de datos PostgreSQL en Ubuntu 24.04 para comprobar cómo funciona cada tipo, cuándo elegir uno u otro, y cómo hacer y restaurar copias de seguridad de un volumen.

Requisitos previos

  • Un servidor con Ubuntu 24.04 LTS y Docker Engine instalado desde el repositorio oficial, por ejemplo un VPS de CubePath.
  • Un usuario no root con privilegios sudo y miembro del grupo docker.
  • Conocer los comandos básicos de Docker (docker run, docker ps, docker exec).

Tipos de almacenamiento en Docker

TipoDónde viven los datosUso recomendado
Volumen con nombre/var/lib/docker/volumes/, gestionado por DockerDatos de aplicaciones y bases de datos en producción
Bind mountCualquier ruta del host que elijasArchivos de configuración, código en desarrollo
tmpfsMemoria RAM, nunca en discoDatos temporales o sensibles que no deben persistir

Los volúmenes son la opción por defecto: Docker gestiona sus permisos, funcionan igual en cualquier servidor, se listan y respaldan con sus propios comandos y no dependen de la estructura de directorios del host.

Paso 1: Comprobar que los datos del contenedor no persisten

Arranca un contenedor, escribe un archivo y elimínalo:

docker run --name prueba alpine sh -c 'echo hola > /datos.txt'
docker rm prueba

Crea otro contenedor a partir de la misma imagen y busca el archivo:

docker run --rm alpine cat /datos.txt
cat: can't open '/datos.txt': No such file or directory

Cada contenedor tiene su propia capa de escritura, que se borra con él. Los datos deben ir a un montaje.

Paso 2: Crear y usar un volumen con nombre

Crea un volumen para los datos de PostgreSQL:

docker volume create pgdata

Compruébalo y consulta dónde lo guarda Docker:

docker volume inspect pgdata
[
    {
        "CreatedAt": "2026-09-25T10:20:14Z",
        "Driver": "local",
        "Labels": null,
        "Mountpoint": "/var/lib/docker/volumes/pgdata/_data",
        "Name": "pgdata",
        "Options": null,
        "Scope": "local"
    }
]

No modifiques archivos directamente en Mountpoint; accede a los datos siempre a través de un contenedor.

Arranca PostgreSQL montando el volumen en su directorio de datos. La sintaxis --mount es más explícita que -v y es la que recomienda Docker:

docker run -d --name pg \
  -e POSTGRES_PASSWORD=your_strong_password \
  --mount type=volume,src=pgdata,dst=/var/lib/postgresql/data \
  postgres:17

Sustituye your_strong_password por una contraseña segura.

Espera unos segundos a que arranque y crea una tabla con un registro:

docker exec pg psql -U postgres -c "CREATE TABLE notas (texto text); INSERT INTO notas VALUES ('dato persistente');"
CREATE TABLE
INSERT 0 1

Paso 3: Verificar que los datos sobreviven al contenedor

Elimina el contenedor por completo:

docker rm -f pg

Crea uno nuevo con el mismo volumen. Como el directorio de datos ya está inicializado, PostgreSQL arranca con la base de datos existente:

docker run -d --name pg \
  -e POSTGRES_PASSWORD=your_strong_password \
  --mount type=volume,src=pgdata,dst=/var/lib/postgresql/data \
  postgres:17

Consulta la tabla:

docker exec pg psql -U postgres -c "SELECT * FROM notas;"
      texto
------------------
 dato persistente
(1 row)

Este es el mismo proceso que sigues al actualizar una imagen: eliminas el contenedor, descargas la nueva versión y lo recreas con el mismo volumen.

Un detalle útil: si montas un volumen vacío en un directorio de la imagen que ya contiene archivos, Docker copia esos archivos al volumen. Los bind mounts no hacen esto; ocultan el contenido original del directorio.

Paso 4: Montar archivos del host con bind mounts

Un bind mount monta una ruta concreta del host dentro del contenedor. Es la forma natural de proporcionar un archivo de configuración que editas desde el servidor. Crea un directorio con una página para Nginx:

mkdir -p ~/sitio
echo '<h1>Servido desde el host</h1>' > ~/sitio/index.html

Móntalo en modo solo lectura en el directorio que sirve Nginx:

docker run -d --name web -p 127.0.0.1:8080:80 \
  --mount type=bind,src="$HOME/sitio",dst=/usr/share/nginx/html,readonly \
  nginx:stable

Comprueba la respuesta:

curl http://127.0.0.1:8080
<h1>Servido desde el host</h1>

Modifica el archivo en el host y vuelve a lanzar curl: el cambio se ve al instante, sin reiniciar el contenedor.

Ten en cuenta estas diferencias con los volúmenes:

  • La ruta de origen debe ser absoluta. Con --mount, si no existe, Docker devuelve un error; con -v, crea un directorio vacío propiedad de root, lo que suele ocultar un error tipográfico.
  • Los permisos son los del host. Si el proceso del contenedor se ejecuta con un UID sin permiso de lectura o escritura sobre esa ruta, falla con Permission denied.
  • El contenedor puede modificar archivos del host si no montas con readonly.

Limpia el ejemplo:

docker rm -f web

Paso 5: Usar tmpfs para datos temporales

Un montaje tmpfs vive solo en memoria y desaparece al detener el contenedor. Es adecuado para cachés o archivos temporales que no quieres escribir en disco. Limita siempre su tamaño:

docker run --rm --mount type=tmpfs,dst=/cache,tmpfs-size=64m alpine df -h /cache
Filesystem                Size      Used Available Use% Mounted on
tmpfs                    64.0M         0     64.0M   0% /cache

Paso 6: Declarar volúmenes en Docker Compose

En Compose, los volúmenes con nombre se declaran en la sección volumes de nivel superior y se referencian desde cada servicio. Crea un proyecto de ejemplo:

mkdir -p ~/pg-compose && cd ~/pg-compose
nano compose.yaml
services:
  db:
    image: postgres:17
    restart: unless-stopped
    environment:
      POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:?define POSTGRES_PASSWORD}
    volumes:
      - db_data:/var/lib/postgresql/data
      - ./init:/docker-entrypoint-initdb.d:ro

volumes:
  db_data:

db_data es un volumen con nombre, y ./init un bind mount relativo al directorio del proyecto (la imagen de PostgreSQL ejecuta los scripts .sql de esa carpeta la primera vez que inicializa la base de datos). Compose antepone el nombre del proyecto al volumen, que se llamará pg-compose_db_data.

Crea la carpeta init y arranca el servicio pasando la contraseña como variable:

mkdir -p init
POSTGRES_PASSWORD=your_strong_password docker compose up -d
docker volume ls
DRIVER    VOLUME NAME
local     pg-compose_db_data
local     pgdata

docker compose down conserva el volumen; solo docker compose down -v lo elimina.

Paso 7: Hacer una copia de seguridad de un volumen

La forma estándar de respaldar un volumen es montarlo en un contenedor temporal junto a un directorio del host y empaquetarlo con tar. Para bases de datos, detén antes el contenedor que lo usa para que los archivos sean coherentes:

docker stop pg
mkdir -p ~/backups
docker run --rm \
  --mount type=volume,src=pgdata,dst=/data,readonly \
  --mount type=bind,src="$HOME/backups",dst=/backup \
  alpine tar czf /backup/pgdata-$(date +%F).tar.gz -C /data .
docker start pg

Comprueba que el archivo existe y contiene los datos:

ls -lh ~/backups
tar tzf ~/backups/pgdata-*.tar.gz | head -n 5
-rw-r--r-- 1 root root 6.4M Sep 25 10:42 pgdata-2026-09-25.tar.gz
./
./PG_VERSION
./base/
./base/1/
./base/1/112

Paso 8: Restaurar un volumen

Para restaurar, crea un volumen vacío y extrae la copia en él. El ejemplo lo restaura en un volumen nuevo, pgdata_restaurado, para no sobrescribir el original:

docker volume create pgdata_restaurado
docker run --rm \
  --mount type=volume,src=pgdata_restaurado,dst=/data \
  --mount type=bind,src="$HOME/backups",dst=/backup,readonly \
  alpine tar xzf /backup/pgdata-2026-09-25.tar.gz -C /data

Sustituye la fecha por la de tu archivo. Verifica la restauración arrancando un PostgreSQL temporal sobre el volumen:

docker run -d --name pg-test \
  -e POSTGRES_PASSWORD=your_strong_password \
  --mount type=volume,src=pgdata_restaurado,dst=/var/lib/postgresql/data \
  postgres:17
docker exec pg-test psql -U postgres -c "SELECT * FROM notas;"

Debe mostrar el registro dato persistente. tar conserva los propietarios numéricos, así que PostgreSQL puede leer los archivos sin ajustar permisos. Elimina el contenedor de prueba al terminar con docker rm -f pg-test.

Paso 9: Limpiar volúmenes que ya no se usan

Lista los volúmenes que no están montados en ningún contenedor:

docker volume ls --filter dangling=true

Elimina un volumen concreto (falla si algún contenedor, aunque esté detenido, lo usa):

docker volume rm pgdata_restaurado

docker volume prune elimina solo los volúmenes anónimos sin uso. Para incluir también los volúmenes con nombre sin uso, añade -a:

docker volume prune -a

Solución de problemas

Permission denied al escribir en un bind mount. El UID del proceso del contenedor no tiene permisos sobre la ruta del host. Consulta el UID con docker exec nombre id y ajusta el propietario de la carpeta con sudo chown, o usa un volumen con nombre.

bind source path does not exist. La ruta de src en --mount type=bind no existe o no es absoluta. Créala antes o usa "$HOME/..." en lugar de ~.

volume is in use al eliminar un volumen. Un contenedor, aunque esté detenido, lo referencia. Localízalo con docker ps -a --filter volume=nombre_volumen y elimínalo primero.

El disco se llena en /var/lib/docker. Revisa el espacio por volumen con docker system df -v y elimina los que no uses.

Conclusión

Has comprobado que los datos del contenedor son efímeros, has guardado una base de datos en un volumen con nombre que sobrevive a la recreación del contenedor, has usado bind mounts y tmpfs para sus casos concretos, y sabes hacer copias de seguridad de un volumen y restaurarlas. Como siguientes pasos, programa las copias con un timer de systemd, guárdalas fuera del servidor en un almacenamiento de objetos compatible con S3 y revisa periódicamente que se pueden restaurar.