Your development Compose file isn’t your CI Compose file. A dedicated CI configuration ensures tests run against a clean, seeded database with no leftover state.
The development stack
Take a typical full-stack project like dockersamples/sbx-quickstart — a FastAPI backend with a Next.js frontend and PostgreSQL:
# compose.yml
services:
backend:
build: ./backend
ports:
- "8000:8000"
environment:
DATABASE_URL: postgresql://postgres:postgres@db:5432/devboard
depends_on:
db:
condition: service_healthy
frontend:
build: ./frontend
ports:
- "3000:3000"
db:
image: postgres:16
environment:
POSTGRES_PASSWORD: postgres
POSTGRES_DB: devboard
volumes:
- db-data:/var/lib/postgresql/data
healthcheck:
test: ["CMD", "pg_isready"]
interval: 5s
retries: 5
volumes:
db-data:
Adding a CI override
Create a compose.ci.yml that adapts the stack for testing:
# compose.ci.yml
services:
db:
# No persistent volume in CI — fresh database every run
volumes:
- ./backend/tests/seed.sql:/docker-entrypoint-initdb.d/seed.sql:ro
backend:
# No port exposure needed — tests run inside the network
ports: !reset []
environment:
TESTING: "true"
frontend:
# Not needed for API tests
profiles: ["frontend"]
tests:
build: ./backend
command: pytest tests/ -v --tb=short
environment:
DATABASE_URL: postgresql://postgres:postgres@db:5432/devboard
depends_on:
backend:
condition: service_started
db:
condition: service_healthy
Key decisions:
- Database seeding: mount a SQL file into
/docker-entrypoint-initdb.d/— Postgres runs it automatically on init - No volumes: the
db-datavolume from the base file is not mounted, so every run starts fresh - Frontend disabled: moved to a profile so it’s not started during API tests
- Test service added: runs pytest against the backend through the Compose network
The seed file
-- backend/tests/seed.sql
INSERT INTO users (username, email) VALUES
('testuser1', 'test1@example.com'),
('testuser2', 'test2@example.com');
INSERT INTO projects (name, owner_id) VALUES
('Test Project', 1);
Running in CI
# Start stack, wait for health, run tests, return test exit code
docker compose -f compose.yml -f compose.ci.yml up \
--build \
--exit-code-from tests
# Clean teardown — remove volumes for a fresh next run
docker compose -f compose.yml -f compose.ci.yml down --volumes
The --exit-code-from tests flag makes the command return the exit code of the tests service — so your CI pipeline fails if tests fail.
Using –wait for multi-step workflows
If you need to run different test suites sequentially, use --wait (Tip #51) to start the stack first, then run tests separately:
# Start and wait for everything to be healthy
docker compose -f compose.yml -f compose.ci.yml up -d --build --wait --wait-timeout 120
# Run API tests
docker compose -f compose.yml -f compose.ci.yml exec backend pytest tests/api/ -v
# Run integration tests
docker compose -f compose.yml -f compose.ci.yml exec backend pytest tests/integration/ -v
# Clean up
docker compose -f compose.yml -f compose.ci.yml down --volumes
Pro tip
Use --project-name to isolate parallel CI runs on the same host:
docker compose -p "ci-${BUILD_ID}" -f compose.yml -f compose.ci.yml up \
--build --exit-code-from tests
docker compose -p "ci-${BUILD_ID}" -f compose.yml -f compose.ci.yml down --volumes
Each pipeline run gets its own networks and containers — no conflicts.
Further reading
- Compose documentation: Multiple Compose files
- dockersamples/sbx-quickstart — the example project used in this post