Copyright (c) 2026 MindMesh Academy. All rights reserved. This content is proprietary and may not be reproduced or distributed without permission.

2.2.3. Service Containers

💡 First Principle: Integration tests need real dependencies — a database, a cache, a queue — and services: gives a job disposable ones that GitHub starts before your steps and tears down after, so tests run against the real thing without you managing infrastructure.

A service is a Docker container attached to the job's lifecycle. GitHub creates a bridge network for the job, so containers reach each other by service label:

jobs:
  test:
    runs-on: ubuntu-latest
    services:
      postgres:
        image: postgres:16
        env:
          POSTGRES_PASSWORD: postgres
        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 test
        env:
          DATABASE_URL: postgres://postgres:postgres@localhost:5432/postgres

The single most exam-relevant detail is hostname resolution, which depends on where your steps run. If the job runs directly on the runner machine (no container: key), your steps reach services at localhost on the mapped port — that's why ports: matters. If the job itself runs in a container (container: key present), Docker's user-defined network gives you the service label as hostname (postgres:5432) and port mapping is unnecessary. Swapping those two is a favorite distractor.

Health checks in options: are how you avoid the flakiest failure in CI: tests connecting before the database finishes initializing. GitHub waits for the health check to pass before running your steps. Service containers require a Linux runner, and images can come from Docker Hub, GHCR, or any registry (with credentials: for private ones).

⚠️ Exam Trap: Without a health check, a service is considered "started" as soon as the container starts — not when the software inside it is ready. Intermittent "connection refused" in a scenario points at a missing --health-cmd, not at the test code.

Reflection Question: Your job adds container: node:20 and suddenly localhost:5432 stops resolving to the Postgres service. What changed about the network topology, and what's the one-word fix to the connection string?

See how it connects
Alvin Varughese
Written byAlvin Varughese
Founder18 professional certifications