GitHub Actions Tutorial — Container Jobs
Container Jobs
Instead of running steps directly on the GitHub-hosted runner VM, you can run them inside a Docker container. This gives you exact control over the runtime environment — specific OS versions, language versions, and pre-installed tools.
Why Use Container Jobs?
| Direct on Runner | Container Job |
|---|---|
| Uses runner’s pre-installed tools | Uses exact image you specify |
| Language version may change | Version locked to container image |
| Quick to start | Slight overhead for image pull |
| Good for most CI | Best for reproducible, controlled environments |
Basic Container Job
jobs:
test:
runs-on: ubuntu-latest
container:
image: node:20-alpine
steps:
- uses: actions/checkout@v4
- run: node --version # 20.x (from container)
- run: npm ci
- run: npm test
The container: field tells GitHub Actions to run all steps inside the specified Docker image.
Container with Options
jobs:
test:
runs-on: ubuntu-latest
container:
image: node:20
env:
NODE_ENV: test
ports:
- 3000:3000
volumes:
- /tmp/data:/data
options: --user root
| Option | Purpose |
|---|---|
image: |
Docker image to use |
env: |
Environment variables inside the container |
ports: |
Port mapping |
volumes: |
Volume mounts |
options: |
Additional docker run options |
credentials: |
Auth for private registries |
Service Containers
Service containers run alongside your job container. Common use: databases for integration tests.
jobs:
test:
runs-on: ubuntu-latest
container:
image: node:20
services:
postgres:
image: postgres:16
env:
POSTGRES_USER: testuser
POSTGRES_PASSWORD: testpass
POSTGRES_DB: testdb
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 ci
- name: Run tests with database
env:
DATABASE_URL: postgresql://testuser:testpass@postgres:5432/testdb
REDIS_URL: redis://redis:6379
run: npm test
Service Container Networking
| Scenario | How to Connect |
|---|---|
| Job in container | Use service name as hostname: postgres:5432 |
| Job directly on runner | Use localhost:5432 (mapped port) |
Private Registry Images
To pull from a private Docker registry:
jobs:
build:
runs-on: ubuntu-latest
container:
image: ghcr.io/my-org/my-image:latest
credentials:
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
When to Use Container Jobs
| Use Case | Container? |
|---|---|
| Need specific language version | ✅ Yes |
| Integration tests with databases | ✅ Yes (service containers) |
| Simple npm/pip install + test | ❌ Use runner directly |
| Need Docker-in-Docker | ❌ Use runner directly |
| Reproducible builds | ✅ Yes |
| macOS or Windows builds | ❌ Container jobs only work on Linux runners |
Important: Container jobs only work with ubuntu-* runners. You cannot use container: with windows-latest or macos-latest.
Common Mistakes
| Mistake | Fix |
|---|---|
| Container job on macOS/Windows | Container jobs only work on Linux runners |
| Can’t connect to service container | Use service name (not localhost) when job runs in a container |
| Image pull fails | Check image name spelling; for private images, add credentials: |
| Permission denied in container | Add options: --user root or adjust image permissions |
Quick Reference
| Concept | Syntax |
|---|---|
| Run in container | container: image: node:20 |
| Service container | services: db: image: postgres:16 |
| Container env | container: env: KEY: value |
| Private registry | container: credentials: username: / password: |
| Works on | Linux runners only (ubuntu-*) |
← Previous: Job Outputs
Next: Practice Exercises →