Skip to main content

Command Palette

Search for a command to run...

Docker Compose for Local Development: Complete Guide

Published
8 min readView as Markdown

const relatedArticles = [ slug: 'docker-for-developers', title: 'Docker for Developers', description: 'Containers, images, and Dockerfile fundamentals.' , slug: 'docker-container-best-practices', title: 'Docker Container Best Practices', description: 'Dockerfile optimization, security, and multi-stage builds.' , slug: 'environment-variables-guide', title: 'Environment Variables Guide', description: 'Manage env vars across dev, staging, and production.' , slug: 'ci-cd-pipeline-guide', title: 'CI/CD Pipeline Guide', description: 'Integrate Docker Compose into your CI/CD workflow.' , ]

return (

/ Schema.org JSON-LD /

/ Breadcrumb Navigation /

Home / Blog / Docker Compose for Local Dev

/ Article Header /

# Docker Compose for Local Development: Complete Guide

Docker March 9, 2026 15 min read

/ Article Content /

Why Docker Compose for Local Development?

Modern applications rarely run in isolation. A typical web app might need a Node.js server, a PostgreSQL database, a Redis cache, and perhaps an Elasticsearch instance. Docker Compose lets you define all of these services in a single YAML file and start them with one command: docker compose up.

The key benefits for local development are reproducibility (every developer gets the exact same environment), isolation (no conflicting versions of PostgreSQL or Node.js installed globally), and simplicity (new team members run one command instead of following a 20-step setup guide).

As of 2026, Docker Compose V2 is the standard. It is integrated into the Docker CLI as docker compose (no hyphen). The older docker-compose (with hyphen) is deprecated. All examples in this guide use the V2 syntax.

Your First docker-compose.yml

A Compose file defines services, networks, and volumes. Here is a minimal example for a Node.js app with PostgreSQL and Redis.

`# docker-compose.yml services: app: build: context: . dockerfile: Dockerfile.dev ports:

  • "3000:3000" volumes:
  • .:/app
  • /app/node_modules # Prevent overwriting container's node_modules environment:
  • DATABASE_URL=postgres://postgres:secret@db:5432/myapp
  • REDIS_URL=redis://cache:6379 depends_on: db: condition: service_healthy cache: condition: service_started

    db: image: postgres:16-alpine ports:

  • "5432:5432" environment: POSTGRES_DB: myapp POSTGRES_USER: postgres POSTGRES_PASSWORD: secret volumes:
  • pgdata:/var/lib/postgresql/data
  • ./init.sql:/docker-entrypoint-initdb.d/init.sql healthcheck: test: ["CMD-SHELL", "pg_isready -U postgres"] interval: 5s timeout: 5s retries: 5

    cache: image: redis:7-alpine ports:

  • "6379:6379"

volumes: pgdata:`

You can validate the YAML syntax of your Compose file by converting it to JSON with our JSON Formatter (paste the YAML output of docker compose config). For a deeper comparison of YAML and JSON formats, see our YAML vs JSON guide.

Development-Optimized Dockerfile

Your development Dockerfile should prioritize fast rebuilds and hot reloading, not production optimization. Use a separate Dockerfile.dev for development.

`# Dockerfile.dev FROM node:22-alpine

WORKDIR /app

Install dependencies first (cached layer)

COPY package.json package-lock.json ./ RUN npm ci

Copy source code (changes frequently, so this layer is rebuilt often)

COPY . .

Use nodemon or tsx for hot reloading

CMD ["npx", "tsx", "watch", "src/index.ts"]`

The key optimization is separating package.json installation from source code copying. Docker caches layers, so if your dependencies have not changed, npm ci will be skipped on subsequent builds. For production Dockerfiles with multi-stage builds, see our Docker Container Best Practices guide.

Volumes and Hot Reloading

The most important feature for local development is hot reloading. You want your code changes to be reflected inside the container instantly, without rebuilding the image. Bind mounts make this possible.

`services: app: volumes:

Bind mount: host directory → container directory

  • .:/app

    Anonymous volume: preserve container's node_modules

    (prevents host node_modules from overwriting container's)

  • /app/node_modules

    Named volume for build cache

  • build_cache:/app/.next

    For frontend with Vite:

    frontend: volumes:

  • ./frontend:/app
  • /app/node_modules environment:

    Vite needs this for HMR to work through Docker

  • CHOKIDAR_USEPOLLING=true
  • WATCHPACK_POLLING=true

volumes: build_cache:`

On macOS and Windows, bind mount performance can be slow because Docker Desktop uses a virtual machine. Docker Desktop 4.x+ includes VirtioFS, which dramatically improves file system performance. Ensure it is enabled in Docker Desktop settings under "General" and "Use VirtioFS."

Docker Compose 2.22+ introduced docker compose watch, which is now the recommended approach for development. It syncs files, rebuilds on dependency changes, and restarts services automatically.

`services: app: build: . develop: watch:

Sync source code changes without rebuild

  • action: sync path: ./src target: /app/src

    Rebuild when dependencies change

  • action: rebuild path: package.json

    Restart on config changes

  • action: sync+restart path: ./config target: /app/config

Run with:

docker compose watch`

Networking Between Services

Docker Compose creates a default network for all services in the Compose file. Services can reach each other by their service name as the hostname. For example, the app service can connect to PostgreSQL at db:5432 and Redis at cache:6379.

`services: frontend: build: ./frontend ports:

  • "3000:3000" networks:
  • frontend-net

    api: build: ./api ports:

  • "4000:4000" networks:
  • frontend-net
  • backend-net

    db: image: postgres:16-alpine networks:

  • backend-net

    No ports exposed to host — only accessible from backend-net

networks: frontend-net: backend-net:

frontend can reach api, but NOT db

api can reach both frontend and db

db can only be reached from backend-net`

Custom networks let you isolate services. In this example, the frontend cannot directly access the database, which mirrors a production architecture. For DNS-related configuration, check our DNS Records Explained guide.

Environment Variables and Secrets

Docker Compose supports multiple ways to pass environment variables to your services. For local development, .env files are the most convenient approach.

`# .env file (automatically loaded by Docker Compose) POSTGRES_DB=myapp POSTGRES_USER=postgres POSTGRES_PASSWORD=secret NODE_ENV=development API_PORT=4000

docker-compose.yml

services: api: build: . env_file:

  • .env # Default env vars
  • .env.local # Local overrides (gitignored) environment:

    Inline vars override env_file

  • DEBUG=true

    db: image: postgres:16-alpine environment: POSTGRES_DB: $POSTGRES_DB POSTGRES_USER: $POSTGRES_USER POSTGRES_PASSWORD: $POSTGRES_PASSWORD`

Always add .env.local to your .gitignore. Commit a .env.example with placeholder values so new developers know which variables to set. For a deeper dive, read our Environment Variables Guide.

Database Seeding and Migrations

Starting with an empty database is rarely useful for development. Docker Compose makes it easy to seed initial data and run migrations automatically.

`services: db: image: postgres:16-alpine volumes:

  • pgdata:/var/lib/postgresql/data

    Files in this directory run on first container creation

  • ./db/init.sql:/docker-entrypoint-initdb.d/01-init.sql
  • ./db/seed.sql:/docker-entrypoint-initdb.d/02-seed.sql

    Run migrations as a one-off service

    migrate: build: . command: npx prisma migrate deploy depends_on: db: condition: service_healthy profiles:

  • setup # Only runs when explicitly requested

Run setup:

docker compose --profile setup up migrate

Reset database (destroy and recreate):

docker compose down -v # -v removes named volumes

docker compose up`

The profiles feature is useful for services that should not run by default. Migration services, test runners, and one-off scripts can be placed in profiles and invoked only when needed.

Debugging Inside Containers

Debugging containerized applications requires exposing debug ports and configuring your IDE to attach to the container's debugger.

`services: app: build: context: . dockerfile: Dockerfile.dev ports:

  • "3000:3000"
  • "9229:9229" # Node.js debug port command: node --inspect=0.0.0.0:9229 src/index.js

    Or for tsx:

    command: npx tsx --inspect=0.0.0.0:9229 watch src/index.ts

VS Code launch.json for attaching to container:

##

"type": "node",

"request": "attach",

"name": "Docker: Attach",

"port": 9229,

"address": "localhost",

"localRoot": "$workspaceFolder",

"remoteRoot": "/app",

"restart": true

##

Shell into a running container for manual debugging:

docker compose exec app sh

docker compose exec db psql -U postgres -d myapp`

The docker compose exec command is invaluable for troubleshooting. You can open a shell, run database queries, check logs, or test network connectivity from inside any container.

Essential Docker Compose Commands

Command Purpose

docker compose up -dStart all services in detached mode docker compose downStop and remove containers (keeps volumes) docker compose down -vStop, remove containers AND volumes (full reset) docker compose logs -f appFollow logs for a specific service docker compose exec app shOpen a shell inside a running container docker compose build --no-cacheRebuild images without cache docker compose psList running services and their ports docker compose watchStart with file sync and auto-rebuild

Common Service Recipes

Here are ready-to-use Compose configurations for popular development dependencies.

MongoDB

` mongo: image: mongo:7 ports:

  • "27017:27017" environment: MONGO_INITDB_ROOT_USERNAME: root MONGO_INITDB_ROOT_PASSWORD: secret volumes:
  • mongodata:/data/db`

Elasticsearch

` elasticsearch: image: elasticsearch:8.15.0 environment:

  • discovery.type=single-node
  • xpack.security.enabled=false
  • "ES_JAVA_OPTS=-Xms512m -Xmx512m" ports:
  • "9200:9200" volumes:
  • esdata:/usr/share/elasticsearch/data`

Mailpit (Email Testing)

` mailpit: image: axllent/mailpit ports:

  • "8025:8025" # Web UI
  • "1025:1025" # SMTP

    Point your app's SMTP to mailpit:1025

    View caught emails at http://localhost:8025`

Best Practices Summary

  • Use separate Dockerfiles -- Dockerfile.dev for development, Dockerfile for production.
  • Use healthchecks on dependencies -- Ensure your app does not start before the database is ready.
  • Pin image versions -- Use postgres:16-alpine not postgres:latest for reproducibility.
  • Use named volumes for data persistence -- Anonymous volumes are lost when containers are removed.
  • Use .dockerignore -- Exclude node_modules, .git, and build artifacts from the Docker build context.
  • Commit docker-compose.yml -- This file is documentation for your development environment and should be version-controlled.
  • Use Docker Compose Watch -- Prefer docker compose watch over bind mounts for better cross-platform performance.
  • Document the setup -- Include a "Getting Started" section in your project README with the exact commands to run.

/ CTA Section /

Validate Your Configuration Files

Docker Compose YAML files are prone to indentation errors. Use our JSON Formatter to validate configuration, or check your environment variable syntax with our developer tools.

Open JSON Formatter

/ Related Articles /

relatedArticles.map((a) => (

a.title

a.description

))

)


Originally published on bytepane.com. Visit for free interactive calculators and tools.

More from this blog

R

Brazora

91 posts