Conoces la escena: un repositorio con docker-compose.yml, docker-compose.dev.yml, docker-compose.prod.yml,
y un archivo más que alguien creó “temporalmente” durante un incidente. Luego pasa un año. Ahora tu pila “dev” activa accidentalmente el proxy inverso
de grado productivo, o prod ejecuta en silencio la imagen de depuración porque la cadena de overrides está embrujada.
Los perfiles de Compose son la respuesta madura: un solo archivo Compose, múltiples pilas, comportamiento predecible. Menos arqueología de YAML,
menos sorpresas de “funciona en mi portátil” y muchas menos reversiónes de madrugada del viernes.
Qué son los perfiles de Compose (y qué no son)
Un perfil de Compose es una etiqueta que adjuntas a un servicio (y a veces a otros recursos) para que solo se inicie cuando ese perfil esté habilitado.
Es básicamente inclusión condicional. El archivo Compose sigue siendo un modelo coherente; los perfiles deciden qué partes están activas en una ejecución.
Aquí está el modelo mental central:
-
Sin perfiles: ejecutar
docker compose upinicia todos los servicios en el archivo (sujetos a dependencias). -
Con perfiles: los servicios etiquetados con perfiles se excluyen a menos que ese perfil esté habilitado vía
--profileoCOMPOSE_PROFILES. -
Servicios por defecto: los servicios sin una entrada
profiles:se comportan como “siempre activos”.
Qué no son los perfiles: un sistema completo de plantillas, un gestor de secretos o un sustituto de una herramienta de despliegue adecuada. No te impedirán
hacer algo imprudente; solo dificultan hacerlo por accidente.
Orientación con opinión: trata los perfiles como barreras de función para la topología en tiempo de ejecución. Úsalos para añadir/quitar sidecars,
herramientas, dependencias solo en dev y ayudantes operativos. No los uses para encubrir arquitecturas de producción fundamentalmente distintas.
Si prod se ejecuta en Kubernetes y dev en Compose, está bien: los perfiles siguen siendo útiles en dev y en la validación local tipo-prod. Pero no finjas que
los perfiles de Compose hacen que dev sea idéntico a prod. Lo hacen más disciplinado.
Una cita para mantener la cabeza fría durante el próximo debate de “solo enviarlo”:
La esperanza no es una estrategia.
— General Gordon R. Sullivan
Broma #1: Si tus archivos Compose de dev y prod divergen lo suficiente, acabarán presentando declaraciones de impuestos por separado.
Hechos e historia: por qué existen los perfiles
Los perfiles ahora parecen obvios, pero son una respuesta a años de realidad desordenada. Un poco de contexto te ayuda a entender las aristas.
8 hechos concretos que importan en la práctica
-
Compose empezó como Fig (era 2013–2014): fue diseñado para aplicaciones multincontenedor locales, no para despliegue empresarial.
Los perfiles son una concesión posterior a cómo la gente realmente lo usaba. -
Los archivos de override se volvieron la solución por defecto:
docker-compose.override.ymlfue una función de conveniencia,
y accidentalmente entrenó a los equipos a bifurcar la configuración sin fin. - Los perfiles llegaron para reducir la proliferación de YAML: permiten que un solo archivo represente múltiples formas sin una pila de overrides.
-
Compose V2 se integró en el CLI de Docker:
docker compose(espacio) reemplazó adocker-compose(guion)
en la mayoría de las instalaciones modernas. Los perfiles están mucho más soportados allí. -
Los perfiles se resuelven del lado del cliente: el CLI de Compose decide qué crear. El Engine no conoce tu intención.
Eso significa que la “fuente de la verdad” es la configuración de Compose que realmente ejecutaste. -
Los perfiles interactúan con dependencias de manera no obvia: un servicio con perfil puede ser incluido porque otro servicio
depende de él (dependiendo de cómo inicies las cosas). Necesitas probar tus rutas de arranque. -
La deriva entre entornos múltiples es un problema de disponibilidad: los archivos YAML duplicados no solo consumen tiempo: crean desconocidos
que aparecen durante incidentes. -
Los perfiles van bien con contenedores de “herramientas operativas”: trabajos de backup, migradores, agentes de logs y UIs administrativas pueden ser
opt-in sin infectar tu pila por defecto.
Principios de diseño: cómo estructurar una pila dev/prod en un solo archivo
Un único archivo Compose puede ser limpio o maldito. Los perfiles no te salvan si diseñas para el caos. Diseña para la previsibilidad en su lugar.
1) Separa los servicios “siempre activos” de los “contextuales”
Pon tu app, su base de datos y lo que sea necesario para arrancar en el conjunto por defecto (sin perfil).
Pon lujos de desarrollador (live reload, UIs administrativas, SMTP falso, S3 local, shells de depuración) detrás de dev.
Pon decisiones exclusivas de producción (TLS real en el borde, reglas proxy que actúan como WAF, reenvío de logs) detrás de prod o ops.
2) Mantén los puertos sencillos, estables e intencionales
En dev probablemente publiques puertos al host. En prod, a menudo no; te conectas a una red y dejas que un proxy inverso maneje el ingreso.
Usa perfiles para evitar incidentes de “prod se vincula accidentalmente a 0.0.0.0:5432”.
3) Prefiere volúmenes nombrados; haz la persistencia explícita
El almacenamiento es donde las diferencias dev/prod se convierten en pérdida de datos. Los volúmenes nombrados están bien para local, pero en prod deberías usar rutas montadas o un driver de volumen gestionado y flujos de trabajo claros de backup/restore.
4) Trata las variables de entorno como API, no como un cajón de trastos
Usa archivos .env, pero no permitas que se conviertan en un segundo lenguaje de configuración. Usa valores por defecto explícitos, documenta las variables requeridas
y valídelas en tu entrypoint si la app es tuya.
5) Compose no es un orquestador; no te disfraces
Compose puede reiniciar contenedores, hacer healthchecks y definir dependencias. No está pensado para programar en varios nodos, hacer rollouts progresivos o gestionar secretos a escala.
Úsalo como un “ejecutor de pilas” fiable. Si necesitas más, migra—a no ser que quieras añadir mil scripts hasta reinventar un Kubernetes peor.
Broma #2: “Solo un archivo override más” es la forma de invocar poltergeists YAML.
Archivo Compose de referencia usando perfiles (dev/prod/ops)
Esta es una línea base realista: una app web, una base de datos Postgres, una caché y ayudantes opcionales. El objetivo no es ser sofisticado.
El objetivo es ser difícil de usar mal.
cr0x@server:~$ cat compose.yml
services:
app:
image: ghcr.io/acme/demo-app:1.8.2
environment:
APP_ENV: ${APP_ENV:-dev}
DATABASE_URL: postgres://app:${POSTGRES_PASSWORD:-devpass}@db:5432/app
REDIS_URL: redis://redis:6379/0
depends_on:
db:
condition: service_healthy
redis:
condition: service_started
networks: [backend]
healthcheck:
test: ["CMD", "curl", "-fsS", "http://localhost:8080/healthz"]
interval: 10s
timeout: 2s
retries: 12
db:
image: postgres:16
environment:
POSTGRES_DB: app
POSTGRES_USER: app
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-devpass}
volumes:
- db_data:/var/lib/postgresql/data
networks: [backend]
healthcheck:
test: ["CMD-SHELL", "pg_isready -U app -d app"]
interval: 5s
timeout: 2s
retries: 20
redis:
image: redis:7
command: ["redis-server", "--save", "", "--appendonly", "no"]
networks: [backend]
# Dev-only: bind ports, live reload, friendly tools
app-dev:
profiles: ["dev"]
image: ghcr.io/acme/demo-app:1.8.2
environment:
APP_ENV: dev
LOG_LEVEL: debug
DATABASE_URL: postgres://app:${POSTGRES_PASSWORD:-devpass}@db:5432/app
REDIS_URL: redis://redis:6379/0
command: ["./run-dev.sh"]
volumes:
- ./src:/app/src:delegated
ports:
- "8080:8080"
depends_on:
db:
condition: service_healthy
redis:
condition: service_started
networks: [backend]
mailhog:
profiles: ["dev"]
image: mailhog/mailhog:v1.0.1
ports:
- "8025:8025"
networks: [backend]
adminer:
profiles: ["dev"]
image: adminer:4
ports:
- "8081:8080"
networks: [backend]
# Prod-ish: reverse proxy and tighter exposure
edge:
profiles: ["prod"]
image: nginx:1.27
volumes:
- ./nginx/conf.d:/etc/nginx/conf.d:ro
ports:
- "80:80"
depends_on:
app:
condition: service_healthy
networks: [frontend, backend]
# Ops-only: migrations and backups
migrate:
profiles: ["ops"]
image: ghcr.io/acme/demo-app:1.8.2
command: ["./migrate.sh"]
environment:
APP_ENV: ${APP_ENV:-prod}
DATABASE_URL: postgres://app:${POSTGRES_PASSWORD}@db:5432/app
depends_on:
db:
condition: service_healthy
networks: [backend]
pg-backup:
profiles: ["ops"]
image: postgres:16
environment:
PGPASSWORD: ${POSTGRES_PASSWORD}
entrypoint: ["/bin/sh", "-lc"]
command: >
pg_dump -h db -U app -d app
| gzip -c
> /backup/app-$(date +%F_%H%M%S).sql.gz
volumes:
- ./backup:/backup
depends_on:
db:
condition: service_healthy
networks: [backend]
networks:
frontend: {}
backend: {}
volumes:
db_data: {}
Qué te aporta esta estructura
-
El por defecto es seguro:
app,db,redisse ejecutan sin exposiciones de puertos al host por defecto. -
Dev es ergonómico: habilita
devpara obtener live-reload, pruebas de correo y Adminer. -
Prod es controlado: habilita
prodpara añadir un proxy en el borde; aún sin puertos de desarrollo aleatorios. - Ops es explícito: migraciones y backups no están “siempre corriendo”; se invocan intencionalmente.
Observa la duplicación deliberada: app y app-dev son servicios separados. Eso no es por pereza.
Es una frontera de seguridad. El servicio de dev vincula puertos y monta código fuente; el servicio de tipo prod no lo hace.
Puedes compartir una etiqueta de imagen mientras separas el comportamiento en tiempo de ejecución.
Tareas prácticas: 12+ comandos reales, salidas y decisiones
A continuación hay movimientos operativos concretos que realmente vas a usar. Cada uno incluye: un comando, qué significa la salida típica y qué decisión tomar después.
Ejecútalos en la raíz del repositorio donde vive compose.yml.
Tarea 1: Verifica que tu Compose soporta perfiles (y qué versión estás ejecutando)
cr0x@server:~$ docker compose version
Docker Compose version v2.27.0
Significado: Compose V2 está instalado. Los perfiles son compatibles.
Si ves “command not found” o un binario v1 antiguo, espera comportamiento inconsistente.
Decisión: Estandariza en docker compose en tu equipo/CI. Mezclar v1/v2 es la forma en que llegan los tickets de “pero ayer funcionaba”.
Tarea 2: Renderiza la configuración efectiva para un perfil (captura sorpresas antes de arrancar contenedores)
cr0x@server:~$ docker compose -f compose.yml --profile dev config
services:
adminer:
image: adminer:4
networks:
backend: null
ports:
- mode: ingress
target: 8080
published: "8081"
protocol: tcp
app:
depends_on:
db:
condition: service_healthy
redis:
condition: service_started
environment:
APP_ENV: dev
DATABASE_URL: postgres://app:devpass@db:5432/app
REDIS_URL: redis://redis:6379/0
healthcheck:
test:
- CMD
- curl
- -fsS
- http://localhost:8080/healthz
image: ghcr.io/acme/demo-app:1.8.2
networks:
backend: null
app-dev:
command:
- ./run-dev.sh
depends_on:
db:
condition: service_healthy
redis:
condition: service_started
environment:
APP_ENV: dev
DATABASE_URL: postgres://app:devpass@db:5432/app
LOG_LEVEL: debug
REDIS_URL: redis://redis:6379/0
image: ghcr.io/acme/demo-app:1.8.2
networks:
backend: null
ports:
- mode: ingress
target: 8080
published: "8080"
protocol: tcp
volumes:
- type: bind
source: /home/cr0x/src
target: /app/src
bind:
create_host_path: true
db:
environment:
POSTGRES_DB: app
POSTGRES_PASSWORD: devpass
POSTGRES_USER: app
healthcheck:
test:
- CMD-SHELL
- pg_isready -U app -d app
image: postgres:16
networks:
backend: null
volumes:
- type: volume
source: db_data
target: /var/lib/postgresql/data
mailhog:
image: mailhog/mailhog:v1.0.1
networks:
backend: null
ports:
- mode: ingress
target: 8025
published: "8025"
protocol: tcp
redis:
command:
- redis-server
- --save
- ""
- --appendonly
- "no"
image: redis:7
networks:
backend: null
networks:
backend: {}
volumes:
db_data: {}
Significado: Puedes ver exactamente qué servicios aparecen con dev. Esta es tu comprobación de cordura para exposiciones accidentales de puertos,
variables de entorno faltantes o imágenes incorrectas.
Decisión: Si la configuración renderizada incluye algo que no pretendías, arregla el archivo Compose antes de ejecutarlo. No depures un error de configuración en tiempo de ejecución.
Tarea 3: Arranca la pila segura por defecto (sin perfiles habilitados)
cr0x@server:~$ docker compose -f compose.yml up -d
[+] Running 4/4
✔ Network server_backend Created
✔ Volume "server_db_data" Created
✔ Container server-db-1 Started
✔ Container server-redis-1 Started
✔ Container server-app-1 Started
Significado: Solo se iniciaron los servicios por defecto. Sin herramientas dev, sin proxy en el borde.
Decisión: Usa esto como tu línea base para pruebas de humo en CI y ejecuciones “tipo-prod local”. Cuanto más aburrido sea, mejor se comporta en incidentes.
Tarea 4: Inicia explícitamente la experiencia de desarrollo
cr0x@server:~$ docker compose -f compose.yml --profile dev up -d
[+] Running 3/3
✔ Container server-mailhog-1 Started
✔ Container server-adminer-1 Started
✔ Container server-app-dev-1 Started
Significado: Compose añadió solo los servicios del perfil dev; los servicios por defecto ya estaban en ejecución.
Decisión: Haz la regla de equipo: “dev es opt-in”. Si alguien quiere puertos de depuración en prod, debe decirlo en voz alta con la bandera de perfil.
Tarea 5: Demuestra qué perfiles están habilitados (útil en los logs de CI)
cr0x@server:~$ COMPOSE_PROFILES=prod docker compose -f compose.yml config --profiles
prod
Significado: El CLI reconoce qué perfil(es) serán considerados. Es un pequeño truco que previene grandes malentendidos.
Decisión: En CI, imprime los perfiles efectivos al inicio del job. Le ahorras tiempo a tu yo futuro durante un incidente.
Tarea 6: Lista contenedores del proyecto y reconoce servicios de perfiles
cr0x@server:~$ docker compose -f compose.yml ps
NAME IMAGE COMMAND SERVICE STATUS PORTS
server-adminer-1 adminer:4 "entrypoint.sh php …" adminer running 0.0.0.0:8081->8080/tcp
server-app-1 ghcr.io/acme/demo-app:1.8.2 "./start.sh" app running (healthy)
server-app-dev-1 ghcr.io/acme/demo-app:1.8.2 "./run-dev.sh" app-dev running 0.0.0.0:8080->8080/tcp
server-db-1 postgres:16 "docker-entrypoint…" db running (healthy) 5432/tcp
server-mailhog-1 mailhog/mailhog:v1.0.1 "MailHog" mailhog running 0.0.0.0:8025->8025/tcp
server-redis-1 redis:7 "docker-entrypoint…" redis running 6379/tcp
Significado: Puedes ver qué servicios se están ejecutando y qué puertos están publicados. La columna PORTS es tu auditoría de “qué expusimos”.
Decisión: Si ves puertos publicados en entornos donde no deberían estar, para y arregla el archivo. No normalices exposiciones accidentales.
Tarea 7: Confirma por qué un servicio no arranca (comprobación de dependencias y healthcheck)
cr0x@server:~$ docker compose -f compose.yml logs --no-log-prefix --tail=30 app
curl: (7) Failed to connect to localhost port 8080: Connection refused
Significado: El healthcheck está fallando. O la app no está escuchando, está escuchando en otro puerto, o se está cayendo antes de hacer bind.
Decisión: Revisa docker compose logs app por errores de arranque, luego docker exec dentro del contenedor para validar el puerto en escucha.
No toques la BD todavía; la mayoría de fallos de healthcheck son de configuración de la app, no de almacenamiento.
Tarea 8: Inspecciona variables de entorno efectivas (encuentra rápido el problema del “.env equivocado”)
cr0x@server:~$ docker compose -f compose.yml exec -T app env | egrep 'APP_ENV|DATABASE_URL|REDIS_URL'
APP_ENV=dev
DATABASE_URL=postgres://app:devpass@db:5432/app
REDIS_URL=redis://redis:6379/0
Significado: El contenedor ve los valores que crees que ve. Si falta la contraseña o está vacía, tu .env no se carga o el nombre de variable está mal.
Decisión: Si las vars están mal, arregla desde el lado del llamador (tu export en shell, la inyección de secretos en CI o el archivo Compose). No “hotfixees” editando contenedores.
Tarea 9: Identifica deriva de imágenes entre servicios dev y prod
cr0x@server:~$ docker compose -f compose.yml images
CONTAINER REPOSITORY TAG IMAGE ID SIZE
server-app-1 ghcr.io/acme/demo-app 1.8.2 7a1d0f2c9a33 212MB
server-app-dev-1 ghcr.io/acme/demo-app 1.8.2 7a1d0f2c9a33 212MB
server-db-1 postgres 16 5e2c6e1e12b8 435MB
server-redis-1 redis 7 1c90a3f8e3a4 118MB
Significado: Ambos servicios app usan el mismo ID de imagen. Eso es bueno: tu comportamiento en dev difiere por comando/volúmenes/puertos, no por código no rastreado.
Decisión: Si los IDs de imagen difieren inesperadamente, decide si eso es intencional. Si no lo es, unifica etiquetas o deja de fingir que los entornos son comparables.
Tarea 10: Demuestra qué servicios forman parte realmente de un perfil (útil durante refactors)
cr0x@server:~$ docker compose -f compose.yml config --services
adminer
app
app-dev
db
edge
mailhog
migrate
pg-backup
redis
Significado: Esto lista todos los servicios en el archivo, incluidos los bloqueados por perfiles. Ahora puedes cotejar propiedad y eliminar peso muerto.
Decisión: Si nadie puede explicar por qué existe un servicio, bórralo o pásalo detrás de un perfil ops y requiere invocación explícita.
Tarea 11: Inicia el perfil prod localmente sin exposición de dev
cr0x@server:~$ COMPOSE_PROFILES=prod docker compose -f compose.yml up -d
[+] Running 1/1
✔ Container server-edge-1 Started
Significado: Solo se añadió el servicio edge; los servicios por defecto ya estaban presentes.
Decisión: Usa esto para validar cambios de configuración de nginx con la misma app/db que usas en otros entornos, sin incorporar herramientas solo para dev.
Tarea 12: Ejecuta trabajos ops puntuales sin dejar contenedores zombis
cr0x@server:~$ COMPOSE_PROFILES=ops docker compose -f compose.yml run --rm migrate
Running migrations...
Migrations complete.
Significado: El contenedor de migración se ejecutó y fue eliminado. No hay servicio de larga duración, no hay reinicios sorpresa.
Decisión: Mantén las “acciones ops” como trabajos run --rm. Si tus migraciones se ejecutan como servicio permanente, te estás creando un pager autoinfligido.
Tarea 13: Realiza un backup con el perfil ops y valida que el archivo exista
cr0x@server:~$ COMPOSE_PROFILES=ops docker compose -f compose.yml run --rm pg-backup
cr0x@server:~$ ls -lh backup | tail -n 2
-rw-r--r-- 1 cr0x cr0x 38M Jan 3 01:12 app-2026-01-03_011230.sql.gz
Significado: El backup aterrizó en el sistema de archivos del host. Esa es la diferencia entre “tenemos backups” y “tenemos una historia reconfortante”.
Decisión: Si el archivo no está, no sigas con cambios riesgosos. Arregla mounts/permisos primero. Los backups que no restauran son solo arte performático.
Tarea 14: Detecta colisiones de puertos antes de culpar a Docker
cr0x@server:~$ ss -ltnp | egrep ':8080|:8081|:8025' || true
LISTEN 0 4096 0.0.0.0:8080 0.0.0.0:* users:(("docker-proxy",pid=22419,fd=4))
LISTEN 0 4096 0.0.0.0:8081 0.0.0.0:* users:(("docker-proxy",pid=22455,fd=4))
LISTEN 0 4096 0.0.0.0:8025 0.0.0.0:* users:(("docker-proxy",pid=22501,fd=4))
Significado: Los puertos del host ya están enlazados por procesos proxy de Docker. Si tu próximo up falla con “port is already allocated”, esta es la razón.
Decisión: O detén la pila competidora o cambia los puertos publicados. No “lo soluciones” ejecutando todo con privilegios y esperando lo mejor.
Tres microhistorias corporativas desde las trincheras
Microhistoria 1: El incidente causado por una suposición errónea
Un equipo SaaS de tamaño medio mantenía dos archivos Compose: uno para dev y otro “tipo-prod”. La suposición era cortés y mortal:
“Son básicamente iguales; prod-like solo añade nginx.” Nadie volvió a verificar esa afirmación después del décimo pequeño cambio.
Un ingeniero nuevo añadió un contenedor Redis al archivo de dev únicamente, porque la app tenía un feature flag y “prod aún no lo usa”.
Semanas después, prod empezó a habilitar la flag en un canario. La pila tipo-prod usada en CI no tenía Redis.
CI pasó porque las pruebas relevantes se omitieron cuando Redis no era detectado.
Luego vino un despliegue donde la flag se activó más ampliamente de lo previsto. El comportamiento de reserva de la app fue reintentar conexiones a Redis
agresivamente. La CPU subió, la latencia de peticiones también, y un par de nodos empezaron a ser matados por el OOM reaper del kernel. No todos ellos,
pero los suficientes para crear un brownout gradual que parecía “falta de red”.
La solución no fue heroica. Colapsaron a un solo archivo Compose y usaron perfiles: Redis pasó a ser defecto en la pila usada para CI, y un nuevo
perfil bloqueó dependencias “experimentales”. Eso forzó una decisión consciente: si la app podría usar Redis en prod, Redis debe existir en el modelo tipo-prod.
Lección: las suposiciones sobre paridad de entornos son como la leche. Expiran silenciosamente y luego arruinan tu día a gritos.
Microhistoria 2: La optimización que salió mal
Un gran equipo de plataforma intentó “optimizar la experiencia de desarrollador” usando perfiles para intercambiar imágenes enteras:
una imagen de depuración pequeña para dev y una imagen endurecida para prod. En papel, redujo tiempo de build local y dejó la imagen de prod más estricta.
En la práctica, crearon un universo bifurcado.
La imagen de dev tenía paquetes extras: curl, netcat, Python y algunos bundles CA que “simplemente hacían que funcionara”.
La imagen de prod era mínima: menos libs, menos herramientas, menos superficie de ataque. Objetivos respetables.
Pero la app tenía una dependencia oculta en certificados CA del sistema debido a un SDK de terceros que hacía llamadas TLS.
Dev nunca vio el bug porque la imagen de depuración tenía la cadena CA correcta. Prod sí: los handshakes TLS fallaban intermitentemente dependiendo del endpoint
que el SDK tocara, y las fallas venían envueltas en excepciones opacas. El incidente se alargó porque los ingenieros seguían reproduciendo en dev, donde funcionaba.
Mantuvieron los perfiles, pero cambiaron la regla: los perfiles pueden cambiar comandos, mounts y puertos, pero no la composición base del SO de la imagen de runtime
sin una prueba formal que ejecute la imagen de prod en flujos de trabajo de dev. También añadieron un perfil “prod-image” que fuerza la imagen de prod localmente.
Lección: optimizar por velocidad cambiando el sustrato de runtime es la forma más rápida de comprar incidentes lentos.
Microhistoria 3: La práctica aburrida pero correcta que salvó el día
Un equipo interno de pagos usaba Compose para dev local y para un pequeño entorno “lab” on-prem usado para integraciones con partners.
Su práctica era poco glamurosa: cada cambio al Compose debía incluir una salida actualizada de docker compose config como artefacto en los logs de CI para cada perfil.
No almacenada para siempre, solo adjuntada al resumen del job.
Una mañana, un cambio movió un mapeo de puerto de un servicio solo para dev a un servicio por defecto. No fue malicioso;
fue un error de copiar/pegar durante un refactor. El servicio resultó ser una UI administrativa de base de datos. Ya sabes lo que sigue.
El entorno lab tenía un firewall estricto, así que no estaba expuesto a Internet. Pero era accesible desde una gran red corporativa,
que es su propio tipo de wilderness. El equipo detectó el error antes de desplegar porque el artefacto CI para el perfil por defecto
mostró de repente un puerto publicado que no estaba allí el día anterior.
Revirtieron, luego reintrodujeron el cambio correctamente detrás del perfil dev. Sin incidente, sin espiral de vergüenza, sin “lo arreglaremos luego”.
Solo una pequeña guardia aburrida haciendo su trabajo.
Lección: imprimir la configuración efectiva es el equivalente operativo de lavarse las manos. No es glamuroso, y previene infecciones.
Guía rápida de diagnóstico: qué comprobar primero/segundo/tercero
Cuando una pila Compose “no funciona”, el camino más rápido es dejar de adivinar qué hizo Compose e inspeccionar qué hizo realmente.
Los perfiles añaden una dimensión más de confusión, así que tu triage debe ser nítido.
Primero: confirma el conjunto de perfiles previsto y la configuración renderizada
-
Ejecuta
docker compose --profile X configy busca:- puertos publicados inesperados
- servicios faltantes que asumías que estaban (cache, broker de mensajes, proxy inverso)
- valores por defecto de env vars que olvidaste que eran por defecto
- Si la salida de config te sorprende, para. Arregla la configuración antes de perseguir síntomas en tiempo de ejecución.
Segundo: revisa el estado y la salud de los contenedores, no solo “running”
-
Ejecuta
docker compose ps. Busca(healthy)y bucles de reinicio. - Un servicio puede estar “Up” y aun así estar muerto por dentro. Los healthchecks son tu detector de mentiras barato.
Tercero: determina si tienes una falla de dependencia o una falla de la app
- Si la BD está unhealthy: revisa almacenamiento, permisos y mounts de volúmenes.
- Si la BD está healthy pero la app unhealthy: revisa logs de la app y variables de entorno.
-
Si todo está healthy pero las peticiones fallan: revisa la red, puertos publicados y configuración del proxy inverso (especialmente si el perfil
prodañade un edge).
Bono: aisla quitando perfiles
Si el perfil dev introduce fallos, ejecuta solo la pila por defecto. Si la pila por defecto funciona, la regresión está en servicios solo de dev,
mounts o conflictos de puertos. Los perfiles hacen esta aislación trivial—si mantienes tus defaults limpios.
Errores comunes: síntoma → causa raíz → solución
Error 1: “¿Por qué mi herramienta de dev se está ejecutando en prod?”
Síntoma: UI admin, MailHog o endpoints de depuración aparecen en entornos donde no corresponden.
Causa raíz: El servicio carece de profiles: ["dev"], o el entorno establece COMPOSE_PROFILES=dev globalmente.
Solución: Añade perfiles al servicio y audita CI/hosts en busca de COMPOSE_PROFILES filtradas. En scripts de prod, establece COMPOSE_PROFILES=prod explícitamente.
Error 2: “Habilitar un perfil no arrancó nada”
Síntoma: docker compose --profile ops up no muestra contenedores nuevos, o solo arrancan los por defecto.
Causa raíz: Los servicios están definidos con un nombre de perfil distinto al que pasaste (error tipográfico), o esperabas que trabajos estilo run aparecieran bajo up.
Solución: Usa docker compose config --services e inspecciona las secciones profiles. Para trabajos puntuales, usa docker compose run --rm SERVICE.
Error 3: “La app no se conecta a la base de datos en dev, pero en prod funciona”
Síntoma: Connection refused/timeouts solo en el perfil dev.
Causa raíz: El servicio dev usa un DATABASE_URL distinto, o lo apuntaste accidentalmente a localhost en lugar del nombre de servicio db.
Solución: En contenedores, usa nombres DNS de servicio en la red de Compose: db:5432. Confirma con docker compose exec app env.
Error 4: “Port is already allocated” aparece aleatoriamente
Síntoma: Arrancar el perfil dev falla con un error de binding de puerto.
Causa raíz: Otra pila ya enlaza el puerto, o arrancaste dos perfiles que publican el mismo puerto host (común con app y app-dev si ambos publican 8080).
Solución: Solo publica puertos en uno de los servicios (típicamente el de dev). Verifica colisiones con ss -ltnp.
Error 5: “depends_on no esperó; la app arrancó demasiado pronto”
Síntoma: La app arranca antes de que la BD esté lista, causando bucles de crash.
Causa raíz: Usaste depends_on sin condiciones de salud, o falta/está incorrecto el healthcheck de la BD.
Solución: Añade healthchecks y usa condition: service_healthy. También haz la app resiliente con reintentos; Compose no es tu capa de fiabilidad.
Error 6: “Creíamos que los servicios de perfil no se creaban, pero sí lo hicieron”
Síntoma: Un servicio con perfil existe como artefacto de contenedor/red, incluso cuando el perfil no estaba habilitado.
Causa raíz: Ejecutaste previamente con ese perfil habilitado; los recursos permanecen hasta que se eliminan. O tu automatización usa docker compose up con variables de entorno establecidas.
Solución: Usa docker compose down (y opcionalmente -v solo en dev). Trata “lo que está corriendo” como estado, no como intención.
Error 7: “Nuestros backups tuvieron éxito pero las restauraciones fallaron”
Síntoma: El job de backup corre sin errores; la restauración posterior falla o produce datos vacíos.
Causa raíz: El contenedor de backup escribió en una ruta dentro del contenedor que no estaba montada, o los permisos impidieron escribir en el host.
Solución: Almacena backups en una ruta montada en el host. Después del backup, verifica la presencia y el tamaño del archivo con ls -lh. Prueba restauraciones periódicamente.
Listas de verificación / plan paso a paso
Paso a paso: migrar de múltiples archivos Compose a uno solo con perfiles
- Inventaria servicios entre archivos. Lista servicios y anota diferencias (puertos, volúmenes, etiquetas de imagen, comandos).
-
Define perfiles que correspondan a decisiones, no a personas.
Usa nombres comodev,prod,ops,debug. Evitaaliceonewthing. - Elige la pila “por defecto segura”. Sin herramientas dev, sin puertos publicados salvo lo requerido para la función básica (a menudo ninguno).
-
Mueve servicios solo para dev detrás de
dev. MailHog, Adminer, S3 falso, UIs de tracing local, etc. -
Divide servicios cuando el comportamiento en runtime difiere materialmente.
Si dev necesita bind mounts y comando distinto: creaapp-deven lugar de intentar alternarlo todo con env vars. - Mantén identidad de imagen estable cuando sea posible. Prefiere la misma imagen para app y app-dev; cambia comando/mounts/puertos.
-
Renderiza configuraciones en CI para cada perfil. Guarda salidas de
docker compose configen los logs de build. - Documenta comandos de “cómo ejecutar”. Hazlos copiables; la gente los copiará de todos modos.
-
Prueba tres rutas: solo por defecto,
--profile dev,--profile prod(o tipo-prod). - Elimina los archivos viejos. No los mantengas “por si acaso”. Así vuelve la deriva.
Lista operativa: antes de declarar la estrategia de perfiles “completa”
- El perfil por defecto arranca y es funcional sin puertos DB publicados.
- La salida de
docker compose configes estable y revisada para cada perfil. - El perfil dev no cambia imágenes base sin un plan de pruebas explícito.
- Las tareas ops usan
run --rmy escriben en rutas montadas en el host. - Los mapeos de puertos son únicos entre servicios que podrían ejecutarse juntos.
- Existen healthchecks para dependencias con estado (BD) y para la app.
- Los secretos no están comprometidos y las invocaciones de prod establecen perfiles explícitamente.
Plan CI: mínimo pero efectivo
- Renderiza config para default + dev + prod y almacena en logs.
- Arranca la pila por defecto, ejecuta pruebas de humo, destruye.
- Arranca la pila de dev (o un subconjunto), ejecuta pruebas unitarias/integración, destruye.
- Ejecuta migraciones ops como trabajo puntual en un entorno descartable.
Preguntas frecuentes (FAQ)
1) ¿Debo usar perfiles o archivos override?
Usa perfiles para cambios de topología (qué servicios existen) y para “herramientas de dev opcionales”.
Usa archivos override con moderación para ajustes máquina-locales (como el puerto personalizado de un desarrollador), y solo si toleras la deriva.
Si debes elegir uno: los perfiles son más fáciles de razonar y auditar.
2) ¿Un servicio puede pertenecer a varios perfiles?
Sí. Puedes poner profiles: ["dev", "ops"] para un servicio útil en ambos contextos.
Ten cuidado: la pertenencia a múltiples perfiles puede volverse un rompecabezas lógico durante incidentes.
Manténlo raro y justificado.
3) ¿Qué pasa si ejecuto docker compose up sin especificar perfil?
Se inician los servicios sin la clave profiles. Los servicios con profiles se ignoran.
Por eso tus servicios por defecto deben ser seguros y mínimos.
4) ¿Habilitar un perfil puede iniciar servicios extra por dependencias?
Puede, dependiendo de cómo inicies las cosas y de cómo declares tus dependencias. Tu trabajo es probar las rutas de arranque:
arrancar “solo la app”, arrancar la pila completa y arrancar servicios de perfil.
Asume que los humanos ejecutarán comandos raros durante incidentes.
5) ¿Los perfiles afectan redes y volúmenes?
Los perfiles condicionan servicios. Las redes y volúmenes se crean típicamente según los servicios que las referencian.
Si un volumen solo lo referencia un servicio profiled, no se creará a menos que ese perfil esté activo.
6) ¿Cómo evito que puertos de dev queden expuestos si alguien ejecuta el perfil equivocado?
Haz que el perfil por defecto sea seguro y que las invocaciones de prod sean explícitas. En scripts, establece COMPOSE_PROFILES=prod
en lugar de confiar en las variables de entorno presentes. Además evita publicar puertos en servicios por defecto a menos que realmente los necesites.
7) ¿Cómo manejar migraciones con perfiles?
Pon las migraciones en un perfil ops como trabajo puntual y ejecútalas con docker compose run --rm migrate.
No conviertas las migraciones en un servicio de larga duración. Si se reinicia, acabarás migrando dos veces. Eso no es un plan de upgrade.
8) ¿Los perfiles son adecuados para despliegues “prod en una sola VM”?
Sí, con disciplina. Los perfiles te ayudan a mantener herramientas ops fuera de la línea base y evitar exposiciones accidentales.
Pero no confundas “funciona en una VM” con “es una plataforma de producción orquestada”.
Añade monitoring, backups y procedimientos de rollback explícitos. Compose no los inventará por ti.
9) ¿Cuál es la forma más limpia de alternar entre comportamiento dev y prod para la misma app?
Prefiere servicios separados (como app y app-dev) cuando las diferencias son significativas (bind mounts, comandos, puertos).
Mantén la misma etiqueta de imagen cuando sea posible. Comportamiento separado, artefacto compartido.
10) ¿Debería mantener un perfil debug?
Sí, si lo usas responsablemente. Un perfil debug para herramientas efímeras (contenedor tcpdump, contenedor shell, agente de profiling)
puede reducir el tiempo medio para entender problemas. Solo no lo dejes convertirse en “prod con ruedines siempre puestos”.
Conclusión: próximos pasos prácticos
Los perfiles de Compose son la forma más simple de dejar de duplicar YAML mientras sigues ejecutando pilas diferentes para distintos contextos.
No eliminan la complejidad; la hacen visible y controlable. Ese es el punto.
Haz esto a continuación, en orden
- Elige una pila por defecto segura sin herramientas de dev y con mínima exposición de puertos al host.
-
Añade perfiles
dev,prodyopspara condicionar lo que es opcional, arriesgado o puntual. -
Haz que la salida de
docker compose configforme parte de los logs de CI para cada perfil. Trátalo como rastro de auditoría. -
Convierte utilidades de migración/backup en trabajos
run --rmdetrás deops. - Elimina tus archivos Compose adicionales una vez validado el enfoque de un solo archivo. La deriva ama el apego sentimental.
Cuando estás de guardia, quieres menos piezas móviles y menos ramas de comportamiento no documentadas. Los perfiles te dan eso—si mantienes tus defaults limpios
y tus perfiles intencionales. Ejecuta menos magia. Entrega más previsibilidad.