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 sudo que pertenezca al grupo docker.
  • 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 datosSíVolcado lógico (pg_dump, mysqldump)
compose.yaml, .env y ficheros de configuraciónSítar del directorio del proyecto
Imágenes públicas (nginx, postgres)NoSe vuelven a descargar del registro
Imágenes propias que no están en ningún registroSídocker save
Sistema de ficheros del contenedorNoDebe 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

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 en POSTGRES_USER, no postgres. Ajusta DB_USER en el script.
  • Error response from daemon: No such container: el script usa nombres de contenedor fijos. Si no defines container_name, Compose los llama miapp-db-1; comprueba el nombre con docker ps.
  • El volcado se restaura con errores invalid input o input file does not appear to be a valid archive: se generó con docker exec -t. Repite la copia sin -t.
  • El timer aparece como failed: revisa journalctl -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.