psql "connection refused" - Postgres Service Not Ready in CI
psql tried to open a TCP connection to Postgres and the kernel returned "Connection refused" because nothing is listening yet. The service container is still booting when your step runs - this is a timing problem, not a credentials or SQL problem.
What this error means
A psql or migration step fails immediately with "Connection refused", naming the host and port 5432. It frequently passes on a re-run once the Postgres container has finished initializing and started accepting connections.
psql: error: connection to server at "postgres" (172.18.0.2), port 5432 failed:
Connection refused
Is the server running on that host and accepting TCP/IP connections?Common causes
Service container still starting
GitHub Actions starts the Postgres service in parallel with your job. The first migrate/psql call can land before Postgres has bound its socket, so the connection is refused.
No healthcheck gating the job
Without a --health-cmd on the service, the job does not wait for readiness and races the container boot.
Transient network blip
A brief loss of connectivity on the runner network can refuse a connection that succeeds moments later.
How to fix it
Add a service healthcheck so the job waits
- Attach a
pg_isreadyhealthcheck to the Postgres service. - GitHub Actions holds the job until the service reports healthy.
- The race disappears because your step only runs once Postgres accepts connections.
services:
postgres:
image: postgres:16
env:
POSTGRES_PASSWORD: postgres
options: >-
--health-cmd "pg_isready -U postgres"
--health-interval 5s
--health-timeout 5s
--health-retries 10Poll for readiness before connecting
Block on pg_isready so the first real query never races the boot.
until pg_isready -h "$PGHOST" -p 5432 -U postgres; do
echo "waiting for postgres..."; sleep 1
done
psql "$DATABASE_URL" -c 'select 1'How to prevent it
- Gate every database step on a healthcheck or
pg_isready, never a fixed sleep. - Use the same readiness wait in local and CI scripts.
- On managed runners (Latchkey), self-healing auto-retries transient failures, and a database container that is slow to accept connections is a classic transient case.