DevOps

Patrones avanzados de Docker Volumes y redes: almacenamiento persistente y aislamiento para stacks de microservicios complejos

Ruslan Ismailov Publicado 18 min de lectura
P

Introducción: por qué el uso correcto de Volumes y redes es la base de un entorno Docker fiable

La contenedorización dejó hace tiempo de ser una tecnología experimental. Docker en producción es la norma para la mayoría de los equipos que construyen arquitecturas de microservicios. Sin embargo, aquí es precisamente donde se marca la diferencia entre quienes "simplemente ejecutan contenedores" y quienes construyen sistemas fiables y escalables.

Dos aspectos clave que determinan la calidad del entorno Docker en producción son la gestión de datos (Docker Volumes) y el aislamiento de red. Un uso incorrecto de los volúmenes provoca pérdida de datos al reiniciar los contenedores, problemas de permisos e incapacidad de escalar. Los errores en la configuración de red abren canales de comunicación no deseados entre servicios, generan vulnerabilidades y complican la depuración.

Este artículo está escrito para ingenieros DevOps y desarrolladores backend que ya trabajan con Docker en producción y desean pasar al siguiente nivel. Analizaremos patrones avanzados de almacenamiento persistente, el modelo de red de Docker desde bridge hasta overlay, implementaremos aislamiento zero-trust y construiremos un caso real con PostgreSQL, Redis y microservicios en Go.

Docker Volumes: tipos, comparativa y escenarios de uso

Docker ofrece tres mecanismos principales para trabajar con datos: named volumes, bind mounts y tmpfs. La elección entre ellos afecta directamente al rendimiento, la portabilidad y la seguridad.

Named Volumes

Los named volumes son el método recomendado para almacenar datos persistentes en Docker. Son gestionados por el demonio de Docker, se almacenan en el directorio /var/lib/docker/volumes/ del host y están completamente aislados del sistema de archivos del host.

Ventajas: portabilidad entre entornos, copias de seguridad sencillas, soporte de plugins de volúmenes para almacenamientos de terceros y gestión correcta de permisos. Utilice named volumes para bases de datos (PostgreSQL, Redis), colas de mensajes y cualquier dato que deba sobrevivir al reinicio del contenedor.

Bind Mounts

Los bind mounts montan un directorio o archivo específico del host en el contenedor. Es una herramienta poderosa para el desarrollo: los cambios en el código del host se reflejan instantáneamente en el contenedor. Sin embargo, en producción los bind mounts crean una dependencia rígida con el sistema de archivos del host, lo que dificulta la portabilidad y puede provocar problemas de permisos (especialmente en modo rootless).

Use bind mounts para: montar archivos de configuración, sockets (por ejemplo, /var/run/docker.sock) y también en pipelines de CI/CD para acceder al código fuente.

tmpfs Mounts

tmpfs almacena datos exclusivamente en la memoria RAM del host: no se escriben en disco y desaparecen al detener el contenedor. Esto es ideal para datos temporales, sesiones, cachés y todo lo que contenga información sensible que no requiera almacenamiento persistente.

Comparativa de rendimiento: tmpfs ofrece la mayor velocidad de lectura/escritura al trabajar con RAM. Los named volumes en el host local son comparables en rendimiento al acceso directo al disco (con una pequeña sobrecarga del sistema de archivos overlay). Los bind mounts pueden ser más lentos en macOS y Windows debido al mecanismo de sincronización entre el host y la máquina virtual.

# Ejemplos de declaración de los tres tipos en docker-compose.yml
services:
  app:
    image: myapp:latest
    volumes:
      # Named volume para datos de la aplicación
      - app_data:/var/lib/app
      # Bind mount para configuración
      - ./config/app.yaml:/etc/app/config.yaml:ro
      # tmpfs para archivos temporales
      - type: tmpfs
        target: /tmp
        tmpfs:
          size: 134217728  # 128MB

  postgres:
    image: postgres:16
    volumes:
      - postgres_data:/var/lib/postgresql/data

volumes:
  app_data:
    driver: local
  postgres_data:
    driver: local
    driver_opts:
      type: none
      o: bind
      device: /data/postgres  # ruta explícita en el host

