perkun.eu Services Portfolio Blog About Contact PL
← Blog

3/25/2026

Docker Compose healthchecks — why containers restart in the wrong order

TL;DR: depends_on: db does NOT wait until the database is ready — it waits until the container starts. depends_on: {db: {condition: service_healthy}} + healthcheck on the database = correct startup order.

Classic mistake on a first Laravel deployment with Docker Compose. Everything looks correct in the configuration — depends_on: db ensures the database container starts before the application. You run docker compose up and 30 seconds later you see in the logs: SQLSTATE[HY000] [2002] Connection refused. Laravel restarts. The database starts. Laravel tries again. This time it works. But why does this happen and how do you fix it properly?

The problem

Laravel starts before MySQL and throws a connection error. In docker compose logs app you see the sequence: several lines of “could not connect to database”, then the container restarts and on the next attempt the connection works, because MySQL had time to initialize.

This is not a random problem — it’s deterministic. Every first run of the environment ends with the same restart. In a development environment this is merely annoying. In a production environment with restart: unless-stopped, the container can get stuck in a loop if the database needs more initialization time (e.g. with a large database).

Why depends_on isn’t enough

depends_on controls Docker container startup order, not the readiness of the process inside the container. This is a fundamental difference.

A MySQL container reaches running state after about 2 seconds from start — that’s how long Docker takes to launch the process. MySQL server inside the container needs 5–15 seconds for full initialization: loading data from disk, InnoDB buffer pool initialization, opening table files, listening on port 3306.

depends_on: db says: “start the app container only after the db container starts”. It does not say: “start the app only when MySQL is ready to accept connections”. This is a subtle but critical difference.

Healthcheck for MySQL/MariaDB

The solution is adding a healthcheck to the database service in docker-compose.yml:

services:
  db:
    image: mariadb:11.4
    environment:
      MYSQL_ROOT_PASSWORD: ${DB_ROOT_PASSWORD}
      MYSQL_DATABASE: ${DB_DATABASE}
      MYSQL_USER: ${DB_USERNAME}
      MYSQL_PASSWORD: ${DB_PASSWORD}
    healthcheck:
      test: ["CMD", "mysqladmin", "ping", "-h", "localhost",
             "--user=root", "--password=$$MYSQL_ROOT_PASSWORD"]
      interval: 5s
      timeout: 3s
      retries: 10
      start_period: 30s
    volumes:
      - db_data:/var/lib/mysql

Two details require attention. First, $$MYSQL_ROOT_PASSWORD instead of $MYSQL_ROOT_PASSWORD — the double dollar sign is an escape in docker-compose YAML that prevents variable interpolation at the Compose level and passes the literal $MYSQL_ROOT_PASSWORD to the container shell, where it is already set as an environment variable. Second, start_period: 30s gives the container 30 seconds to start before the healthcheck begins counting as a failure — important for large databases with long initialization.

Healthcheck for PostgreSQL

PostgreSQL has a simpler and faster diagnostic tool:

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

pg_isready checks whether PostgreSQL accepts connections and completes in a fraction of a second. Faster than the MySQL equivalent.

depends_on with condition

After adding a healthcheck we can use condition: service_healthy in depends_on:

services:
  app:
    image: your-laravel-app
    depends_on:
      db:
        condition: service_healthy
      redis:
        condition: service_healthy
    environment:
      DB_HOST: db
      REDIS_HOST: redis
    restart: unless-stopped

  db:
    image: mariadb:11.4
    healthcheck:
      test: ["CMD", "mysqladmin", "ping", "-h", "localhost",
             "--user=root", "--password=$$MYSQL_ROOT_PASSWORD"]
      interval: 5s
      timeout: 3s
      retries: 10
      start_period: 30s

  redis:
    image: redis:7-alpine
    healthcheck:
      test: ["CMD", "redis-cli", "ping"]
      interval: 5s
      timeout: 3s
      retries: 5

volumes:
  db_data:

The application starts only when both services — database and Redis — are in healthy state. The first startup may take longer, but there will be no connection errors in the logs.

Debugging

When a healthcheck isn’t working as expected, docker inspect gives the full picture:

docker inspect db_container_name | jq '.[0].State.Health'

The output shows the current status (healthy, unhealthy, starting), the time of the last check, and logs from the last few healthcheck calls with exit codes. Typical errors are a wrong password in the healthcheck command or too short a start_period for a large database.

Summary

depends_on without condition: service_healthy is a ticking time bomb in a production environment. Healthchecks are a one-time cost of 10 minutes of configuration that eliminates an entire class of application startup errors. Add them to every service that needs time to initialize — database, cache, message queue.