DockerIntermediate

Docker Compose for Local Development Environments

Using docker-compose.yml to spin up a full local stack — app, database, cache — with one command, and the patterns that make it actually pleasant to work with.

DevFieldGuideJuly 8, 2026 (updated July 29, 2026)6 min read
Share:

Running a database, a cache, and your app together locally usually means either installing each one natively (version conflicts across projects, guaranteed) or hand-writing a handful of docker run commands you forget the flags to every time. Compose fixes both.

A typical setup

yaml
# docker-compose.yml
services:
  app:
    build: .
    ports:
      - "3000:3000"
    environment:
      DATABASE_URL: postgres://postgres:postgres@db:5432/myapp
    depends_on:
      - db
      - redis
    volumes:
      - .:/app
      - /app/node_modules
 
  db:
    image: postgres:16
    environment:
      POSTGRES_PASSWORD: postgres
      POSTGRES_DB: myapp
    ports:
      - "5432:5432"
    volumes:
      - db-data:/var/lib/postgresql/data
 
  redis:
    image: redis:7
    ports:
      - "6379:6379"
 
volumes:
  db-data:
bash
docker compose up          # start everything
docker compose up -d        # same, but detached (background)
docker compose down         # stop and remove containers
docker compose logs -f app  # follow logs for one service

One command brings up the app, a Postgres database, and Redis, all networked together automatically — services can reach each other by name (db, redis) without manually configuring a Docker network.

appYour service, port 3000
dbPostgres, reachable as "db"
redisCache, reachable as "redis"

The volumes trick that avoids a common node_modules bug

yaml
volumes:
  - .:/app              # mount the whole project directory
  - /app/node_modules     # but NOT node_modules — use the container's own

Without the second line, mounting your entire project directory into the container also overwrites the container's node_modules with whatever's on your host machine — which can be the wrong platform's compiled binaries (e.g., host is macOS, container is Linux) and cause cryptic native-module errors. This pattern keeps the code live-synced for hot reload while letting the container manage its own dependencies.

Environment-specific overrides

yaml
# docker-compose.override.yml (automatically merged with docker-compose.yml)
services:
  app:
    command: npm run dev
    environment:
      NODE_ENV: development

Compose automatically merges docker-compose.override.yml on top of the base file if it exists — a clean way to keep local-only tweaks (like running a dev server with hot reload) separate from a base config that might also be used in CI.

Running one-off commands inside a service

bash
docker compose exec app npm run migrate
docker compose exec db psql -U postgres -d myapp

exec runs a command inside an already-running container — the way you'd run a database migration, open a database shell, or debug something without stopping the stack.

Waiting for dependencies to actually be ready

depends_on controls startup order, not readiness — Postgres's container can report "started" before it's actually accepting connections, which can cause the app to fail on its first connection attempt. For anything beyond local convenience, add a healthcheck:

yaml
services:
  db:
    image: postgres:16
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U postgres"]
      interval: 2s
      timeout: 5s
      retries: 5
 
  app:
    depends_on:
      db:
        condition: service_healthy

This makes app actually wait until Postgres is verified ready to accept connections, not just "the container process started."

Why this beats installing everything natively

Every teammate runs the exact same Postgres version, the exact same Redis version, with zero risk of "works on my machine" caused by a locally installed version drifting from what production actually runs. Onboarding a new developer becomes git clone + docker compose up, instead of a setup document that's perpetually out of date.

Scaling a service locally

docker compose up --scale runs multiple instances of a single service, useful for testing behavior under multiple concurrent instances (load balancing, distributed locking, race conditions) without deploying to a real cluster:

bash
docker compose up --scale app=3

This only works cleanly for services that don't publish a fixed host port directly (three instances can't all bind host port 3000) — either omit the ports mapping for the scaled service and access it through another service in the same network, or put a lightweight reverse proxy (like nginx) in front of it within the Compose network to load-balance across the scaled instances.

Named networks for multi-project isolation

By default, Compose creates a network scoped to the project (the directory name, or COMPOSE_PROJECT_NAME), so two unrelated projects each running docker compose up don't interfere with each other even if they both define a service called db. For genuinely shared infrastructure across multiple Compose projects (a shared local Postgres instance multiple app repos connect to), an explicit external network is the right pattern instead of relying on each project's isolated default network:

yaml
networks:
  shared-db:
    external: true
bash
docker network create shared-db

Layering multiple Compose files with -f

Beyond the automatic docker-compose.override.yml merge, explicit -f flags let you compose several files deliberately — a common pattern for a base file plus environment-specific additions (local, CI, staging-like):

bash
docker compose -f docker-compose.yml -f docker-compose.ci.yml up

Later files override or extend earlier ones field by field, not wholesale — a docker-compose.ci.yml might only override environment variables for one service while everything else still comes from the base file, letting CI-specific tweaks stay minimal and explicit rather than duplicating the entire stack definition.

Once a service defined in Compose is ready to actually ship, the same image benefits from multi-stage Docker builds to keep it lean, and the infrastructure it deploys onto is the kind of thing worth managing with Infrastructure as Code rather than clicking through a console by hand.

Common mistakes

Common mistakes
  • Mounting the entire project directory without excluding node_modules (or the language equivalent), then debugging confusing native-module errors that are actually a host/container platform mismatch, not a real bug.
  • Relying on depends_on alone for startup ordering and assuming it means "ready," not just "container process started" — leads to intermittent first-connection failures that look flaky but are actually a real, fixable race condition.
  • Committing real secrets (database passwords, API keys) directly into docker-compose.yml instead of an untracked .env file — fine for genuinely disposable local-only credentials, a real risk if that same file is ever reused as a template for a shared or production config.
  • Never pruning unused volumes and images. Local Docker installs accumulate stopped containers, dangling images, and orphaned volumes over time — docker system prune (or docker volume prune specifically) periodically prevents this from quietly consuming significant disk space.

Check your understanding

Docker Basics Quiz

Q1Multiple choice

Which command builds a Docker image from a Dockerfile in the current directory?

Q2True / False

True or false: containers created from the same image share the exact same writable filesystem layer.

Q3Code completion

Fill in the missing flag: this command should map the host's port 8080 to the container's port 80.

docker run ___ 8080:80 nginx
Q4Scenario

Your container exits immediately after `docker run` with no error output. What's the most likely first thing to check?

Advertisement

Frequently Asked Questions

Advertisement
DevFieldGuide
DevFieldGuide

Editorial Team

Practical tutorials and developer tools, written and maintained by the DevFieldGuide team.

Enjoyed this article?

Get the next one straight to your inbox, along with the best of what we publish each week.

Related Articles

More in Docker

View all