Escenarios avanzados con Volumes

Volume Plugins

Docker dispone de un sistema de plugins para volúmenes que permite integrarse con sistemas de almacenamiento externos. Los plugins más populares en producción son:

  • docker-volume-netshare — montaje de recursos compartidos NFS, EFS y Samba
  • rexray — integración con AWS EBS, GCE Persistent Disk, Azure Disk
  • local-persist — almacenamiento de datos en un directorio especificado del host con nomenclatura
  • convoy — snapshots y copias de seguridad en S3

Volúmenes NFS para entornos distribuidos

En entornos multi-nodo (Docker Swarm, clústeres sin Kubernetes) a menudo se necesita acceso compartido a datos desde varios hosts. NFS es una solución probada para estos escenarios.

# Creación de un volumen NFS mediante CLI
docker volume create \
  --driver local \
  --opt type=nfs \
  --opt o=addr=192.168.1.100,rw,nfsvers=4 \
  --opt device=:/exports/postgres_data \
  postgres_nfs_volume

# Declaración de un volumen NFS en docker-compose.yml
volumes:
  shared_data:
    driver: local
    driver_opts:
      type: nfs
      o: "addr=192.168.1.100,rw,nfsvers=4,hard,intr"
      device: ":/exports/shared_data"

Volúmenes compartidos entre servicios

A veces varios contenedores deben acceder a los mismos datos; por ejemplo, la aplicación y un contenedor sidecar para procesamiento de logs o generación de estáticos. Para ello se utiliza el patrón volumes-from o un named volume compartido.

services:
  app:
    image: goapp:latest
    volumes:
      - shared_uploads:/app/uploads

  nginx:
    image: nginx:alpine
    volumes:
      - shared_uploads:/usr/share/nginx/html/uploads:ro
    depends_on:
      - app

volumes:
  shared_uploads:

Importante: al compartir volúmenes, asegúrese de que los UID/GID de los usuarios en los contenedores sean compatibles. La incompatibilidad de permisos es una de las causas más frecuentes de errores en producción.

Copia de seguridad y restauración de datos en Docker Volumes

Patrón básico con contenedor temporal

El enfoque estándar para hacer copias de seguridad de named volumes es lanzar un contenedor temporal que monte el volumen necesario y cree un archivo comprimido.

# Copia de seguridad del volumen postgres_data en un archivo tar
docker run --rm \
  -v postgres_data:/data:ro \
  -v $(pwd)/backups:/backups \
  alpine \
  tar czf /backups/postgres_data_$(date +%Y%m%d_%H%M%S).tar.gz -C /data .

# Restauración desde el archivo
docker run --rm \
  -v postgres_data:/data \
  -v $(pwd)/backups:/backups \
  alpine \
  tar xzf /backups/postgres_data_20260101_120000.tar.gz -C /data

Copia de seguridad de PostgreSQL con pg_dump

Para bases de datos es preferible usar herramientas especializadas en lugar de copiar archivos directamente. Copiar los archivos de PostgreSQL con la base en funcionamiento puede provocar corrupción de datos.

# Volcado lógico de PostgreSQL
docker exec postgres_container \
  pg_dump -U postgres -d mydb --format=custom \
  > backups/mydb_$(date +%Y%m%d).dump

# Restauración
docker exec -i postgres_container \
  pg_restore -U postgres -d mydb --format=custom \
  < backups/mydb_20260101.dump

# Automatización mediante cron en un contenedor separado
services:
  postgres_backup:
    image: postgres:16
    environment:
      PGPASSWORD: ${POSTGRES_PASSWORD}
    volumes:
      - ./backups:/backups
    entrypoint: |
      sh -c 'while true; do
        pg_dump -h postgres -U postgres -d mydb --format=custom \
          > /backups/mydb_$$(date +%Y%m%d_%H%M%S).dump;
        find /backups -name "*.dump" -mtime +7 -delete;
        sleep 86400;
      done'
    depends_on:
      - postgres
    networks:
      - backend

Integración con almacenamiento de objetos

En producción, las copias de seguridad deben almacenarse fuera del host. Use rclone o aws cli para sincronizar con almacenamientos compatibles con S3 (AWS S3, MinIO, Yandex Object Storage).

