From Single Container to Full Stack
In the previous lesson, you learned how to run tests in a single Docker container. But real applications rarely run in isolation. A typical web application needs a database, a cache, possibly a message queue, and maybe an email service. Docker Compose lets you define all of these as a single stack that starts and stops together.
For QA, Docker Compose is transformative. Instead of manually setting up PostgreSQL, Redis, and the application before running integration tests, you define everything in a docker-compose.yml file and start it with a single command.
Docker Compose Basics
Minimal Example
version: '3.8'
services:
app:
build: .
ports:
- "3000:3000"
environment:
- DATABASE_URL=postgresql://test:test@db:5432/testdb
- REDIS_URL=redis://cache:6379
depends_on:
db:
condition: service_healthy
cache:
condition: service_started
db:
image: postgres:15
environment:
POSTGRES_DB: testdb
POSTGRES_USER: test
POSTGRES_PASSWORD: test
healthcheck:
test: ["CMD-SHELL", "pg_isready -U test"]
interval: 5s
timeout: 5s
retries: 5
cache:
image: redis:7-alpine
Starting and Stopping
# Start all services
docker compose up -d
# Start and rebuild images
docker compose up -d --build
# View logs
docker compose logs -f
# Stop and remove containers
docker compose down
# Stop, remove containers AND volumes (clean state)
docker compose down -v
Test Environment Patterns
Pattern 1: App + Dependencies + Test Runner
The most common pattern for integration and E2E testing:
version: '3.8'
services:
app:
build: .
ports:
- "3000:3000"
environment:
- NODE_ENV=test
- DATABASE_URL=postgresql://test:test@db:5432/testdb
depends_on:
db:
condition: service_healthy
db:
image: postgres:15
environment:
POSTGRES_DB: testdb
POSTGRES_USER: test
POSTGRES_PASSWORD: test
volumes:
- ./scripts/init-test-db.sql:/docker-entrypoint-initdb.d/init.sql
healthcheck:
test: ["CMD-SHELL", "pg_isready -U test"]
interval: 5s
timeout: 5s
retries: 5
tests:
build:
context: .
dockerfile: Dockerfile.tests
environment:
- BASE_URL=http://app:3000
- DATABASE_URL=postgresql://test:test@db:5432/testdb
depends_on:
app:
condition: service_started
volumes:
- ./test-results:/app/test-results
Running the tests:
# Start dependencies, run tests, get exit code
docker compose run --rm tests
echo "Exit code: $?"
# Clean up everything
docker compose down -v
Pattern 2: Parallel Browser Testing
version: '3.8'
services:
app:
build: .
ports:
- "3000:3000"
chrome-tests:
image: mcr.microsoft.com/playwright:v1.40.0-focal
working_dir: /app
volumes:
- .:/app
- ./results/chrome:/app/test-results
environment:
- BASE_URL=http://app:3000
command: npx playwright test --project=chromium
depends_on:
- app
firefox-tests:
image: mcr.microsoft.com/playwright:v1.40.0-focal
working_dir: /app
volumes:
- .:/app
- ./results/firefox:/app/test-results
environment:
- BASE_URL=http://app:3000
command: npx playwright test --project=firefox
depends_on:
- app
Pattern 3: Full Integration Stack
version: '3.8'
services:
app:
build: .
environment:
- DATABASE_URL=postgresql://test:test@db:5432/testdb
- REDIS_URL=redis://cache:6379
- MAIL_HOST=mailhog
- MAIL_PORT=1025
- S3_ENDPOINT=http://localstack:4566
depends_on:
db:
condition: service_healthy
cache:
condition: service_started
db:
image: postgres:15
environment:
POSTGRES_DB: testdb
POSTGRES_USER: test
POSTGRES_PASSWORD: test
healthcheck:
test: ["CMD-SHELL", "pg_isready -U test"]
interval: 5s
timeout: 5s
retries: 5
cache:
image: redis:7-alpine
mailhog:
image: mailhog/mailhog
ports:
- "8025:8025"
localstack:
image: localstack/localstack
environment:
- SERVICES=s3
- DEFAULT_REGION=us-east-1
Health Checks
Health checks are essential for test reliability. Without them, tests may start before the database is ready to accept connections.
db:
image: postgres:15
healthcheck:
test: ["CMD-SHELL", "pg_isready -U test"]
interval: 5s
timeout: 5s
retries: 5
start_period: 10s
elasticsearch:
image: elasticsearch:8.11.0
healthcheck:
test: ["CMD-SHELL", "curl -f http://localhost:9200/_cluster/health || exit 1"]
interval: 10s
timeout: 10s
retries: 10
start_period: 30s
Exercise: Build a Complete Test Environment
Design a docker-compose.yml for an e-commerce application with:
- Node.js API backend
- PostgreSQL database (with seed data)
- Redis cache
- Email service (for order confirmations)
- Test runner for API and E2E tests
- Test results saved to the host machine
Solution
version: '3.8'
services:
api:
build:
context: .
dockerfile: Dockerfile
ports:
- "3000:3000"
environment:
- NODE_ENV=test
- DATABASE_URL=postgresql://test:test@db:5432/ecommerce_test
- REDIS_URL=redis://cache:6379
- MAIL_HOST=mailhog
- MAIL_PORT=1025
- JWT_SECRET=test-secret
depends_on:
db:
condition: service_healthy
cache:
condition: service_started
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:3000/health"]
interval: 5s
timeout: 5s
retries: 10
db:
image: postgres:15
environment:
POSTGRES_DB: ecommerce_test
POSTGRES_USER: test
POSTGRES_PASSWORD: test
volumes:
- ./scripts/seed-test-data.sql:/docker-entrypoint-initdb.d/01-seed.sql
healthcheck:
test: ["CMD-SHELL", "pg_isready -U test"]
interval: 5s
timeout: 5s
retries: 5
cache:
image: redis:7-alpine
mailhog:
image: mailhog/mailhog
ports:
- "8025:8025"
api-tests:
build:
context: .
dockerfile: Dockerfile.tests
environment:
- BASE_URL=http://api:3000
- DATABASE_URL=postgresql://test:test@db:5432/ecommerce_test
- MAILHOG_URL=http://mailhog:8025
command: npm run test:api
depends_on:
api:
condition: service_healthy
volumes:
- ./test-results/api:/app/test-results
e2e-tests:
image: mcr.microsoft.com/playwright:v1.40.0-focal
working_dir: /app
volumes:
- .:/app
- ./test-results/e2e:/app/test-results
environment:
- BASE_URL=http://api:3000
command: npx playwright test
depends_on:
api:
condition: service_healthy
volumes:
test-results:
Usage:
# Run API tests
docker compose run --rm api-tests
# Run E2E tests
docker compose run --rm e2e-tests
# Run both (parallel)
docker compose up api-tests e2e-tests
# Clean up
docker compose down -v
CI Integration
Integrate Docker Compose into your CI pipeline:
# GitHub Actions example
- name: Start test environment
run: docker compose up -d --build --wait
- name: Run tests
run: docker compose run --rm tests
- name: Collect results
if: always()
run: |
docker compose logs > docker-logs.txt
- name: Teardown
if: always()
run: docker compose down -v
Best Practices
Always use health checks with
depends_on: condition: service_healthy. Starting order alone is not enough — the database process may start but not be ready for connections.Use
docker compose down -vafter tests. The-vflag removes volumes, ensuring each test run starts with a clean state.Mount test results as volumes. This makes reports accessible on the host machine and in CI artifacts.
Keep test data in SQL seed files mounted via
docker-entrypoint-initdb.d/. This ensures consistent, reproducible test data.Use
docker compose run --rmfor test execution. The--rmflag removes the container after it exits, preventing accumulation of stopped containers.Pin image versions. Use
postgres:15instead ofpostgres:latestto avoid unexpected behavior changes.