--- title: "Copias de seguridad de volúmenes Docker en Ubuntu 24.04" description: "Haz copias de seguridad de volúmenes Docker y bases de datos PostgreSQL con un script, prográmalas con un timer de systemd y comprueba que puedes restaurarlas." category: "Docker Avanzado" language: es source: https://cubepath.com/es/docs/docker-avanzado/copia-de-seguridad-y-restauracion-de-contenedores-docker updated: 2026-09-25 --- # Copias de seguridad de volúmenes Docker en Ubuntu 24.04 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 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. ```bash sudo mkdir -p /opt/miapp sudo chown "$USER": /opt/miapp cd /opt/miapp ``` Crea el fichero de Compose: ```bash nano compose.yaml ``` ```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: ```bash echo 'POSTGRES_PASSWORD=tu_contraseña_segura' > .env chmod 600 .env ``` Arranca el proyecto y añade algunos datos de prueba: ```bash 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: ```bash docker volume ls ``` ```output 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: ```bash 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: ```bash 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: ```bash 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 ```bash 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: ```bash sudo ls -lh /var/backups/docker sudo tar tzf /var/backups/docker/miapp_uploads.tar.gz ``` ```output 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: ```bash sudo nano /usr/local/sbin/docker-backup.sh ``` ```bash #!/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: ```bash sudo chmod 750 /usr/local/sbin/docker-backup.sh sudo /usr/local/sbin/docker-backup.sh ``` ```output Copia completada: 2026-09-25_1030 ``` > Nota: si 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 con `docker 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: ```bash sudo nano /etc/systemd/system/docker-backup.service ``` ```ini [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: ```bash sudo nano /etc/systemd/system/docker-backup.timer ``` ```ini [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: ```bash sudo systemctl daemon-reload sudo systemctl enable --now docker-backup.timer ``` Comprueba cuándo se ejecutará la próxima vez: ```bash systemctl list-timers docker-backup.timer ``` ```output 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: ```bash 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): ```bash 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): ```bash 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: ```bash docker run --rm -v restore_test:/data alpine cat /data/index.html ``` ```output copia de prueba ``` Borra el volumen de prueba cuando termines: ```bash 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: ```bash 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: ```bash docker exec miapp-db psql -U app -d appdb_restore -c "SELECT * FROM notas;" ``` ```output 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: ```bash 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.