GitHub Actions Service Containers
Many applications need a database, a cache, or a message queue to run their tests. Installing those tools by hand on every run wastes time. Service containers start them for you as small Docker containers that live beside your job and disappear when it ends.
Service Containers in Simple Words
Imagine a chef who needs a refrigerator while cooking. The kitchen delivers a fridge before the shift and removes it afterward. Your job is the chef, and the service container is the fridge. It exists only for the length of the job.
Basic Structure
A services block sits inside a job. Each service gets a label, an image, and optional settings.
jobs:
test:
runs-on: ubuntu-latest
services:
postgres:
image: postgres:16
env:
POSTGRES_PASSWORD: testpass
POSTGRES_DB: shop_test
ports:
- 5432:5432
options: >-
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
steps:
- uses: actions/checkout@v4
- name: Run tests
run: npm test
env:
DATABASE_URL: postgres://postgres:testpass@localhost:5432/shop_testUnderstanding Each Setting
image
The image names the Docker image to download. Pin a version tag such as postgres:16 so results stay repeatable.
env
Environment variables configure the service. The Postgres image reads the password and database name from these values.
ports
The entry 5432:5432 maps a container port to a port on the runner machine. Write only the container port, such as 5432, to let GitHub pick a random free host port.
options
These flags pass straight to the Docker create command. Health checks make the job wait until the database accepts connections, which stops random startup failures.
Architecture Diagram
+---------------------- GitHub-hosted runner -----------------------+
| |
| +----------------+ localhost:5432 +--------------------+ |
| | Job steps | -----------------> | postgres container | |
| | (npm test) | | (service) | |
| +----------------+ +--------------------+ |
| |
+-------------------------------------------------------------------+Two Ways to Connect
| Where the Job Runs | Hostname to Use | Port to Use |
|---|---|---|
| Directly on the runner | localhost | The mapped host port |
Inside a container (container: key) | The service label, such as postgres | The container port |
Containers on the same job share a private Docker network. The label becomes a hostname automatically.
Example With Two Services
A web application might need both a database and a cache.
services:
db:
image: mysql:8
env:
MYSQL_ROOT_PASSWORD: rootpass
ports:
- 3306:3306
cache:
image: redis:7
ports:
- 6379:6379
options: --health-cmd "redis-cli ping" --health-interval 10s
Job steps ---> db (MySQL, port 3306)
---> cache (Redis, port 6379)Rules and Limits
- Service containers run only on Linux runners. Windows and macOS runners do not support them.
- Each job gets its own fresh services. Services never carry data from a previous run.
- Private images need a
credentialsblock with a username and a secret password.
Troubleshooting
Connection refused errors usually mean the service is not ready. Add a health check in options. A wrong hostname causes the next most common failure: use localhost for runner jobs and the label for container jobs. When a test cannot find tables, run your migration step before the tests start.
Why Teams Choose This Approach
Tests run against a real database instead of a fake one, so bugs surface earlier. Setup stays inside the workflow file, which means every developer and every run uses identical versions. Cleanup happens automatically, and no leftover data pollutes the next run.
