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.