Service containers are dependencies — a database, cache, message broker, or a stubbed API — that the CI system starts as containers alongside your test job, giving integration tests a real, ephemeral backend that is created fresh for the run and torn down afterward.
Integration and API tests often need a real database or dependent service, not a mock. Pointing them at a shared, long-lived staging database is a recipe for flakiness: tests collide on shared data, one run's leftovers break the next, and the shared instance becomes a bottleneck and single point of failure. Service containers solve this by spinning up a dedicated, disposable instance (e.g. Postgres) scoped to the job. The test job waits for the service to be healthy, runs migrations/seed against it, executes tests, and the container is discarded — perfect isolation and repeatability. Each parallel job gets its own service, so parallelism doesn't cause data collisions. This is the sweet spot between fully mocking (fast but not testing the real integration) and using shared infrastructure (realistic but flaky and contended).
API integration tests used to hit a shared staging Postgres and flaked whenever two pipelines ran at once, colliding on the same rows. Switching to a Postgres service container per job gives each run its own clean database seeded from scratch, so concurrent pipelines never interfere and results are deterministic.
jobs:
integration:
runs-on: ubuntu-latest
services:
postgres:
image: postgres:16
env:
POSTGRES_PASSWORD: test
POSTGRES_DB: appdb
ports: ['5432:5432']
options: >-
--health-cmd="pg_isready" --health-interval=5s
--health-timeout=5s --health-retries=5
steps:
- uses: actions/checkout@v4
- run: npm ci
- run: npm run db:migrate && npm run db:seed
env: { DATABASE_URL: 'postgres://postgres:test@localhost:5432/appdb' }
- run: npm run test:integration
env: { DATABASE_URL: 'postgres://postgres:test@localhost:5432/appdb' }Scenario: Integration tests point at a shared staging database. They flake whenever multiple pipelines run concurrently (duplicate-key and 'row not found' errors), and occasionally corrupt staging data for the whole team. How do you redesign this?