docker

Docker Compose for Node.js Application

Docker Compose config for a Node.js app with hot-reload, debugging, and development-friendly defaults.

Overview

A Docker Compose setup optimized for Node.js application development. Features hot-reloading with volume mounts, Node.js debugging support, environment variable management, and proper signal handling for graceful shutdowns.

Configuration

# docker-compose.yml

services:
  app:
    build:
      context: .                       # Build from current directory
      dockerfile: Dockerfile           # Path to Dockerfile
      target: development              # Use dev stage of multi-stage build
      args:
        NODE_VERSION: 20               # Pass build arguments

    container_name: node-app
    restart: unless-stopped

    ports:
      - "3000:3000"                    # Application port
      - "9229:9229"                    # Node.js debugger port

    environment:
      NODE_ENV: development
      PORT: 3000
      LOG_LEVEL: debug

    env_file:
      - .env                           # Load additional env vars from file

    volumes:
      - .:/app                         # Mount source code for hot-reload
      - /app/node_modules              # Anonymous volume — keep container's node_modules
      - app_logs:/app/logs             # Persist log files

    # Use nodemon or tsx for auto-restart on file changes
    command: npx nodemon --inspect=0.0.0.0:9229 src/index.ts

    # Proper signal handling for graceful shutdown
    init: true                         # Use tini as PID 1
    stop_grace_period: 10s             # Wait 10s before SIGKILL

    healthcheck:
      test: ["CMD", "node", "-e", "fetch('http://localhost:3000/health').then(r => { if (!r.ok) throw new Error() })"]
      interval: 15s
      timeout: 5s
      retries: 3
      start_period: 20s

    # Resource constraints
    deploy:
      resources:
        limits:
          memory: 1G
        reservations:
          memory: 256M

  # Run tests in a separate service
  test:
    build:
      context: .
      dockerfile: Dockerfile
      target: development
    profiles:
      - test                           # Only runs with --profile test

    environment:
      NODE_ENV: test
      CI: "true"

    volumes:
      - .:/app
      - /app/node_modules

    command: npm test

volumes:
  app_logs:
    driver: local
# Dockerfile (referenced above)

FROM node:20-alpine AS development

WORKDIR /app

# Install dependencies first (layer caching)
COPY package*.json ./
RUN npm ci

# Copy source code
COPY . .

# Expose ports
EXPOSE 3000 9229

# Default command (overridden by docker-compose)
CMD ["npm", "run", "dev"]

Key Options Explained

  • /app/node_modules anonymous volume — Prevents the host’s node_modules from overriding the container’s. The container keeps its own copy installed during build.
  • --inspect=0.0.0.0:9229 — Enables Node.js debugger on all interfaces so VS Code or Chrome DevTools can attach from the host.
  • init: true — Runs tini as PID 1 to properly forward signals. Without this, Node.js won’t receive SIGTERM for graceful shutdown.
  • target: development — Uses only the development stage from a multi-stage Dockerfile, skipping production optimization steps.
  • env_file — Loads environment variables from a .env file. Docker Compose reads this automatically, keeping secrets out of docker-compose.yml.

Common Modifications

  • Use watch mode: Replace the volumes mount with Docker Compose develop.watch for selective syncing and rebuild triggers.
  • Add database dependency: Include depends_on: postgres: condition: service_healthy to ensure the database starts first.
  • TypeScript compilation: Change command to npx tsx watch src/index.ts for faster TypeScript execution without a separate compilation step.
  • Production override: Create docker-compose.prod.yml that removes debug ports, volume mounts, and sets NODE_ENV=production.
  • Monorepo support: Adjust context and volumes to mount specific workspace packages.