Laravel con Docker Compose: cinco errores de infraestructura que vale la pena evitar

Nombres de servicio, healthchecks de Postgres, un scheduler sin tabla de caché, colas y migraciones al desplegar: los errores que aparecen al llevar Laravel a Docker Compose y cómo evitarlos.

Laravel con Docker Compose: cinco errores de infraestructura que vale la pena evitar
Photo by Ilya Pavlov / Unsplash
Nombres de servicio, healthchecks de Postgres, un scheduler sin tabla de caché, colas y migraciones al desplegar: los errores que aparecen al llevar Laravel a Docker Compose y cómo evitarlos.

Resumen

Llevar una aplicación Laravel a Docker Compose parece trivial hasta que el primer despliegue falla por razones que no están en tu código. Estos son cinco errores de infraestructura que encontré depurando proyectos reales, con el síntoma, la causa y la solución.

1. El nombre del servicio no coincide con DB_HOST

Síntoma: la aplicación no puede conectarse a la base de datos: getaddrinfo failed, Connection refused o Name or service not known.

Causa: en una red de Compose, los contenedores se resuelven por el nombre del servicio. Si tu servicio se llama postgres en docker-compose.yml pero el .env dice DB_HOST=db, no hay nada que resolver. Pasa mucho al renombrar servicios o al copiar un .env de otro proyecto.

Solución: que el valor de DB_HOST sea exactamente el nombre del servicio.

services:
  postgres:
    image: postgres:16
  app:
    environment:
      DB_HOST: postgres # el nombre del servicio, no "localhost"

Un clásico relacionado: usar localhost dentro del contenedor. Dentro de un contenedor, localhost es ese mismo contenedor.

2. Healthcheck de Postgres con credenciales distintas a las reales

Síntoma: el contenedor de la base de datos queda unhealthy y los servicios que dependen de él nunca arrancan. En los logs de Postgres: FATAL: role "postgres" does not exist.

Causa: el healthcheck usa un usuario o una base de datos distintos de los configurados en las variables de entorno. Si defines POSTGRES_USER=laravel y tu healthcheck ejecuta pg_isready -U postgres, estás comprobando contra un rol que no existe.

Solución: que el healthcheck use las mismas variables, y escapar el $ para que lo resuelva el contenedor y no Compose:

services:
  postgres:
    image: postgres:16
    environment:
      POSTGRES_USER: ${DB_USERNAME}
      POSTGRES_PASSWORD: ${DB_PASSWORD}
      POSTGRES_DB: ${DB_DATABASE}
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U $${POSTGRES_USER} -d $${POSTGRES_DB}"]
      interval: 5s
      timeout: 3s
      retries: 10

  app:
    depends_on:
      postgres:
        condition: service_healthy

Con depends_on y service_healthy, la app espera hasta que la base de datos realmente acepta conexiones, no solo hasta que el contenedor existe.

3. El scheduler falla porque la tabla de caché no existe

Síntoma: el scheduler lanza errores relacionados con la tabla cache o cache_locks, sobre todo cuando usas tareas con withoutOverlapping() o onOneServer().

Causa: con CACHE_STORE=database, Laravel guarda la caché y los locks en tablas. Si esas tablas no existen (la migración no se ejecutó o la base de datos es nueva), el scheduler falla al intentar adquirir un lock.

Solución: asegúrate de que la tabla de caché esté migrada. En Laravel 11 o superior:

php artisan make:cache-table
php artisan migrate

(En versiones anteriores el comando era cache:table.) Y ejecuta el scheduler como un proceso propio:

php artisan schedule:work

4. Todos los procesos en un solo contenedor

Síntoma: en desarrollo todo funciona, y en producción las colas se detienen en silencio, los reinicios son impredecibles y no puedes escalar solo los workers.

Causa: en desarrollo es normal usar concurrently para levantar el servidor web, la cola y el scheduler en una sola terminal. Llevar ese patrón a producción mete varios procesos en un contenedor, y Docker solo vigila el proceso principal: si la cola muere, nadie se entera.

Solución: un proceso por contenedor, con la misma imagen y un comando distinto:

services:
  app:
    image: myapp:latest
    command: php-fpm

  queue:
    image: myapp:latest
    command: php artisan queue:work --tries=3 --timeout=90
    restart: unless-stopped

  scheduler:
    image: myapp:latest
    command: php artisan schedule:work
    restart: unless-stopped

Ventajas: reinicios independientes, escalado de workers por separado, logs separados y señales de parada manejadas por Docker.

5. Las migraciones se ejecutan en cada arranque del contenedor

Síntoma: al desplegar con varias réplicas aparecen errores intermitentes de migración, bloqueos en la base de datos o tablas creadas a medias.

Causa: poner php artisan migrate --force en el entrypoint hace que cada contenedor intente migrar al iniciar. Con varias réplicas, compiten entre sí.

Solución: las migraciones son un paso del despliegue, no del arranque de la aplicación. Ejecútalas una sola vez, como tarea previa en el pipeline de CI/CD o como un servicio de una sola ejecución:

services:
  migrate:
    image: myapp:latest
    command: php artisan migrate --force
    restart: "no"
    depends_on:
      postgres:
        condition: service_healthy

Y haz que app, queue y scheduler arranquen después de que la migración termine con éxito.

Lista de verificación rápida

  • [ ] DB_HOST coincide con el nombre del servicio, nunca localhost.
  • [ ] El healthcheck de la base de datos usa las mismas credenciales que el servicio.
  • [ ] depends_on usa condition: service_healthy.
  • [ ] La tabla de caché existe si usas CACHE_STORE=database.
  • [ ] Un proceso por contenedor: web, cola y scheduler separados.
  • [ ] Las migraciones se ejecutan una vez por despliegue, no en cada arranque.
  • [ ] Las imágenes tienen una versión fija, no latest.

Conclusión

Casi todos estos errores tienen el mismo origen: asumir que la infraestructura se comporta como tu máquina local. Compose tiene reglas claras (resolución por nombre, orden de arranque, un proceso por contenedor). Conocerlas ahorra horas de depuración.

Si tienes un error recurrente en tu propio despliegue, compártelo en los comentarios: seguro alguien más se lo ha encontrado también.