deniz.in

Markets

Weather

Loading weather

· via dev.to (home feed)

Five Docker Compose pitfalls that break Laravel deployments, and how to fix them

A dev.to guide covers five infrastructure mistakes that surface when Laravel moves to Docker Compose, from mismatched database hostnames to migration races between replicas, with the fix for each.

Five Docker Compose pitfalls that break Laravel deployments, and how to fix them

Running a Laravel application under Docker Compose looks straightforward until a deployment fails for reasons that have nothing to do with the application code. A post on dev.to, originally published in Spanish, walks through five infrastructure mistakes the author hit while debugging real projects, pairing each with its symptom, cause and fix.

The database hostname must match the service name

The first failure appears as connection errors such as getaddrinfo failed or Connection refused. The cause, according to the dev.to post, is that containers on a Compose network find each other by service name: if the database service is called postgres in the compose file but the application environment sets DB_HOST=db, there is nothing to resolve. This often happens when services are renamed or an .env file is copied from another project. The fix is to make DB_HOST exactly equal to the service name. A related trap is using localhost inside a container, where it refers to that same container rather than the host or any sibling service.

Healthchecks that probe a role that does not exist

The symptom here is a Postgres container stuck in an unhealthy state while every service depending on it never starts, with logs showing FATAL: role "postgres" does not exist. The healthcheck is calling pg_isready against the default postgres user even though POSTGRES_USER was set to something else, so it tests a role that was never created. The dev.to author recommends making the healthcheck read the same environment variables the service uses, and escaping the dollar signs as $$ so the container, not Compose, expands them. Pairing that with depends_on and condition: service_healthy makes the application wait until the database actually accepts connections, rather than until the container merely exists.

The scheduler and the missing cache table

When the scheduler reports errors about the cache or cache_locks tables — particularly for tasks using withoutOverlapping() or onOneServer() — the underlying issue is configuration. With CACHE_STORE=database, Laravel keeps its cache and locks in database tables, and a fresh or unmigrated database leaves the scheduler unable to acquire a lock. The fix in the post is to generate and run the cache table migration, using php artisan make:cache-table on Laravel 11 and later (cache:table on older versions), and to run the scheduler as its own process with php artisan schedule:work.

One process per container

Development setups commonly launch the web server, queue worker and scheduler together in a single terminal. Carrying that pattern into production puts several processes in one container, and Docker supervises only the main one: if the queue worker dies, nothing notices, workers cannot be scaled independently, and restart behaviour becomes unpredictable. The recommended structure keeps a single image but splits it into services with distinct commands — php-fpm for the application, php artisan queue:work with retry and timeout flags for the queue, and php artisan schedule:work for scheduling. The dev.to post lists the payoff: independent restarts, separate scaling for workers, cleaner logs and stop signals handled by Docker.

Migrations belong to the deployment, not the boot sequence

Putting php artisan migrate --force in the container entrypoint works with a single instance and breaks with several: replicas race each other, producing intermittent migration errors, database locks or half-created tables. Migrations should run once per deployment instead — either as a step in the CI/CD pipeline before new containers start, or as a one-shot Compose service with restart: "no" that depends on a healthy database. The application, queue and scheduler services should then start only after that migration step succeeds.

A checklist for the next deployment

The post closes with a short checklist: DB_HOST matches the service name and is never localhost; the database healthcheck uses the service's real credentials; depends_on waits on service_healthy; the cache table exists when the cache store is the database; web, queue and scheduler run in separate containers; migrations run once per deployment; and images are pinned to fixed versions rather than latest.

Why it matters

Every failure on the list shares one root cause, in the author's framing: assuming the infrastructure will behave like a local development machine. Compose has deterministic rules — name-based resolution, ordered startup, one supervised process per container — and each of these bugs is a violation of one of them. The lesson also travels beyond Laravel: the same class of mistake bites any PHP, Python or Node application moved into Compose, and recognising the pattern turns a confusing production outage into a short configuration fix.

  • #laravel
  • #docker
  • #docker-compose
  • #php
  • #devops
  • #deployment

Related posts