Problema

En entornos donde la aplicación está compuesta por varios contenedores (por ejemplo, PHP‑Apache y Nginx) el proceso de despliegue suele reducirse a “pull image → run”. Cuando la arquitectura requiere orquestación ligera –docker‑compose– y la infraestructura se basa en máquinas virtuales (EC2, Compute Engine, etc.), la pregunta recurrente es cómo iniciar la pila completa desde el user‑data de la instancia. El reto es evitar pasos manuales, mantener la solución independiente de servicios gestionados (ECS, Fargate, Cloud Run) y conservar la capacidad de mover la misma configuración a otra nube sin cambios sustanciales.

Causa

  1. Expectativas de “single image” – la mayoría de los scripts de user‑data están diseñados para descargar una única imagen y ejecutarla. Docker Compose introduce varios contenedores, volúmenes y redes que el script debe crear antes de que el primer contenedor arranque.
  2. Distribución de artefactos – el archivo docker-compose.yml y los archivos de configuración (certificados TLS, archivos de Nginx) no están en el registro de contenedores. Si no se empaquetan o se copian al arranque, el proceso falla por falta de recursos.
  3. Orden de arranque y dependencias – los contenedores pueden depender unos de otros (PHP necesita Nginx, Nginx necesita los certificados). Un script que lanza los contenedores sin esperar a que los recursos estén disponibles genera errores intermitentes.
  4. Persistencia de datos – si el docker-compose.yml declara volúmenes anónimos, cada reinicio de la instancia crea un nuevo volumen, lo que rompe la consistencia de la base de datos o de los archivos estáticos.

Solución

1. Empaquetado de artefactos en el repositorio de código

Mantén el docker-compose.yml, los archivos de Nginx y los certificados en el mismo repositorio que el pipeline CI. Configura el pipeline para crear un artefacto zip con la siguiente estructura:

release/
├─ docker-compose.yml
├─ nginx/
│  ├─ default.conf
│  └─ certs/
│     ├─ fullchain.pem
│     └─ privkey.pem

El artefacto se sube a un bucket S3 (o a un bucket compatible con otras nubes). El user‑data solo necesita descargar ese zip, descomprimirlo y lanzar Docker Compose.

2. Script de user‑data idempotente

El script debe:

  1. Instalar Docker y Docker Compose (versión compatible con el host).
  2. Descargar el artefacto desde el bucket.
  3. Verificar la integridad (checksum).
  4. Descomprimir en /opt/app.
  5. Ejecutar docker compose up -d --remove-orphans.
  6. Registrar el estado en CloudWatch Logs (o equivalente) para facilitar la depuración.

Un enfoque idempotente permite que la instancia se reinicie sin volver a descargar o reconstruir la pila si ya está presente.

3. Uso de variables de entorno para la portabilidad

Define variables como ECR_REGISTRY, APP_VERSION y TLS_CERT_PATH en el propio user‑data o en el parámetro Store (SSM, Parameter Store). El docker-compose.yml referencia esas variables con la sintaxis ${VARIABLE}. De esta forma, mover la misma configuración a GCP o Azure solo implica cambiar los valores de las variables, no el archivo de composición.

4. Manejo de dependencias con depends_on y healthcheck

En el docker-compose.yml declara depends_on con la opción condition: service_healthy y define healthcheck para cada contenedor crítico. Así Docker Compose esperará a que MySQL o el contenedor de Nginx estén listos antes de iniciar PHP.

5. Volúmenes nombrados y backup externo

En lugar de volúmenes anónimos, usa volúmenes nombrados (app-data:) y monta un directorio de EBS o de un disco adicional. Esto permite snapshots y evita la pérdida de datos al recrear la instancia.

6. Opcional: Terraform o CloudFormation para la instancia

Aunque el objetivo es mantener la solución agnóstica, envolver la creación de la instancia en Terraform con un user‑data parametrizado facilita la reproducibilidad y la migración entre proveedores.

