2.2.3. Service Containers
💡 First Principle: Integration tests need real dependencies — a database, a cache, a queue — and services: gives a job disposable ones that GitHub starts before your steps and tears down after, so tests run against the real thing without you managing infrastructure.
A service is a Docker container attached to the job's lifecycle. GitHub creates a bridge network for the job, so containers reach each other by service label:
jobs:
test:
runs-on: ubuntu-latest
services:
postgres:
image: postgres:16
env:
POSTGRES_PASSWORD: postgres
ports:
- 5432:5432
options: >-
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
redis:
image: redis:7
ports: ['6379:6379']
steps:
- uses: actions/checkout@v4
- run: npm test
env:
DATABASE_URL: postgres://postgres:postgres@localhost:5432/postgres
The single most exam-relevant detail is hostname resolution, which depends on where your steps run. If the job runs directly on the runner machine (no container: key), your steps reach services at localhost on the mapped port — that's why ports: matters. If the job itself runs in a container (container: key present), Docker's user-defined network gives you the service label as hostname (postgres:5432) and port mapping is unnecessary. Swapping those two is a favorite distractor.
Health checks in options: are how you avoid the flakiest failure in CI: tests connecting before the database finishes initializing. GitHub waits for the health check to pass before running your steps. Service containers require a Linux runner, and images can come from Docker Hub, GHCR, or any registry (with credentials: for private ones).
⚠️ Exam Trap: Without a health check, a service is considered "started" as soon as the container starts — not when the software inside it is ready. Intermittent "connection refused" in a scenario points at a missing --health-cmd, not at the test code.
Reflection Question: Your job adds container: node:20 and suddenly localhost:5432 stops resolving to the Postgres service. What changed about the network topology, and what's the one-word fix to the connection string?