6.3 Service Containers & Network Topology
Key Takeaways
- Service containers (`services:`) provide ephemeral, isolated Docker containers (e.g., PostgreSQL, Redis, MySQL) alongside workflow jobs to facilitate integration and end-to-end testing.
- When a job runs directly on a virtual machine host runner, service container ports must be mapped (`ports: - 5432:5432`), and steps communicate with the service via `localhost:<port>` or `127.0.0.1:<port>`.
- When a job runs inside a container (`container: { image: ... }`), all service containers and the job container share the same user-defined Docker bridge network, allowing direct communication via the service container name (`postgres:5432`, `redis:6379`) without port mapping.
- Service health checks (`options: --health-cmd ... --health-interval ... --health-timeout ... --health-retries ...`) ensure the service daemon is fully initialized before workflow steps begin execution.
- Service containers are isolated per job and automatically torn down when the job finishes, preventing cross-test data pollution and resource leaks.
Service Containers & Network Topology
Modern continuous integration pipelines require realistic testing environments that mirror production infrastructure. Rather than relying on fragile mock libraries, integration tests frequently require real database engines (PostgreSQL, MySQL, MongoDB), in-memory caches (Redis, Memcached), or messaging brokers (RabbitMQ, Kafka).
GitHub Actions provides Service Containers (services:) to host these auxiliary services in ephemeral, isolated Docker containers for the duration of a job. Mastering the network topology differences between host-level VM jobs and containerized jobs is a fundamental concept on the GH-200 exam.
1. Service Containers Architecture & Lifecycle
A service container is declared under the jobs.<job_id>.services key. GitHub Actions manages the complete lifecycle of service containers:
- Pre-Job Initialization: Before any workflow steps execute, the runner pulls the container images and starts all defined service containers concurrently.
- Health Check Validation: If health checks are configured, the runner waits until all services report a healthy status before invoking the first step.
- Step Execution: Job steps interact with the services across the network.
- Post-Job Teardown: Once all job steps complete (whether successful, failed, or cancelled), the runner automatically stops and deletes all service containers and their associated networks.
+-----------------------------------------------------------------------------+
| SERVICE CONTAINER LIFECYCLE |
| |
| [Job Queued] ──> [Pull Images & Start Containers in Parallel] |
| │ |
| v |
| [Evaluate Health Checks (--health-cmd)] |
| │ |
| v |
| [Execute Job Steps (Checkout, Test)] |
| │ |
| v |
| [Stop & Destroy Service Containers] ──> [Job Completed] |
+-----------------------------------------------------------------------------+
2. Network Topology: Host VM Runner vs. Container Job
The way workflow steps connect to service containers depends entirely on whether the job itself executes directly on the host virtual machine or inside a job container.
+-----------------------------------------------------------------------------+
| SCENARIO A: JOB ON HOST VM RUNNER |
| |
| +---------------------------------------------------------------------+ |
| | RUNNER HOST VM (runs-on: ubuntu-latest) | |
| | | |
| | Steps execute on Host OS: | |
| | `npm test` connects to: `localhost:5432` or `127.0.0.1:6379` | |
| | │ │ | |
| | │ Port 5432:5432 │ Port 6379:6379 | |
| | v v | |
| | +───────────────────────────+ +───────────────────────────+ | |
| | | Service Container: pg | | Service Container: redis | | |
| | | image: postgres:16 | | image: redis:7-alpine | | |
| | +───────────────────────────+ +───────────────────────────+ | |
| +---------------------------------------------------------------------+ |
+-----------------------------------------------------------------------------+
+-----------------------------------------------------------------------------+
| SCENARIO B: JOB INSIDE CONTAINER |
| |
| +---------------------------------------------------------------------+ |
| | RUNNER HOST VM (runs-on: ubuntu-latest) | |
| | | |
| | +----------------------- DOCKER BRIDGE NETWORK -----------------+ | |
| | | | | |
| | | +──────────────────────+ +──────────────────────────+ | | |
| | | | Job Container | | Service Container | | | |
| | | | (container: node:20) | | (services.postgres) | | | |
| | | | | | | | | |
| | | | Connects directly |──────>| Hostname: 'postgres' | | | |
| | | | to: 'postgres:5432' | | Port: 5432 (Internal) | | | |
| | | +──────────────────────+ +──────────────────────────+ | | |
| | | | | |
| | +───────────────────────────────────────────────────────────────+ | |
| +---------------------------------------------------------------------+ |
+-----------------------------------------------------------------------------+
Detailed Comparison Matrix
| Configuration Dimension | Job on Host VM Runner (runs-on: ubuntu-latest) | Job in Container (container: node:20) |
|---|---|---|
| Where Steps Run | Directly in the host virtual machine operating system. | Inside the specified container execution context. |
| Service Hostname | localhost or 127.0.0.1 | Service container label name (e.g., postgres, redis). |
Port Mapping (ports:) | Mandatory: Must map host port to container port (e.g., 5432:5432). | Not Required: Containers share the Docker bridge network; all ports are accessible directly. |
| Docker Network | Runner creates bridge network for services; maps ports to host. | Runner attaches job container and service containers to the same bridge network. |
| DNS Resolution | Host OS cannot resolve container names via Docker DNS. | Docker internal DNS resolves container label names automatically. |
[!WARNING] If your job runs on a host VM and you omit the
ports:declaration in your service definition, your steps will not be able to connect to the service container vialocalhost, resulting in connection refused errors (ECONNREFUSED).
3. Configuring Health Checks for Service Containers
Database engines and distributed systems take several seconds to initialize filesystems, run migrations, and open listening sockets. If steps execute before the service is ready, tests will fail with race conditions.
Health checks are configured using Docker daemon options passed via the options: key:
services:
postgres:
image: postgres:16
env:
POSTGRES_DB: testdb
POSTGRES_USER: testuser
POSTGRES_PASSWORD: testpassword
ports:
- 5432:5432
# Docker health check parameters
options: >-
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
Health Check Parameters Explained
--health-cmd: The command executed inside the container to test service readiness (e.g.,pg_isready,mysqladmin ping,redis-cli ping, orcurl -f http://localhost:8080/health).--health-interval: The duration between consecutive health check probes (e.g.,10s).--health-timeout: The maximum time permitted for the health command to complete before marking the check as a failure (e.g.,5s).--health-retries: The number of consecutive failed checks required before the container is marked unhealthy (e.g.,5).--health-start-period: Grace period providing the container time to bootstrap before health checks begin counting against retries.
4. Complete Multi-Service Integration Workflow
The following complete workflow demonstrates running both PostgreSQL and Redis service containers to support an integration test suite running on an Ubuntu VM host:
name: Backend Integration Tests
on:
push:
branches: [main]
pull_request:
branches: [main]
jobs:
integration-suite:
runs-on: ubuntu-latest
services:
postgres:
image: postgres:16-alpine
env:
POSTGRES_DB: test_enterprise
POSTGRES_USER: runner_admin
POSTGRES_PASSWORD: secret_password123
ports:
- 5432:5432
options: >-
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
redis:
image: redis:7-alpine
ports:
- 6379:6379
options: >-
--health-cmd "redis-cli ping"
--health-interval 5s
--health-timeout 3s
--health-retries 3
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Setup Node.js runtime
uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm'
- name: Install test dependencies
run: npm ci
- name: Execute Integration Tests against Services
run: npm run test:integration
env:
DATABASE_URL: postgres://runner_admin:secret_password123@localhost:5432/test_enterprise
REDIS_URL: redis://localhost:6379
A developer configures a workflow job with runs-on: ubuntu-latest and declares a PostgreSQL service container under services.postgres with image: postgres:16. In the job steps, the test application fails with ECONNREFUSED when trying to connect to localhost:5432. What is the most likely configuration omission in the workflow file?
A workflow job is configured to execute inside a container using container: node:20-alpine, and defines a Redis cache service under services.redis with image: redis:7-alpine. How should the Node.js test script running in this job connect to the Redis service?
An integration test suite periodically fails because database migration scripts execute immediately before the PostgreSQL database engine in the service container has finished bootstrapping. What is the recommended GitHub Actions configuration to eliminate this race condition?