↓Skip to main content
  1. Docker Tutorials/

How to Use Docker Compose for Development and Testing

·3 mins Draft

Local development and CI testing put different demands on a Compose setup than production does: you want hot reload, verbose logging, and disposable state — none of which belong in a production configuration. Compose’s override-file mechanism lets you keep one base definition and layer environment-specific differences on top.

Hot reload with bind mounts #

For local development, mount your source directory into the running container so edits on the host are reflected immediately, instead of rebuilding the image on every change:

# docker-compose.override.yml (merged automatically with docker-compose.yml)
services:
  api:
    volumes:
      - ./src:/app/src
    command: npm run dev   # overrides the Dockerfile's CMD with a watch-mode dev server

Compose automatically merges docker-compose.yml with docker-compose.override.yml if the override file is present — no extra flag needed. This keeps the base file production-safe (no bind mounts, no dev-only command) while docker compose up locally picks up the override transparently.

Separate files for separate environments #

For anything beyond a simple dev/prod split, name override files explicitly and choose which to apply:

docker compose -f docker-compose.yml -f docker-compose.test.yml up
# docker-compose.test.yml
services:
  db:
    image: postgres:16
    tmpfs:
      - /var/lib/postgresql/data   # in-memory, wiped on every run — no persistence needed for tests
  api:
    environment:
      NODE_ENV: test

Running the database on tmpfs for tests is a deliberate choice: test runs should start from a known-empty state every time, and disk persistence just risks stale data leaking between test runs.

Integration tests against a real database #

The core value of Compose for testing: CI can spin up a real Postgres, run migrations, run the test suite against it, and tear it down — instead of mocking the database and hoping the mock’s behavior matches reality closely enough to be worth trusting.

docker compose -f docker-compose.test.yml up -d db
docker compose -f docker-compose.test.yml run --rm api npm test
docker compose -f docker-compose.test.yml down -v

run --rm (rather than up) is the right choice for the test command itself — it starts a one-off container, streams output to your terminal, exits with the test runner’s exit code (so CI can detect failure), and removes itself afterward.

Watching for build-time vs. runtime confusion #

A common mistake: editing a Dockerfile’s RUN npm ci step and expecting a running dev container to pick up new dependencies. It won’t — RUN instructions only execute during docker build. After changing dependencies, rebuild:

docker compose up --build

--build forces Compose to rebuild images (respecting layer cache — see how building works) before starting, instead of reusing whatever was built last time.

Keeping dev and prod honestly close #

The point of override files is to differ only where you must (hot reload, test data, verbose logging) while keeping the base image, base service definitions, and versions identical to production. If your dev Compose file uses a different database major version or a different base image than production, you’ve quietly reintroduced the “works in dev, breaks in prod” gap that containers were supposed to close in the first place.