# Copia de seguridad con posterior subida a S3
docker run --rm \
  -v postgres_data:/data:ro \
  -e AWS_ACCESS_KEY_ID=${AWS_ACCESS_KEY_ID} \
  -e AWS_SECRET_ACCESS_KEY=${AWS_SECRET_ACCESS_KEY} \
  amazon/aws-cli \
  s3 cp - s3://my-backups/postgres/data_$(date +%Y%m%d).tar.gz \
  < <(tar czf - -C /data .)

Modelo de red de Docker: comparativa detallada

Docker soporta varios tipos de redes, cada uno óptimo para sus propios escenarios.

Red Bridge

Por defecto, Docker crea una red bridge (docker0). Cada contenedor recibe una dirección IP de la subred de este puente. Los contenedores en la misma red bridge pueden comunicarse por IP, pero no por nombre; para la resolución de nombres se necesitan redes bridge definidas por el usuario (user-defined bridge networks).

Una red bridge definida por el usuario proporciona resolución DNS automática entre contenedores por nombre de servicio, lo cual es una ventaja clave respecto a la red por defecto. Use redes bridge personalizadas para todas las aplicaciones multi-contenedor locales.

Red Host

En modo host, el contenedor utiliza directamente la pila de red del host. No hay aislamiento: los puertos del contenedor quedan disponibles en el host sin necesidad de mapeo. Esto proporciona el máximo rendimiento (sin sobrecarga de NAT), pero elimina completamente el aislamiento de red. Use la red host solo para aplicaciones de red de alto rendimiento (como agentes de monitorización) o cuando el rendimiento sea crítico.

Red Overlay

Las redes overlay permiten la comunicación entre contenedores en diferentes hosts, siendo la base de Docker Swarm. Encapsulan el tráfico en túneles VXLAN, garantizando una interacción transparente entre contenedores independientemente de la topología física.

Macvlan

Macvlan asigna al contenedor la dirección MAC de una interfaz de red física, haciéndolo "visible" en la red física como un dispositivo independiente. Esto es necesario para aplicaciones legacy que esperan una conexión directa a la red física, o para monitorizar el tráfico de red. Requiere soporte del modo promiscuo en el adaptador de red.

# Creación de una red overlay (requiere Swarm inicializado)
docker network create \
  --driver overlay \
  --attachable \
  --subnet 10.10.0.0/16 \
  --opt encrypted \
  production_overlay

# Creación de una red macvlan
docker network create \
  --driver macvlan \
  --subnet 192.168.1.0/24 \
  --gateway 192.168.1.1 \
  -o parent=eth0 \
  macvlan_net

Redes Overlay en Docker Swarm y diferencias con el networking de Kubernetes

Docker Swarm utiliza redes overlay como mecanismo principal de comunicación entre servicios. Al crear un Swarm se crean automáticamente la red ingress para balanceo de carga y docker_gwbridge para la comunicación con el exterior.

Características clave de overlay en Swarm:

  • Service discovery integrado mediante DNS: las solicitudes por nombre de servicio se balancean automáticamente entre réplicas (modo VIP)
  • Cifrado de tráfico mediante el flag --opt encrypted (AES-GCM 128 bits)
  • Gestión automática de claves de cifrado entre nodos
  • Encapsulación VXLAN con puerto UDP 4789

A diferencia de Kubernetes, donde el modelo de red se implementa mediante plugins CNI (Calico, Flannel, Cilium), Docker Swarm ofrece una solución integrada sin dependencias externas. Kubernetes proporciona mucha más flexibilidad: NetworkPolicy para control detallado del tráfico en capas L3/L4, soporte de eBPF en Cilium para trabajar en capa L7 e integración con service mesh (Istio, Linkerd). Docker Swarm es más sencillo de configurar, pero menos flexible para políticas de aislamiento complejas.

# Declaración de servicios en Swarm con red overlay
version: '3.8'
services:
  api:
    image: goapi:latest
    networks:
      - backend_overlay
    deploy:
      replicas: 3
      update_config:
        parallelism: 1
        delay: 10s

  postgres:
    image: postgres:16
    networks:
      - backend_overlay
    deploy:
      replicas: 1
      placement:
        constraints:
          - node.role == manager
    volumes:
      - postgres_data:/var/lib/postgresql/data

