Skip to main content

Docker Compose in CI/CD Pipelines — GitHub Actions Integration

Use Docker Compose inside GitHub Actions to run integration tests against real databases, caches, and queues before every merge.

44 Terms

Overview and What You Will Learn

Unit tests with mocked database calls catch some bugs. They miss the ones that matter most — a SQL query with a subtle JOIN error, a Redis key expiry race condition, a RabbitMQ message acknowledgement bug. Catching those requires running tests against the real thing.

Docker Compose makes this practical inside CI. The same compose file used for local development can spin up real Postgres, Redis, and RabbitMQ instances inside a GitHub Actions runner, run the full integration test suite against them, and tear everything down — all in a few minutes, on every pull request.

By the end of this lab you will:

  • Run a Compose stack inside a GitHub Actions workflow
  • Use Compose profiles to define services that only start in CI
  • Override compose config for CI-specific ports and settings
  • Wait correctly for services to be ready before running tests
  • Collect logs from a failed CI run for debugging
  • Cache Docker layers in GitHub Actions to speed up repeated runs

Why This Matters in Production

At CRED, a backend pull request cannot merge unless the integration test suite passes against a real Postgres and Redis instance spun up inside the CI runner itself — not mocks, not a shared staging database that ten engineers are also hitting at the same time. This catches data-layer bugs before they reach staging, where they are far more expensive to trace back to a single commit.

The key engineering insight is that CI dependencies should be ephemeral and isolated per run. Using Compose for this means every PR gets a brand new, throwaway Postgres instance — no test pollution between runs, no shared state, no "works on CI sometimes" flakiness caused by leftover data from a previous run.

Core Principles

CI pipeline with Compose service lifecycle:

Bash
+------------------------------------------+
| GitHub Actions job starts |
+------------------------------------------+
|
v
+------------------------------------------+
| docker compose up -d --wait | <- starts postgres, redis,
+------------------------------------------+ rabbitmq, waits for healthy
|
v
+------------------------------------------+
| Run integration test suite | <- tests hit real services
+------------------------------------------+ over localhost ports
|
v
+------------------------------------------+
| Collect logs if tests failed | <- docker compose logs
+------------------------------------------+ uploaded as CI artifact
|
v
+------------------------------------------+
| docker compose down --volumes | <- full teardown, no
+------------------------------------------+ leftover state for next run

Profiles separate CI-only services from default services:

Bash
+------------------------+ +------------------------------+
| Default services | | CI-profile services |
| | | |
| api, postgres, redis | <------> | test-seeder, mock-payment-gw |
| start with plain | | start only with --profile ci |
| docker compose up | | docker compose --profile ci |
+------------------------+ +------------------------------+

Detailed Step-by-Step Practical Lab

Milestone 1 — Write a CI-specific compose file

YAML
## docker-compose.ci.yml
## Overrides ports and adds CI-only services on top of the base file
services:
postgres:
ports:
# Different host port avoids collisions with other CI jobs
# running on the same self-hosted runner
- "55432:5432"
healthcheck:
test: ["CMD-SHELL", "pg_isready -U cred_test"]
interval: 3s
timeout: 2s
retries: 10
redis:
ports:
- "56379:6379"
test-seeder:
build: ./scripts/seed
profiles: ["ci"]
depends_on:
postgres:
condition: service_healthy
environment:
DATABASE_URL: postgres://cred_test:test_pw@postgres:5432/cred_test

Milestone 2 — Write the GitHub Actions workflow

YAML
## .github/workflows/integration-tests.yml
name: Integration Tests
on:
pull_request:
branches: [main]
jobs:
integration-tests:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Cache Docker layers
uses: actions/cache@v4
with:
path: /tmp/.buildx-cache
key: ${{ runner.os }}-buildx-${{ hashFiles('**/Dockerfile') }}
restore-keys: |
${{ runner.os }}-buildx-
- name: Start dependency stack
run: |
docker compose \
-f docker-compose.yml \
-f docker-compose.ci.yml \
--profile ci \
up -d --wait
- name: Run integration test suite
run: |
export DATABASE_URL=postgres://cred_test:test_pw@localhost:55432/cred_test
export REDIS_URL=redis://localhost:56379
npm run test:integration
- name: Collect logs on failure
if: failure()
run: docker compose logs --no-color > compose-logs.txt
- name: Upload failure logs
if: failure()
uses: actions/upload-artifact@v4
with:
name: compose-logs
path: compose-logs.txt
- name: Tear down stack
if: always()
run: docker compose -f docker-compose.yml -f docker-compose.ci.yml down --volumes

