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.
| Service |
Health command |
Reference |
| Redis |
redis-cli ping |
redis-service.md |
| PostgreSQL |
pg_isready |
postgres-service.md |
| Generic |
curl -f http://... |
Adapt per service |
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
| File |
Purpose |
| redis-service.md |
Redis config, health checks, test |
| postgres-service.md |
PostgreSQL config, health checks, test |
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