Cuándo aplicar esta solución

  • Entornos multi‑container donde la orquestación ligera es suficiente y no se justifica un clúster completo.
  • Despliegues automatizados que deben ejecutarse desde una única fuente de verdad (CI pipeline).
  • Requerimientos de portabilidad entre nubes públicas o entre entornos on‑premise y cloud.
  • Aplicaciones con dependencias estáticas (certificados TLS, archivos de configuración) que no pueden ser empaquetadas dentro de la imagen.

No es adecuada cuando:

  • La carga supera la capacidad de una sola VM y se necesita escalado horizontal automático.
  • Se requiere integración profunda con servicios gestionados (ALB, Service Mesh) que ya están disponibles en el proveedor.
  • La organización ya ha adoptado una plataforma de orquestación completa (ECS, EKS, GKE) y el coste de mantener scripts de user‑data supera los beneficios.

Código

#!/bin/bash
set -euo pipefail

# Variables de entorno (pueden venir de SSM o del launch template)
ECR_REGISTRY="${ECR_REGISTRY:-123456789012.dkr.ecr.us-east-1.amazonaws.com}"
APP_VERSION="${APP_VERSION:-latest}"
ARTIFACT_URL="${ARTIFACT_URL:-https://my-bucket.s3.amazonaws.com/release.zip}"
ARTIFACT_SHA256="${ARTIFACT_SHA256:-}"
TARGET_DIR="/opt/app"

# Instalar Docker y Docker Compose (Amazon Linux 2)
yum update -y
amazon-linux-extras install docker -y
systemctl enable docker
systemctl start docker

# Docker Compose v2 (binario)
curl -L "https://github.com/docker/compose/releases/download/v2.27.0/docker-compose-linux-x86_64" -o /usr/local/bin/docker-compose
chmod +x /usr/local/bin/docker-compose

# Crear directorio objetivo
mkdir -p "$TARGET_DIR"
cd "$TARGET_DIR"

# Descargar artefacto
curl -fsSL "$ARTIFACT_URL" -o release.zip

# Verificar checksum si se provee
if [[ -n "$ARTIFACT_SHA256" ]]; then
  echo "$ARTIFACT_SHA256  release.zip" | sha256sum -c -
fi

# Descomprimir
unzip -o release.zip

# Login a ECR (asume IAM role con permisos)
aws ecr get-login-password | docker login --username AWS --password-stdin "$ECR_REGISTRY"

# Lanzar la pila
docker compose pull
docker compose up -d --remove-orphans

# Salida de logs para diagnóstico
docker compose logs --no-color --tail 100

Verificación

  1. Estado del servicio – ejecuta docker compose ps dentro de /opt/app. Todos los contenedores deben estar en State: Up.
  2. Healthchecks – revisa que cada contenedor reporte healthy con docker inspect --format='{{json .State.Health}}' <container>.
  3. Conectividad TLS – abre https://<public‑ip> y verifica que el certificado sea el esperado (puedes usar openssl s_client -connect <ip>:443 -servername <host>).
  4. Persistencia – crea un archivo dentro del volumen de la aplicación, reinicia la instancia y comprueba que el archivo sigue presente.
  5. Logs – revisa los logs de CloudWatch (o del agente de logs que hayas configurado) para detectar errores de docker compose up.

Notas adicionales

  • Tiempo de arranque: la primera ejecución descarga imágenes desde ECR, lo que puede tardar varios minutos. Considera habilitar una capa de caché con un registro proxy interno si el despliegue es frecuente.
  • Rotación de certificados: al actualizar los certificados, solo reemplaza los archivos en el bucket y ejecuta docker compose up -d nginx. Docker Compose detectará el cambio y recreará el contenedor sin afectar a PHP.
  • Seguridad: evita almacenar credenciales en texto plano dentro del user‑data. Usa IAM roles para EC2 y parámetros en SSM con cifrado.
  • Depuración remota: habilita docker compose exec <service> sh para entrar en los contenedores y validar configuraciones sin necesidad de reconstruir la imagen.
  • Migración: para mover la misma pila a GCP, solo cambia ECR_REGISTRY por el registro de Artifact Registry y actualiza la URL del bucket a Cloud Storage; el resto del script permanece idéntico.