Introduction

GitHub Actions starts service containers before your job steps and waits for Docker to mark them healthy. When the image is fine but the health command is wrong, missing from the image, or runs too early, the workflow sits at startup with no useful test output because the job never reaches your first step.

Symptoms

  • The workflow log stalls while waiting for postgres, mysql, or another service to become healthy
  • The same image works locally when you connect to it manually
  • Container logs show startup progress, but the job still times out waiting for health
  • The failure started after changing the image tag, service options, or initialization flags

Common Causes

  • The health check command references a binary that is not present in the image
  • The check targets the wrong database name, socket, or port
  • The service needs extra startup time for initialization or recovery
  • The workflow overrides the image entrypoint or required environment variables

Step-by-Step Fix

  1. 1.Reproduce the exact health check outside GitHub Actions
  2. 2.Run the same image and health command locally so you can see whether the command itself is valid before debugging the workflow.

```bash docker run --rm --name gha-postgres ^ --health-cmd="pg_isready -U postgres -d app" ^ --health-interval=10s ^ --health-timeout=5s ^ --health-retries=5 ^ -e POSTGRES_PASSWORD=postgres ^ -e POSTGRES_DB=app ^ postgres:16

docker inspect gha-postgres --format "{{json .State.Health }}" ```

  1. 1.Verify the health command exists in the image and matches its startup model
  2. 2.Some images do not ship curl, mysqladmin, or the client tool your workflow assumes is available.
bash
docker run --rm postgres:16 sh -lc "command -v pg_isready && pg_isready -U postgres -d app"
docker run --rm mysql:8 sh -lc "command -v mysqladmin && mysqladmin --help | head"
  1. 1.Give slow-starting services a real startup window
  2. 2.Initialization, crash recovery, or schema bootstrap can take longer than the default health timing.
yaml
services:
  mysql:
    image: mysql:8
    env:
      MYSQL_ROOT_PASSWORD: root
      MYSQL_DATABASE: app
    options: >-
      --health-cmd="mysqladmin ping -proot"
      --health-start-period=20s
      --health-interval=10s
      --health-timeout=5s
      --health-retries=10
  1. 1.Keep the job connection settings aligned with the service definition
  2. 2.After the container is healthy, the job still needs to connect using the host and port model that matches the runner type you use.

```yaml env: DATABASE_URL: postgres://postgres:postgres@127.0.0.1:5432/app

services: postgres: image: postgres:16 ports: - 5432:5432 ```

Prevention

  • Test every service image update with the exact health command used in CI
  • Prefer service-specific readiness commands such as pg_isready over generic TCP checks
  • Add health-start-period for databases that initialize data on first boot
  • Keep health commands and job connection settings in the same workflow review