networks:
  backend_overlay:
    driver: overlay
    driver_opts:
      encrypted: "true"
    attachable: false

volumes:
  postgres_data:
    driver: local

Aislamiento de red: enfoque zero-trust en Docker

Zero-trust en el contexto de Docker significa: cada servicio se comunica únicamente con los servicios con los que debe hacerlo. Por defecto, todos los contenedores en la misma red bridge pueden comunicarse entre sí, lo que viola el principio de mínimo privilegio.

Segmentación de redes

La principal herramienta de aislamiento en Docker Compose es dividir los servicios en varias redes. Cada red es un dominio L2 independiente. Un servicio puede pertenecer a varias redes, lo que permite construir configuraciones similares a DMZ.

services:
  nginx:
    image: nginx:alpine
    networks:
      - frontend
      - dmz
    ports:
      - "80:80"

  api:
    image: goapi:latest
    networks:
      - dmz
      - backend
    # api NO está conectado a frontend — nginx no puede acceder a api directamente
    # api ve a nginx a través de dmz y a postgres a través de backend

  postgres:
    image: postgres:16
    networks:
      - backend
    # postgres solo es visible en la red backend
    # nginx no tiene acceso a él

  redis:
    image: redis:7-alpine
    networks:
      - backend
    # redis también está aislado en backend

networks:
  frontend:
    driver: bridge
    internal: false  # acceso a internet
  dmz:
    driver: bridge
    internal: true   # solo tráfico interno
  backend:
    driver: bridge
    internal: true   # solo tráfico interno
    driver_opts:
      com.docker.network.bridge.enable_icc: "false"  # deshabilitar comunicación entre contenedores

Medidas adicionales de aislamiento

  • --icc=false a nivel del demonio Docker: prohíbe la comunicación directa entre contenedores en la red por defecto; requiere --link explícito (enfoque obsoleto) o redes personalizadas
  • Sistemas de archivos de solo lectura: read_only: true en Compose para evitar escrituras en el contenedor
  • Perfiles seccomp y AppArmor: limitación de las llamadas al sistema del contenedor
  • User namespace remapping: aislamiento de UID/GID de los contenedores respecto al host
  • Gestión de secretos: use Docker Secrets o Vault para transmitir datos sensibles en lugar de variables de entorno
# Deshabilitar la comunicación entre contenedores a nivel del demonio
# /etc/docker/daemon.json
{
  "icc": false,
  "userns-remap": "default",
  "log-driver": "json-file",
  "log-opts": {
    "max-size": "10m",
    "max-file": "3"
  }
}

Caso real: PostgreSQL, Redis y microservicios en Go

Veamos un ejemplo completo de organización del almacenamiento y la red para un stack de producción típico: Go API, PostgreSQL como base de datos principal, Redis para caché y sesiones, y Nginx como reverse proxy.

version: '3.8'

x-logging: &default-logging
  driver: json-file
  options:
    max-size: "10m"
    max-file: "3"

