Docker Compose for Development: Building Reproducible Local Environments

"Works on my machine" is the war cry of every development team that lacks environment reproducibility. Docker Compose eliminates this by defining your entire local stack—services, networks, volumes, and dependencies—in a single declarative YAML file. Every developer, CI runner, and staging host runs the exact same environment.
Structuring a Multi-Service Compose File
A well-designed docker-compose.yml separates concerns while keeping services interconnected. For a typical web application with a database, cache layer, and background worker:
version: "3.9"
services:
web:
build:
context: .
dockerfile: Dockerfile.dev
ports:
- "3000:3000"
volumes:
- .:/app
- /app/node_modules
depends_on:
postgres:
condition: service_healthy
redis:
condition: service_started
environment:
DATABASE_URL: postgres://user:pass@postgres:5432/app
REDIS_URL: redis://redis:6379
postgres:
image: postgres:16-alpine
volumes:
- pgdata:/var/lib/postgresql/data
- ./scripts/init.sql:/docker-entrypoint-initdb.d/init.sql
healthcheck:
test: ["CMD-SHELL", "pg_isready -U user"]
interval: 5s
redis:
image: redis:7-alpine
volumes:
- redisdata:/data
worker:
build:
context: .
dockerfile: Dockerfile.dev
command: npm run worker
volumes:
- .:/app
depends_on:
postgres:
condition: service_healthy
volumes:
pgdata:
redisdata:
Key design decisions here: named volumes for databases so data survives container restarts, health checks on PostgreSQL so the app doesn't connect before the database is ready, and a bind mount for the application directory with an anonymous volume for node_modules to prevent host-OS binary conflicts.
Volume Mounts and Hot Reloading
The bind mount .:/app is what makes Docker Compose viable for development—file changes on the host propagate instantly into the container. Most frameworks with file watchers (Vite, Next.js, Rails, Django) detect these changes and trigger hot reloads without additional configuration.
Watch out for performance on macOS and Windows: bind mounts through Docker Desktop are significantly slower than native filesystem access. Mounting entire monorepos can degrade file-watching performance. Solutions include using Docker Sync or delegating mounts (,delegated suffix on Docker Desktop).
Networking and Service Discovery
Compose creates a default bridge network and registers each service name as a DNS entry. Services communicate by hostname: the web service connects to postgres:5432, not localhost:5432. This mirrors how Kubernetes service discovery works, giving you production parity at development time.
For testing external services like OAuth providers or third-party APIs, add a service block that mocks the external endpoint:
wiremock:
image: wiremock/wiremock:latest
ports:
- "8080:8080"
volumes:
- ./mocks:/home/wiremock
command: --global-response-templating
Environment Overrides and Profiles
Use multiple Compose files to layer configuration. A docker-compose.override.yml is automatically merged when running docker compose up. Keep production-like service definitions in the base file and add development conveniences (additional ports, debuggers, profiling tools) in overrides.
Compose profiles let developers spin up only the services they need:
profile: frontend-only
services:
web:
profiles: [full, frontend]
worker:
profiles: [full, worker]
A frontend developer runs docker compose --profile frontend up and skips the worker container entirely, saving memory and startup time.
Production Parity Considerations
The Dockerfile.dev should differ minimally from the production Dockerfile. Use multi-stage builds to share the base layer:
FROM node:20-alpine AS base
WORKDIR /app
COPY package*.json ./
RUN npm ci
FROM base AS dev
RUN npm install --include=dev
CMD ["npm", "run", "dev"]
FROM base AS production
COPY . .
RUN npm run build
CMD ["node", "dist/server.js"]
Both Dockerfiles start from the same base, ensuring the OS packages and Node version match between development and production. This catches deployment issues at docker compose up, not after git push.
Reproducibility Across Teams
Pin image tags to specific digests instead of latest or major version tags:
postgres:
image: postgres:16-alpine@sha256:abc123...
Add a .env file to your repository (with a .env.example committed) for secrets-optional configuration. Each developer drops a personal .env with local overrides that never enters the repo. Combine this with env_file directives to keep configuration clean.
Build Reproducible Environments with SoniNow
Docker Compose is the fastest path to a reproducible development environment that matches production behavior. Our team at SoniNow specializes in containerized development workflows and can help your team set up Docker-based environments that eliminate "works on my machine" for good.
Related Insights

AI-Generated Code: Using LLMs for Development Workflows in 2026
Learn how to effectively use AI-generated code in development workflows including prompt patterns for code, review strategies, security considerations, and integration with CI/CD.

CI/CD Pipeline Design: Automating Build, Test, and Deployment Workflows
A guide to designing CI/CD pipelines that automate build, test, and deployment including GitHub Actions, GitLab CI, environment strategies, and rollback patterns.

CI/CD Pipeline for Next.js: GitHub Actions to Vercel and Docker Deployments
A step-by-step guide to building CI/CD pipelines for Next.js applications using GitHub Actions including automated testing, preview deployments, Docker builds, and production rollouts.