dev_tools
Back to guides

Docker + Node.js

Containerize a Node.js application with a production-ready multi-stage Dockerfile and Docker Compose for local development.

Docker
Node.js
Docker Compose
DevOps
1

Create a .dockerignore File

Exclude files that should not be copied into the image — node_modules, build output, and secrets. This dramatically reduces image size and build time.

.dockerignore
node_modules
npm-debug.log
dist
.next
.env
.env.local
.git
.gitignore
README.md
*.test.ts
coverage
2

Write a Multi-Stage Dockerfile

Multi-stage builds keep the final image lean — the builder stage installs all dev dependencies and compiles TypeScript, while the runner stage copies only the compiled output and production deps.

Dockerfile
# ── Stage 1: Install deps & build ────────────────────────────────────
FROM node:20-alpine AS builder
WORKDIR /app

COPY package*.json ./
RUN npm ci

COPY . .
RUN npm run build

# ── Stage 2: Production image ────────────────────────────────────────
FROM node:20-alpine AS runner
WORKDIR /app

ENV NODE_ENV=production

# Copy only what's needed to run
COPY package*.json ./
RUN npm ci --omit=dev

COPY --from=builder /app/dist ./dist

# Run as non-root for security
RUN addgroup -S appgroup && adduser -S appuser -G appgroup
USER appuser

EXPOSE 3000

CMD ["node", "dist/index.js"]
💡Alpine-based images are ~5x smaller than the default debian ones. Adjust the CMD to match your app's entry point.
3

Build and Run the Image

Build the Docker image and run a container locally to confirm everything works before setting up Compose.

docker build -t my-node-app . && docker run -p 3000:3000 --env-file .env my-node-app
4

Create docker-compose.yml for Local Development

Docker Compose wires up your app, a database, and any other services. Using volumes to mount source code enables hot-reload without rebuilding the image on every change.

docker-compose.yml
version: '3.9'

services:
  app:
    build:
      context: .
      target: builder        # use builder stage for dev (has ts-node-dev)
    command: npm run dev
    ports:
      - '3000:3000'
    volumes:
      - .:/app               # live-reload: changes reflect immediately
      - /app/node_modules    # prevent host node_modules from overriding container's
    env_file:
      - .env
    depends_on:
      db:
        condition: service_healthy

  db:
    image: mongo:7
    ports:
      - '27017:27017'
    volumes:
      - mongo_data:/data/db
    healthcheck:
      test: ['CMD', 'mongosh', '--eval', "db.adminCommand('ping')"]
      interval: 10s
      timeout: 5s
      retries: 5

volumes:
  mongo_data:
5

Start Services with Docker Compose

Spin up all services defined in docker-compose.yml. The -d flag runs them in detached (background) mode.

docker compose up -d
💡Use `docker compose logs -f app` to tail logs, and `docker compose down` to stop all services.
6

Create a Production docker-compose.prod.yml

Override the dev compose file for production deployments. This targets the lean runner stage and removes volume mounts.

docker-compose.prod.yml
version: '3.9'

services:
  app:
    build:
      context: .
      target: runner          # lean production image
    command: node dist/index.js
    restart: unless-stopped
    environment:
      NODE_ENV: production
      PORT: 3000
      MONGODB_URI: mongodb://db:27017/myapp
    ports:
      - '3000:3000'
    depends_on:
      db:
        condition: service_healthy

  db:
    image: mongo:7
    restart: unless-stopped
    volumes:
      - mongo_data:/data/db
    healthcheck:
      test: ['CMD', 'mongosh', '--eval', "db.adminCommand('ping')"]
      interval: 10s
      timeout: 5s
      retries: 5

volumes:
  mongo_data:
7

Add Useful npm Scripts

Add convenience scripts to package.json so the team doesn't have to remember long Docker commands.

package.json (scripts section)
"scripts": {
  "dev": "ts-node-dev --respawn --transpile-only src/index.ts",
  "build": "tsc",
  "start": "node dist/index.js",
  "docker:dev": "docker compose up -d",
  "docker:down": "docker compose down",
  "docker:logs": "docker compose logs -f app",
  "docker:prod": "docker compose -f docker-compose.prod.yml up -d --build"
}
8

Verify the Running Container

Check that your container is up, inspect its logs, and exec into it for debugging if needed.

docker compose ps && curl http://localhost:3000
💡Use `docker compose exec app sh` to open a shell inside the running container for debugging.

Found this guide helpful? Check out more tools and guides.

Browse All Guides