services:
  nginx:
    image: nginx:1.25-alpine
    restart: unless-stopped
    ports:
      - "443:443"
      - "80:80"
    volumes:
      - ./nginx/conf.d:/etc/nginx/conf.d:ro
      - ./nginx/ssl:/etc/nginx/ssl:ro
      - nginx_cache:/var/cache/nginx
    networks:
      - frontend
      - dmz
    logging: *default-logging
    depends_on:
      - api

  api:
    image: mycompany/goapi:${API_VERSION:-latest}
    restart: unless-stopped
    environment:
      DATABASE_URL: postgresql://appuser:${POSTGRES_PASSWORD}@postgres:5432/appdb?sslmode=disable
      REDIS_URL: redis://:${REDIS_PASSWORD}@redis:6379/0
      APP_ENV: production
    volumes:
      - api_uploads:/app/uploads
      - type: tmpfs
        target: /tmp
        tmpfs:
          size: 67108864  # 64MB
    networks:
      - dmz
      - backend
    logging: *default-logging
    healthcheck:
      test: ["CMD", "wget", "-qO-", "http://localhost:8080/health"]
      interval: 30s
      timeout: 10s
      retries: 3
    depends_on:
      postgres:
        condition: service_healthy
      redis:
        condition: service_healthy
    read_only: true
    tmpfs:
      - /tmp

  postgres:
    image: postgres:16-alpine
    restart: unless-stopped
    environment:
      POSTGRES_DB: appdb
      POSTGRES_USER: appuser
      POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
      PGDATA: /var/lib/postgresql/data/pgdata
    volumes:
      - postgres_data:/var/lib/postgresql/data
      - postgres_wal:/var/lib/postgresql/wal
      - ./postgres/postgresql.conf:/etc/postgresql/postgresql.conf:ro
      - ./postgres/pg_hba.conf:/etc/postgresql/pg_hba.conf:ro
    networks:
      - backend
    logging: *default-logging
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U appuser -d appdb"]
      interval: 10s
      timeout: 5s
      retries: 5
    command: postgres -c config_file=/etc/postgresql/postgresql.conf

  redis:
    image: redis:7-alpine
    restart: unless-stopped
    command: redis-server --requirepass ${REDIS_PASSWORD} --maxmemory 256mb --maxmemory-policy allkeys-lru --appendonly yes
    volumes:
      - redis_data:/data
    networks:
      - backend
    logging: *default-logging
    healthcheck:
      test: ["CMD", "redis-cli", "-a", "${REDIS_PASSWORD}", "ping"]
      interval: 10s
      timeout: 5s
      retries: 3

  postgres_backup:
    image: postgres:16-alpine
    restart: unless-stopped
    environment:
      PGPASSWORD: ${POSTGRES_PASSWORD}
      POSTGRES_USER: appuser
      POSTGRES_DB: appdb
    volumes:
      - postgres_backups:/backups
    networks:
      - backend
    entrypoint: |
      sh -c 'while true; do
        pg_dump -h postgres -U appuser -d appdb --format=custom \
          -f /backups/appdb_$$(date +%Y%m%d_%H%M%S).dump;
        find /backups -name "*.dump" -mtime +7 -delete;
        echo "Backup completado en $$(date)";
        sleep 86400;
      done'
    depends_on:
      postgres:
        condition: service_healthy

networks:
  frontend:
    driver: bridge
    ipam:
      config:
        - subnet: 172.20.0.0/24
  dmz:
    driver: bridge
    internal: true
    ipam:
      config:
        - subnet: 172.20.1.0/24
  backend:
    driver: bridge
    internal: true
    ipam:
      config:
        - subnet: 172.20.2.0/24

volumes:
  postgres_data:
    driver: local
    driver_opts:
      type: none
      o: bind
      device: /data/postgres
  postgres_wal:
    driver: local
  postgres_backups:
    driver: local
    driver_opts:
      type: none
      o: bind
      device: /data/backups
  redis_data:
    driver: local
  api_uploads:
    driver: local
  nginx_cache:
    driver: local

Observe las decisiones clave en esta configuración:

  • PostgreSQL y Redis están aislados en la red backend: nginx no puede acceder a ellos físicamente
  • La Go API tiene acceso a dmz (para comunicarse con nginx) y a backend (para las bases de datos)
  • Los datos de PostgreSQL se almacenan en un directorio montado mediante bind mount en el host con ruta explícita para facilitar las copias de seguridad
  • Redis utiliza AOF para persistencia con la política de desalojo allkeys-lru
  • El contenedor de la API se ejecuta con read_only: true y escrituras solo en tmpfs y named volume
  • El contenedor de copias de seguridad está aislado en la red backend sin acceso al frontend

Errores frecuentes y cómo evitarlos

1. Almacenar datos en la capa escribible del contenedor

Cualquier dato escrito en un contenedor sin montar un volumen vive únicamente en la capa escribible y se pierde al eliminar el contenedor. Declare siempre VOLUME en el Dockerfile o monte volúmenes de forma explícita.

2. Usar la red por defecto (default bridge)

La red bridge por defecto no soporta resolución DNS por nombre de contenedor. Cree siempre redes personalizadas en Compose o mediante docker network create.

