Patrones avanzados de Docker Volumes y redes: almacenamiento persistente y aislamiento para stacks de microservicios complejos
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
--linkexplícito (enfoque obsoleto) o redes personalizadas - Sistemas de archivos de solo lectura:
read_only: trueen 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 abackend(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: truey 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 (
exposeen lugar deports) - 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 dfy 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í →