Un contenedor Docker se puede recrear en segundos a partir de su imagen y su compose.yaml; lo que no se puede recrear son los datos que guarda en volúmenes. En este tutorial montarás una aplicación de ejemplo con PostgreSQL y un volumen de ficheros en Ubuntu 24.04, escribirás un script que vuelca la base de datos y empaqueta los volúmenes, lo programarás con un timer de systemd y restaurarás una copia para comprobar que funciona.
Requisitos previos
Para seguir esta guía necesitas:
- Un servidor con Ubuntu 24.04 LTS, por ejemplo un VPS de CubePath.
- Un usuario no root con privilegios
sudoque pertenezca al grupodocker. - Docker Engine y el plugin Docker Compose instalados desde el repositorio oficial de Docker.
- Espacio libre en disco suficiente para al menos dos copias completas de tus datos.
Qué hay que copiar (y qué no)
Antes de escribir comandos conviene tener claro qué partes de un entorno Docker contienen información que no se puede regenerar:
| Elemento | ¿Copiar? | Cómo |
|---|---|---|
| Volúmenes con ficheros (subidas, configuración) | Sí | tar desde un contenedor temporal |
| Bases de datos | Sí | Volcado lógico (pg_dump, mysqldump) |
compose.yaml, .env y ficheros de configuración | Sí | tar del directorio del proyecto |
Imágenes públicas (nginx, postgres) | No | Se vuelven a descargar del registro |
| Imágenes propias que no están en ningún registro | Sí | docker save |
| Sistema de ficheros del contenedor | No | Debe ser desechable |
Comandos como docker commit o docker export guardan el sistema de ficheros del contenedor, pero no el contenido de los volúmenes, así que no sirven como copia de seguridad de datos.
Copiar con tar los ficheros de una base de datos en marcha puede producir una copia inconsistente. Por eso las bases de datos se copian con su herramienta de volcado, que genera una instantánea coherente sin parar el servicio.
Paso 1: Crear una aplicación de ejemplo
Para tener algo que copiar, crea un proyecto con PostgreSQL y un servidor Nginx que sirve ficheros desde un volumen. Si ya tienes tu propia aplicación, adapta los nombres de contenedores, bases de datos y volúmenes en los pasos siguientes.
sudo mkdir -p /opt/miapp
sudo chown "$USER": /opt/miapp
cd /opt/miapp
Crea el fichero de Compose:
nano compose.yaml
services:
db:
image: postgres:17
container_name: miapp-db
restart: unless-stopped
environment:
POSTGRES_USER: app
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
POSTGRES_DB: appdb
volumes:
- db_data:/var/lib/postgresql/data
web:
image: nginx:alpine
container_name: miapp-web
restart: unless-stopped
ports:
- "8080:80"
volumes:
- uploads:/usr/share/nginx/html
volumes:
db_data:
uploads:
Guarda la contraseña en un fichero .env junto al compose.yaml. Sustituye tu_contraseña_segura por una contraseña real:
echo 'POSTGRES_PASSWORD=tu_contraseña_segura' > .env
chmod 600 .env
Arranca el proyecto y añade algunos datos de prueba:
docker compose up -d
docker exec miapp-web sh -c 'echo "copia de prueba" > /usr/share/nginx/html/index.html'
docker exec miapp-db psql -U app -d appdb -c "CREATE TABLE notas (id serial PRIMARY KEY, texto text); INSERT INTO notas (texto) VALUES ('hola');"
Si el último comando falla con the database system is starting up, espera unos segundos y repítelo.
Compose antepone el nombre del proyecto (el nombre del directorio) a los volúmenes. Comprueba sus nombres reales:
docker volume ls
DRIVER VOLUME NAME
local miapp_db_data
local miapp_uploads
Paso 2: Hacer una copia manual
Antes de automatizar nada, haz cada parte de la copia a mano para ver qué produce. Crea el directorio de destino, accesible solo para root:
sudo install -d -m 0700 /var/backups/docker
Volcar la base de datos
pg_dump con -Fc genera un volcado comprimido en formato personalizado que después se restaura con pg_restore. Se ejecuta dentro del contenedor y la salida se redirige a un fichero del host:
docker exec miapp-db pg_dump -U app -Fc appdb | sudo tee /var/backups/docker/appdb.dump > /dev/null
No uses docker exec -t aquí: la opción -t asigna una terminal y corrompe la salida binaria.
Empaquetar un volumen
Para copiar un volumen se arranca un contenedor temporal de Alpine que lo monta en modo solo lectura y escribe un .tar.gz en el directorio de copias del host:
sudo docker run --rm \
-v miapp_uploads:/data:ro \
-v /var/backups/docker:/backup \
alpine tar czf /backup/miapp_uploads.tar.gz -C /data .
La opción -C /data . guarda las rutas relativas, lo que facilita restaurar el contenido en otro volumen.
Copiar la configuración del proyecto
sudo tar czf /var/backups/docker/miapp_config.tar.gz -C /opt miapp
Comprueba que los tres ficheros existen y que el .tar.gz del volumen contiene lo esperado:
sudo ls -lh /var/backups/docker
sudo tar tzf /var/backups/docker/miapp_uploads.tar.gz
total 12K
-rw-r--r-- 1 root root 1.3K Sep 25 10:20 appdb.dump
-rw-r--r-- 1 root root 512 Sep 25 10:21 miapp_config.tar.gz
-rw-r--r-- 1 root root 180 Sep 25 10:21 miapp_uploads.tar.gz
./
./index.html
Paso 3: Escribir el script de copia
Ahora junta los tres pasos en un script con fecha en los nombres de fichero y rotación de copias antiguas. Créalo en /usr/local/sbin, ya que lo ejecutará root:
sudo nano /usr/local/sbin/docker-backup.sh
#!/usr/bin/env bash
set -euo pipefail
BACKUP_DIR="/var/backups/docker"
RETENTION_DAYS=7
PROJECT_DIR="/opt/miapp"
DB_CONTAINER="miapp-db"
DB_USER="app"
DB_NAME="appdb"
VOLUMES=("miapp_uploads")
STAMP="$(date +%F_%H%M)"
umask 077
mkdir -p "$BACKUP_DIR"
# 1. Volcado lógico de PostgreSQL (se escribe en .tmp y se renombra al terminar)
docker exec "$DB_CONTAINER" pg_dump -U "$DB_USER" -Fc "$DB_NAME" \
> "$BACKUP_DIR/${DB_NAME}_${STAMP}.dump.tmp"
mv "$BACKUP_DIR/${DB_NAME}_${STAMP}.dump.tmp" "$BACKUP_DIR/${DB_NAME}_${STAMP}.dump"
# 2. Volúmenes con ficheros
for vol in "${VOLUMES[@]}"; do
docker run --rm \
-v "${vol}:/data:ro" \
-v "${BACKUP_DIR}:/backup" \
alpine tar czf "/backup/${vol}_${STAMP}.tar.gz" -C /data .
done
# 3. Configuración del proyecto (compose.yaml, .env)
tar czf "$BACKUP_DIR/config_${STAMP}.tar.gz" \
-C "$(dirname "$PROJECT_DIR")" "$(basename "$PROJECT_DIR")"
# 4. Rotación: borra copias con más de RETENTION_DAYS días
find "$BACKUP_DIR" -maxdepth 1 -type f \
\( -name '*.dump' -o -name '*.tar.gz' -o -name '*.tmp' \) \
-mtime +"$RETENTION_DAYS" -delete
echo "Copia completada: $STAMP"
Con set -euo pipefail, cualquier comando que falle detiene el script con un código de salida distinto de cero, de modo que systemd marcará la ejecución como fallida en lugar de dar por buena una copia incompleta. El volcado se escribe con extensión .tmp y solo se renombra si pg_dump termina bien.
Hazlo ejecutable y lánzalo una vez:
sudo chmod 750 /usr/local/sbin/docker-backup.sh
sudo /usr/local/sbin/docker-backup.sh
Copia completada: 2026-09-25_1030
Notasi tienes imágenes propias que no están publicadas en ningún registro, añade al script una línea como
docker save mi-imagen:1.0 | gzip > "$BACKUP_DIR/mi-imagen_${STAMP}.tar.gz". Se restauran condocker load -i fichero.tar.gz. Las imágenes públicas no hace falta copiarlas.
Paso 4: Programar la copia con un timer de systemd
Un timer de systemd tiene ventajas frente a cron: registra la salida en el journal, muestra la próxima ejecución y, con Persistent=true, ejecuta la copia pendiente si el servidor estaba apagado a la hora programada.
Crea el servicio:
sudo nano /etc/systemd/system/docker-backup.service
[Unit]
Description=Copia de seguridad de volúmenes y bases de datos Docker
Requires=docker.service
After=docker.service
[Service]
Type=oneshot
ExecStart=/usr/local/sbin/docker-backup.sh
Crea el timer, que lo lanzará cada día a las 03:00:
sudo nano /etc/systemd/system/docker-backup.timer
[Unit]
Description=Copia diaria de Docker
[Timer]
OnCalendar=*-*-* 03:00:00
Persistent=true
RandomizedDelaySec=10m
[Install]
WantedBy=timers.target
Recarga systemd y activa el timer:
sudo systemctl daemon-reload
sudo systemctl enable --now docker-backup.timer
Comprueba cuándo se ejecutará la próxima vez:
systemctl list-timers docker-backup.timer
NEXT LEFT LAST PASSED UNIT ACTIVATES
Fri 2026-09-26 03:04:12 UTC 16h left - - docker-backup.timer docker-backup.service
Puedes forzar una ejecución inmediata y revisar su resultado en el journal:
sudo systemctl start docker-backup.service
journalctl -u docker-backup.service -n 20 --no-pager
Paso 5: Enviar las copias fuera del servidor
Una copia que vive en el mismo disco que los datos no te protege si pierdes el servidor. Como mínimo, sincroniza el directorio con otra máquina. Por ejemplo, con rsync sobre SSH hacia un servidor de copias (sustituye backup@your_backup_server por el tuyo y configura antes una clave SSH para root):
sudo rsync -a /var/backups/docker/ backup@your_backup_server:/srv/backups/$(hostname)/
Si prefieres un almacenamiento de objetos compatible con S3, herramientas como rclone o restic cumplen la misma función y añaden cifrado. Añade el comando de sincronización al final del script para que se ejecute tras cada copia.
Paso 6: Restaurar y verificar
Una copia solo es válida si has comprobado que se puede restaurar. Haz la prueba sin tocar los datos de producción.
Restaurar un volumen en un volumen de prueba
Crea un volumen nuevo y extrae en él la copia más reciente (ajusta el nombre del fichero):
docker volume create restore_test
sudo docker run --rm \
-v restore_test:/data \
-v /var/backups/docker:/backup:ro \
alpine tar xzf /backup/miapp_uploads_2026-09-25_1030.tar.gz -C /data
Comprueba el contenido:
docker run --rm -v restore_test:/data alpine cat /data/index.html
copia de prueba
Borra el volumen de prueba cuando termines:
docker volume rm restore_test
Para restaurar sobre el volumen real, para antes el servicio que lo usa (docker compose stop web), vacía el volumen con find /data -mindepth 1 -delete dentro de un contenedor temporal, extrae la copia de la misma forma y vuelve a arrancarlo.
Restaurar la base de datos
Crea una base de datos vacía y restaura el volcado en ella:
docker exec miapp-db createdb -U app appdb_restore
sudo cat /var/backups/docker/appdb_2026-09-25_1030.dump | docker exec -i miapp-db pg_restore -U app -d appdb_restore
Aquí sí hace falta -i para que el contenedor lea el volcado desde la entrada estándar. Comprueba que los datos están:
docker exec miapp-db psql -U app -d appdb_restore -c "SELECT * FROM notas;"
id | texto
----+-------
1 | hola
(1 row)
Para restaurar sobre la base de datos original usa pg_restore --clean --if-exists -d appdb, que elimina los objetos existentes antes de recrearlos. Cuando termines la prueba, borra la base de datos temporal:
docker exec miapp-db dropdb -U app appdb_restore
Recuperar en un servidor nuevo
Si pierdes el servidor completo, el orden es: instalar Docker, extraer config_*.tar.gz en /opt, arrancar solo la base de datos con docker compose up -d db, restaurar el volcado con pg_restore, restaurar los volúmenes de ficheros y, por último, docker compose up -d.
Solución de problemas
pg_dump: error: connection to server ... failed: FATAL: role "postgres" does not exist: el usuario de la base de datos es el definido enPOSTGRES_USER, nopostgres. AjustaDB_USERen el script.Error response from daemon: No such container: el script usa nombres de contenedor fijos. Si no definescontainer_name, Compose los llamamiapp-db-1; comprueba el nombre condocker ps.- El volcado se restaura con errores
invalid inputoinput file does not appear to be a valid archive: se generó condocker exec -t. Repite la copia sin-t. - El timer aparece como
failed: revisajournalctl -u docker-backup.service; la línea anterior al fallo indica qué comando falló.
Conclusión
Tienes una copia diaria de la base de datos, los volúmenes de ficheros y la configuración de tu proyecto Docker, con rotación automática y un procedimiento de restauración probado. Como siguientes pasos, envía las copias a un almacenamiento externo cifrado con restic, añade al script el volcado de otras bases de datos (mysqldump para MySQL o MariaDB) y repite la prueba de restauración de forma periódica.