3. Exponer puertos de bases de datos al exterior

La directiva ports: - "5432:5432" para PostgreSQL hace que la base de datos sea accesible en todas las interfaces del host. En producción, use expose en lugar de ports para los servicios que no deben ser accesibles desde fuera.

4. Ausencia de dependencias con healthcheck

La directiva depends_on sin condition: service_healthy solo garantiza el orden de inicio de los contenedores, no la disponibilidad del servicio. La aplicación Go puede arrancar antes de que PostgreSQL acepte conexiones. Use siempre healthcheck.

5. Ignorar los permisos en los volúmenes

Si la aplicación en el contenedor se ejecuta como usuario sin privilegios (práctica recomendada) pero el directorio en el host pertenece a root, se producen errores de Permission denied. Solución: defina user en el Dockerfile y garantice la compatibilidad de UID/GID en el host.

6. Transmitir secretos mediante variables de entorno

Las variables de entorno son visibles mediante docker inspect y pueden aparecer en los logs. Use Docker Secrets (en Swarm) o monte los secretos como archivos mediante bind mount desde un directorio protegido.

7. Ausencia de límites en volúmenes y recursos

Sin límites, un contenedor puede agotar el espacio en disco del host. Use driver_opts para establecer cuotas o monitorice el uso con docker system df.

# Monitorización del uso de volúmenes y contenedores
docker system df -v

# Ver detalles de un volumen específico
docker volume inspect postgres_data

# Limpiar volúmenes no utilizados (¡CON PRECAUCIÓN en producción!)
docker volume prune --filter "label!=keep"

# Inspeccionar redes
docker network inspect backend

# Ver todos los contenedores conectados a una red
docker network inspect backend --format '{{range .Containers}}{{.Name}} {{.IPv4Address}}{{"\\n"}}{{end}}'

Conclusión: checklist para un entorno Docker listo para producción

Resumamos todas las prácticas tratadas en forma de checklist para evaluar la preparación de su entorno Docker para producción.

Volumes y almacenamiento

  • Todos los datos persistentes se almacenan en named volumes o bind mounts, no en la capa escribible
  • Los datos de bases de datos (PostgreSQL, Redis) en volúmenes con nombre y rutas explícitas
  • Los datos temporales se almacenan en tmpfs
  • Se ha configurado la copia de seguridad automática con rotación y descarga a almacenamiento externo
  • Se ha verificado la compatibilidad de UID/GID entre contenedores y el host
  • Para entornos multi-host se han configurado volúmenes NFS o plugins de volúmenes

Aislamiento de red

  • Se usan redes bridge personalizadas, no la red bridge por defecto
  • Los servicios están separados en redes funcionales (frontend/dmz/backend)
  • Las bases de datos y los servicios internos están en redes internas
  • Los puertos de bases de datos no están publicados en el host (expose en lugar de ports)
  • Las redes overlay en Swarm están configuradas con cifrado
  • Se ha configurado icc=false a nivel del demonio o mediante driver_opts

Seguridad y fiabilidad

  • Los contenedores se ejecutan como usuarios sin privilegios
  • Los archivos de configuración se montan en modo solo lectura
  • Los contenedores de API se ejecutan con read_only: true
  • Los secretos no se transmiten mediante variables de entorno en texto plano
  • Se han configurado healthcheck para todos los servicios y dependencias con condition: service_healthy
  • Se han configurado límites de recursos (CPU, memoria) y rotación de logs
  • Se ejecuta regularmente docker system df y se limpian los recursos no utilizados

El uso correcto de Docker Volumes y redes no es simplemente un detalle técnico, sino la base sobre la que se construyen la fiabilidad, la seguridad y la escalabilidad de todo el stack de microservicios. La inversión de tiempo en configurar estos aspectos se recupera con creces cuando ocurren incidentes en producción.

Tecnologías

Etiquetas

Ruslan Ismailov

Desarrollador Senior Web / Backend. Desarrollador senior web/backend con 9 años de experiencia. Stack: PHP, Laravel, PostgreSQL, Redis, Docker, Kubernetes, REST, microservicios, CI/CD. Más sobre mí →