GitHub Actions Service Containers
Run Docker containers (Redis, PostgreSQL, etc.) alongside workflow jobs for
integration testing.
Not what you need? For building Docker container actions
(Dockerfile + action.yml), see the github-docker-action skill.
Prerequisites
- Linux runners only (
ubuntu-latest or self-hosted Linux with Docker)
- Service containers cannot be used inside composite actions
- Each service is created fresh per job and destroyed when the job completes
Job Type Decision Tree
Networking depends on whether your job runs in a container or on the runner:
Where does your job run?
├─ In a container (`container:` key set)
│ ├─ Hostname: service label name (e.g., `redis`, `postgres`)
│ ├─ Port mapping: NOT needed (Docker bridge network)
│ └─ See reference files → "Container Job" sections
│
└─ Directly on runner (no `container:` key)
├─ Hostname: `localhost`
├─ Port mapping: REQUIRED (e.g., `ports: ['6379:6379']`)
└─ See reference files → "Runner Job" sections
Quick Reference
| Setting | Container Job | Runner Job |
|---|
container: | Set (e.g., node:20-bookworm-slim) | Omitted |
| Service hostname | Label name (redis) | localhost |
ports: | Not needed | Required ('6379:6379') |
| Dynamic port | N/A | ${{ job.services.<label>.ports['<port>'] }} |
| Network | Docker bridge (automatic) | Host network via mapped ports |
Minimal Service Definition
services:
redis:
image: redis
ports:
- 6379:6379
options: >-
--health-cmd "redis-cli ping"
--health-interval 10s
--health-timeout 5s
--health-retries 5
Service fields
| Field | Purpose |
|---|
image: | Docker Hub image (or ghcr.io/... for private) |
ports: | Host:container port mapping (runner jobs only) |
options: | Docker --health-* flags for readiness checks |
env: | Environment variables passed to the container |
credentials: | username: / password: for private registries |
Port mapping syntax
| Value | Description |
|---|
8080:80 | Maps container TCP port 80 to host port 8080 |
8080:80/udp | Maps container UDP port 80 to host port 8080 |
8080/udp | Maps random host port to container UDP port 8080 |
Private Registry Authentication
services:
db:
image: ghcr.io/org/private-image:latest
credentials:
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
cache:
image: redis
credentials:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_PASSWORD }}
Health Checks
Always define health checks — without them, job steps start before the
service is ready.
Common health check options:
options: >-
--health-cmd "<command>"
--health-interval 10s
--health-timeout 5s
--health-retries 5
Reading Order
| Task | Files to Read |
|---|
| Add Redis to CI | SKILL.md + redis-service.md |
| Add PostgreSQL to CI | SKILL.md + postgres-service.md |
| Debug networking issues | SKILL.md (decision tree + quick ref) |
| Add other service (generic) | SKILL.md (minimal definition) |
In This Reference
Gotchas
- Windows/macOS runners: Service containers are Linux-only
- Missing health checks: Steps start immediately; service may not be ready
- Port conflicts: Two services mapping the same host port fail silently
container: + ports:: Port mapping is ignored in container jobs (bridge handles it)
- Dynamic ports: Omit host port (
- 6379) then read via job.services.<label>.ports['6379']
- Composite actions: Service containers cannot be created inside composite actions