Dockerize Node.js API
|

Dockerizing a Node.js Express API (Step-by-Step)

If you’re building a Node.js application and thinking about deployment, Docker should be your starting point.

Whether you deploy to AWS ECS, a virtual machine, or even Lambda, Docker gives you one critical benefit: consistent, portable environments across development, testing, and production.

In this guide, we’ll walk through a practical, production‑ready workflow to:

  • Dockerize a Node.js Express API
  • Use multi‑stage Docker builds
  • Test everything locally with Docker Compose

🚀 Complete JavaScript Guide (Beginner + Advanced)

🚀 NodeJS – The Complete Guide (MVC, REST APIs, GraphQL, Deno)


Why Docker?

Docker allows you to package your application together with its runtime, dependencies, and configuration into a single container image.

Unlike virtual machines, containers share the host OS, which makes them:

  • Faster to start
  • More lightweight
  • Easier to scale

Most importantly, Docker eliminates the classic “it works on my machine” problem.


Step 1: Create a .dockerignore File

Before writing a Dockerfile, start with .dockerignore.

It works just like .gitignore and prevents unnecessary files from being copied into your image.

Create a .dockerignore file in the project root:

node_modules
dist
.env
.env.example
.vscode
.git
.github
.husky

Even though your app needs node_modules and dist to run, we’ll install dependencies and build the app inside Docker instead.

This keeps images smaller and builds faster.


Step 2: Create a Multi‑Stage Dockerfile

We’ll use Node 24 Alpine (LTS at the time of writing) and a multi‑stage build.

Multi‑stage builds let you:

  • Build the app in one stage
  • Ship only what’s needed in the final image
  • Reduce image size and attack surface

Dockerfile

# ---------- Builder Stage ----------
FROM node:24-alpine AS builder

RUN apk add --no-cache libc6-compat

WORKDIR /app

COPY . .

RUN npm install
RUN npm run build

# ---------- Installer Stage ----------
FROM node:22-alpine AS installer

RUN apk add --no-cache libc6-compat openssl

WORKDIR /app

COPY --from=builder /app/dist ./dist
COPY --from=builder /app/prisma ./prisma
COPY --from=builder /app/package.json ./package.json
COPY --from=builder /app/package-lock.json ./package-lock.json

RUN npm ci --omit=dev
RUN npm run prisma:generate

# ---------- Runner Stage ----------
FROM node:22-alpine AS runner

RUN apk add --no-cache libc6-compat openssl

WORKDIR /app

# Run as non-root user
RUN addgroup -S expressjs && adduser -S expressjs -G expressjs
USER expressjs

COPY --from=installer /app .

CMD ["npm", "run", "docker-start"]

Why This Matters

  • npm ci --omit=dev installs only production dependencies
  • Dev dependencies are used only during build
  • App runs as a non‑root user for better security

Step 3: Add a docker-start Script

Before deploying, your app needs to:

  1. Run database migrations
  2. Start the server

Add this script to your package.json:

{
  "scripts": {
    "docker-start": "npm run migrate && npm run start"
  }
}

Step 4: Test Locally with Docker Compose

Before deploying anywhere, test locally.

Docker Compose lets you run your API and database together in a production‑like environment.

Create docker-compose.yml in the project root.

services:
  app:
    build:
      context: .
      dockerfile: Dockerfile
    environment:
      NODE_ENV: production
      DATABASE_URL: mysql://root:secret@db:3306/task_manager
    ports:
      - "3000:3000"
    depends_on:
      - db

  db:
    image: mysql:8.0
    environment:
      MYSQL_ROOT_PASSWORD: secret
      MYSQL_DATABASE: task_manager
    ports:
      - "33061:3306"
    volumes:
      - db-data:/var/lib/mysql

volumes:
  db-data:

Important Gotcha 🚨

Inside Docker Compose, the database host is db, not localhost.


Step 5: Run Docker Compose

Start everything with:

docker-compose up -d

The first run may fail with:

P1001: Can't reach database server at db:3306

This happens because MySQL needs time to initialize.

Fix it by restarting once:

docker-compose down
docker-compose up -d

Logging Inside Containers

Avoid writing logs to files inside containers ❌

  • Hard to access
  • Bloats images
  • Not scalable

Instead:

  • Log to stdout
  • Use a logging service (CloudWatch, Datadog, etc.)

For local testing, comment out file‑based log transports.

Rebuild after changes:

docker-compose up -d --build

Verify Everything Works

With containers running, test the app:

curl http://localhost:3000/health

If you get a response, your app:

  • Connected to the database
  • Ran migrations
  • Started successfully 🎉

Cleanup When You’re Done

If you no longer need containers, images, or volumes:

docker-compose down --rmi all --volumes

This will:

  • Stop and remove containers
  • Remove images
  • Remove volumes

Final Thoughts

This workflow lets you:

  • Dockerize once
  • Test locally with confidence
  • Deploy the same image anywhere

ECS, virtual machines, or Lambda—all with zero surprises.

If you want to take the next step and deploy this image to AWS ECS, that’s the perfect follow‑up.

Happy shipping 🚀

Share this article

Similar Posts