Milestone 3 — Use --wait instead of manual sleep or polling scripts

Bash
## --wait blocks until all services with a healthcheck report healthy
## Replaces fragile custom polling loops entirely
docker compose up -d --wait
## Set a custom timeout if the default is too short for slow services
docker compose up -d --wait --wait-timeout 60

Milestone 4 — Activate CI-only services with profiles

Bash
## Locally: test-seeder does NOT start (no profile flag passed)
docker compose up -d
## In CI: test-seeder DOES start, populating fixtures before tests run
docker compose --profile ci up -d --wait

Milestone 5 — Collect logs from a failed CI run

Bash
## Dump all service logs, no color codes (cleaner in CI log viewers)
docker compose logs --no-color > compose-logs.txt
## Dump logs for just one suspicious service
docker compose logs --no-color postgres > postgres-logs.txt
## Include timestamps to correlate with test failure timing
docker compose logs --no-color -t > compose-logs-timestamped.txt

Milestone 6 — Guarantee teardown even when tests fail

YAML
## The 'if: always()' condition is critical -- without it,
## a failed test step would skip teardown and leak containers
## on the runner, eventually exhausting disk space
- name: Tear down stack
if: always()
run: docker compose down --volumes --remove-orphans

Production Best Practices and Common Pitfalls

Scenario Wrong Correct
Waiting for services sleep 10 before running tests docker compose up -d --wait
CI-only fixtures Always running seed/mock services locally too Use profiles: ["ci"] to isolate them
Port collisions in CI Same ports as local dev compose file Override file maps different host ports
Failed test cleanup Teardown step runs only on success if: always() on the teardown step
Debugging a flaky CI failure Re-run and hope Upload docker compose logs as a CI artifact every failure

Quick Reference and Troubleshooting Commands

Task Command
Start and wait for healthy docker compose up -d --wait
Start with CI profile docker compose --profile ci up -d --wait
Use multiple compose files docker compose -f base.yml -f ci.yml up -d
Dump logs for debugging docker compose logs --no-color > out.txt
Full teardown including orphans docker compose down --volumes --remove-orphans
View merged CI config docker compose -f base.yml -f ci.yml config
Tip

Use docker compose up -d --wait instead of hand-written polling loops or sleep statements. It directly uses each service's healthcheck definition, so the CI pipeline and local development share exactly the same readiness logic.

Remember

Multiple -f flags merge compose files in the order given, with later files overriding earlier ones. docker compose -f docker-compose.yml -f docker-compose.ci.yml applies the base file first, then layers CI-specific overrides on top.

Common Mistake

Forgetting if: always() on the teardown step in a GitHub Actions workflow. If a test step fails, the workflow skips remaining steps by default — leaving containers and volumes running on the runner and silently consuming resources on every failed PR.

Security

Never reuse production database credentials in a CI compose override file, even temporarily. Use clearly fake, CI-only credentials (e.g. cred_test / test_pw) that have zero validity outside the ephemeral CI environment, so an accidental leak in CI logs causes no real damage.

Resources

Explore More in Docker Compose and Multi-Container Applications

All 6 Topics

Frequently Asked Questions

Is Docker Compose in CI/CD Pipelines — GitHub Actions Integration free to learn on DevOps Network?

Yes - this topic, like everything on DevOps Network, is 100% free with no paywall or sign-up gate.

What does the Docker Compose in CI/CD Pipelines — GitHub Actions Integration topic cover?

Use Docker Compose inside GitHub Actions to run integration tests against real databases, caches, and queues before every merge.