How to Dockerize Your Node.js + TypeScript App (with NestJS): Building for Portability and Scale

posted 4 min read

Why Docker Even Matters in 2025?

In today’s development world, writing code is just step one. What matters more is how easily that code can run across environments, machines, or team members' laptops — without breaking.

The old joke “it works on my machine” is no longer funny. It's now a blocker.

Docker solves this. It lets you:

  • Run your app the same way on any machine
  • Eliminate “dependency hell”
  • Prepare your app for scale with Kubernetes and cloud-native tools

In this guide, we’ll Dockerize a NestJS (Node.js + TypeScript) app from scratch in a way that’s portable, scalable, and production-ready, with some nice developer conveniences too.

Why NestJS (Over Express.js)?
Express.js is fantastic for small or quick projects — it’s minimal, flexible, and fast to set up. But as your codebase or team grows, Express can become a burden:

  • No enforced structure
  • Middleware quickly becomes messy
  • Dependency injection is manual
  • No native scalability patterns

You’ll find yourself spending more time organizing your code than actually building.

That’s where NestJS comes in. It hass:

  • Modular architecture (Controllers, Services, Modules)
  • Built-in dependency injection for clean, testable code
  • TypeScript-first support with decorators
  • Consistent patterns that help teams work better together
  • First-party modules for auth, GraphQL, WebSockets, microservices, caching, and more

The Dev Shift: From Local to Portable
Modern dev teams value:

  • Reproducibility (code must run the same everywhere)
  • Fast onboarding (new devs should be productive in minutes, not hours)
  • Cloud readiness (apps should scale easily, even in containers or clusters)

And Docker is the foundation for all of that.

It wraps your app, dependencies, OS-level configs, and runtime into a self-contained unit called a container, so your app can run anywhere.

Your Project structure
For this guide, we’re working with a typical NestJS app with a few added files for Docker support:

my-nest-app/
├── src/
├── .env
├── Dockerfile
├── docker-compose.yml
├── Makefile
├── package.json
├── tsconfig.json

We’ll focus on the Docker-related files below.

The Dockerfile (Production-Ready)
Here’s the Dockerfile used to build both the dev and prod image:

# Use Node.js 18 Alpine as base image
FROM node:18-alpine AS builder

# Set working directory
WORKDIR /app

# Copy package files
COPY package*.json ./

# Install dependencies
RUN npm ci --only=production && npm cache clean --force

# Copy source code
COPY . .

# Build the application
RUN npm run build

# Production stage
FROM node:18-alpine AS production

# Install postgresql-client for pg_isready
RUN apk add --no-cache postgresql-client

# Create app directory
WORKDIR /app

# Create non-root user
RUN addgroup -g 1001 -S nodejs
RUN adduser -S nestjs -u 1001

# Copy package files
COPY package*.json ./

# Install only production dependencies
RUN npm ci --only=production && npm cache clean --force

# Copy built application from builder stage
COPY --from=builder --chown=nestjs:nodejs /app/dist ./dist
COPY --from=builder --chown=nestjs:nodejs /app/typeorm.config.ts ./
COPY --from=builder --chown=nestjs:nodejs /app/healthcheck.js ./
COPY --chown=nestjs:nodejs scripts/start.sh ./scripts/

# Make start script executable
RUN chmod +x ./scripts/start.sh

# Change to non-root user
USER nestjs

# Expose port
EXPOSE 3000

# Health check
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
  CMD node healthcheck.js

# Start the application
CMD ["./scripts/start.sh", "node", "dist/main"]

This Dockerfile uses a multi-stage build to reduce final image size, includes a healthcheck, and runs your app as a non-root user for security.

The .dockerignore file:
This prevents unwanted files from bloating your image:

node_modules
npm-debug.log
dist
.git
.gitignore
README.md
.env
.nyc_output
coverage
.vscode
.idea
*.log
.DS_Store

Docker Compose for Local Development
docker-compose.yml helps you spin up your app and a Postgres DB in one command. Here’s how:

version: '3.8'

services:
  app:
    build:
      context: .
      dockerfile: Dockerfile
    ports:
      - "${PORT}:3000"
    environment:
      - NODE_ENV=${NODE_ENV}
      - DB_HOST=${DB_HOST}
      - DB_PORT=${DB_PORT}
      - DB_USERNAME=${DB_USERNAME}
      - DB_PASSWORD=${DB_PASSWORD}
      - DB_NAME=${DB_NAME}
      - JWT_SECRET=${JWT_SECRET}
    depends_on:
      postgres:
        condition: service_healthy
    networks:
      - app-network

  postgres:
    image: postgres:15-alpine
    environment:
      - POSTGRES_USER=${DB_USERNAME}
      - POSTGRES_PASSWORD=${DB_PASSWORD}
      - POSTGRES_DB=${DB_NAME}
    volumes:
      - postgres_data:/var/lib/postgresql/data
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER}"]
      interval: 10s
      timeout: 5s
      retries: 5
    networks:
      - app-network

volumes:
  postgres_data:

networks:
  app-network:
    driver: bridge

All sensitive values are loaded from .env, so you don’t hardcode secrets into the YAML.

Using Makefiles to Simplify Docker Commands
Running long docker-compose commands over and over is tiring. That’s why I use a Makefile.

A Makefile is like a command cheat sheet, it lets you define short, memorable aliases for long or repetitive shell commands.

Instead of this:

You just do:

```make docker-up```
Here’s a snippet from my Makefile:

````
docker-build:
docker-compose build

docker-up:
docker-compose up -d
@echo "Application starting... Wait a moment then visit http://localhost:3000"

docker-down:
docker-compose down

docker-logs:
docker-compose logs -f

restart: docker-down docker-up

```

This simplifies daily tasks, CI/CD workflows, and team onboarding. You can add test runners, cleanup tasks, migrations, and more.

You can check out the full working example on GitHub:
[https://github.com/Osalumense/nestjs-auth-docker][1]


With this setup, you've built a portable, secure, and scalable backend that can:

Run anywhere, locally or in the cloud

Spin up with one command

Be extended easily (Docker Compose, Makefiles, CI/CD, Kubernetes, etc.)

NestJS gives you structure.
Docker gives you portability.
Makefiles give you simplicity.
If you read this far, tweet to the author to show them you care. Tweet a Thanks

Nice one.... I appreciate the detailed steps and practical tips. Quick question: how do you handle environment-specific configs in your Docker setup for different stages like dev and production?

Hi Muzzamil,
Thanks for checking out the post.

To handle environment-specific configs (like dev, staging, and production), I use multiple Docker Compose files, for example:

docker-compose.dev.yml

docker-compose.prod.yml

Then, in my Makefile, I define commands that target each environment. For instance:

dev-build:
docker-compose -f docker-compose.dev.yml up --build

prod-build:
docker-compose -f docker-compose.prod.yml up --build

This way, I can build for dev with this command:

make dev-build

And then build for prod with this command:

make prod-build

Each Compose file can have its envs, volume mounts, ports, health checks, etc., tailored to that stage.
I hope this helps

More Posts

How to set up TypeScript with Node.js and Express

Sunny - Jul 13

How to set up TypeScript with Node.js and Express (2025)

Sunny - Jun 6

Javascript,Typescript and Node.js

Maja OLAGUNJU 1 - Aug 11

Express and TypeScript: How to Set Up Your Project

Mubaraq Yusuf - May 26

Build a Full-Stack Video Streaming App with React.js, Node.js, Next.js, MongoDB, Bunny CDN, and Material UI

torver213 - Mar 29
chevron_left