> ## Content Index
> Fetch the complete content index at: https://blog.ifirestone.dev/llms.txt
> Use this file to discover other available public pages before exploring further.

# Laravel with Docker Compose: Five Infrastructure Mistakes Worth Avoiding
- URL: https://blog.ifirestone.dev/laravel-with-docker-compose-five-infrastructure-mistakes-worth-avoiding/
- Published: 2026-10-23T03:42:50.000Z
- Updated: 2026-10-23T03:42:50.000Z
- Description: Service names, Postgres healthchecks, a scheduler without a cache table, queues and migrations on deploy: the mistakes that show up when taking Laravel to Docker Compose and how to avoid them.
- Author: Ivan Firestone
- Tags: docker, laravel, devops, english

---

> Service names, Postgres healthchecks, a scheduler without a cache table, queues and migrations on deploy: the mistakes that show up when taking Laravel to Docker Compose and how to avoid them.

Taking a Laravel application to Docker Compose seems trivial until the first deployment fails for reasons that aren't in your code. These are five infrastructure mistakes I found while debugging real projects, with the symptom, the cause and the fix.

## 1\. The Service Name Doesn't Match `DB_HOST`

**Symptom:** the application can't connect to the database: `getaddrinfo failed`, `Connection refused` or `Name or service not known`.

**Cause:** on a Compose network, containers are resolved by **service name**. If your service is called `postgres` in `docker-compose.yml` but the `.env` says `DB_HOST=db`, there's nothing to resolve. It happens a lot when renaming services or copying an `.env` from another project.

**Fix:** make the value of `DB_HOST` exactly the service name.

```yaml
services:
  postgres:
    image: postgres:16
  app:
    environment:
      DB_HOST: postgres # the service name, not "localhost"

```

A related classic: using `localhost` inside the container. Inside a container, `localhost` is that same container.

## 2\. Postgres Healthcheck with Credentials Different from the Real Ones

**Symptom:** the database container ends up `unhealthy` and the services that depend on it never start. In the Postgres logs: `FATAL: role "postgres" does not exist`.

**Cause:** the healthcheck uses a user or database different from those configured in the environment variables. If you define `POSTGRES_USER=laravel` and your healthcheck runs `pg_isready -U postgres`, it checks against a role that doesn't exist.

**Fix:** have the healthcheck use the same variables, and escape the `$` so it's resolved by the container and not by Compose:

```yaml
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

```

With `depends_on` and `service_healthy`, the app waits until the database actually accepts connections, not just until the container exists.

## 3\. The Scheduler Fails Because the Cache Table Doesn't Exist

**Symptom:** the scheduler throws errors related to the `cache` or `cache_locks` table, especially when you use tasks with `withoutOverlapping()` or `onOneServer()`.

**Cause:** with `CACHE_STORE=database`, Laravel stores the cache and locks in tables. If those tables don't exist (migration not run or a fresh database), the scheduler fails when trying to acquire a lock.

**Fix:** make sure the cache table is migrated. In Laravel 11 or higher:

```bash
php artisan make:cache-table
php artisan migrate

```

(In earlier versions the command was `cache:table`.) And run the scheduler as its own process:

```bash
php artisan schedule:work

```

## 4\. All Processes in a Single Container

**Symptom:** in development everything works, and in production queues silently stop, restarts are unpredictable and you can't scale just the *workers*.

**Cause:** in development it's normal to use `concurrently` to bring up the web server, the queue and the scheduler in a single terminal. Carrying that pattern to production packs several processes into one container, and Docker only watches the main process: if the queue dies, nobody notices.

**Fix:** one process per container, with the same image and a different command:

```yaml
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

```

Advantages: independent restarts, separate worker scaling, separate logs and stop signals handled by Docker.

## 5\. Migrations Run on Every Container Start

**Symptom:** when deploying with several replicas, intermittent migration errors, database locks or half-created tables appear.

**Cause:** putting `php artisan migrate --force` in the `entrypoint` makes **every** container try to migrate on startup. With several replicas, they compete with each other.

**Fix:** migrations are a deployment step, not an application startup step. Run them **only once**, as a prior task in the CI/CD pipeline or as a one-shot service:

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

```

And have `app`, `queue` and `scheduler` start after the migration finishes successfully.

## Quick Checklist

- \[ \] `DB_HOST` matches the service name, never `localhost`.
- \[ \] The database healthcheck uses the same credentials as the service.
- \[ \] `depends_on` uses `condition: service_healthy`.
- \[ \] The cache table exists if you use `CACHE_STORE=database`.
- \[ \] One process per container: web, queue and scheduler separated.
- \[ \] Migrations run once per deployment, not on every startup.
- \[ \] Images have a pinned version, not `latest`.

## Conclusion

Almost all of these mistakes have the same origin: assuming the infrastructure behaves like your local machine. Compose has clear rules (resolution by name, startup order, one process per container). Knowing them saves hours of debugging.

If you have a recurring error in your own deployment, share it in the comments: surely someone else has run into it too.