Skip to main content
ARCHITECTVI

Software Engineer

Available for work

Open to opportunities
๐ŸณDevOps

Docker and Containerization

Getting started with Docker for development and deployment of applications.

Feb 15, 2024ยท11 min read
DockerDevOpsContainerization

Docker eliminated the "works on my machine" problem at the cost of a learning curve. Once internalised, containerisation becomes the most reliable way to build reproducible, deployment-ready applications. This guide covers practical Docker usage for Node.js/NestJS applications โ€” the same setup I use across all my production services.

The Mental Model: Image vs Container

An image is a read-only blueprint (like a class). A container is a running instance of that blueprint (like an object). You build images once, then run as many containers as you need. Images are stored in registries (Docker Hub, ECR, GCR).

Multi-Stage Builds: Keep Production Images Lean

A common mistake is shipping development dependencies into production. Multi-stage builds solve this by using one stage to compile/build and a separate minimal stage to run.

dockerfile
# Stage 1 โ€” build
FROM node:20-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build

# Stage 2 โ€” production (only runtime deps)
FROM node:20-alpine AS runner
WORKDIR /app
ENV NODE_ENV=production

COPY package*.json ./
RUN npm ci --omit=dev && npm cache clean --force

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

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

EXPOSE 3000
CMD ["node", "dist/main.js"]
๐Ÿ’กThe multi-stage build above reduces the final image from ~800 MB to ~120 MB. Smaller images pull faster, reduce attack surface, and cost less in registry storage.

.dockerignore Is As Important As .gitignore

Without a .dockerignore, the COPY . . instruction sends your entire working directory to the Docker daemon, including node_modules, .git, and local env files. Always create one.

text
node_modules
dist
.git
.env
.env.local
*.log
coverage
.next
README.md

Docker Compose for Local Development

Docker Compose orchestrates multi-container environments locally. Use it to run your app alongside its dependencies (PostgreSQL, Redis, etc.) with a single command.

yaml
# compose.yml
services:
  api:
    build:
      context: .
      target: builder        # use the build stage for hot-reload
    volumes:
      - .:/app
      - /app/node_modules    # anonymous volume โ€” prevent host override
    ports:
      - "3000:3000"
    environment:
      DATABASE_URL: postgres://dev:dev@db:5432/dev
      REDIS_URL: redis://cache:6379
    depends_on:
      db:
        condition: service_healthy
      cache:
        condition: service_started

  db:
    image: postgres:16-alpine
    environment:
      POSTGRES_USER: dev
      POSTGRES_PASSWORD: dev
      POSTGRES_DB: dev
    volumes:
      - pg_data:/var/lib/postgresql/data
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U dev"]
      interval: 5s
      timeout: 5s
      retries: 5

  cache:
    image: redis:7-alpine

volumes:
  pg_data:

Health Checks and Graceful Shutdown

Production containers need health checks so the orchestrator (Kubernetes, ECS) knows when a container is ready for traffic and when to restart it.

typescript
// NestJS health check endpoint
@Controller('health')
export class HealthController {
  constructor(
    private health: HealthCheckService,
    private db: TypeOrmHealthIndicator,
    private redis: MicroserviceHealthIndicator,
  ) {}

  @Get()
  @HealthCheck()
  check() {
    return this.health.check([
      () => this.db.pingCheck('database'),
      () => this.redis.pingCheck('redis', { transport: Transport.REDIS }),
    ]);
  }
}

// Graceful shutdown โ€” drain in-flight requests before exit
async function bootstrap() {
  const app = await NestFactory.create(AppModule);
  app.enableShutdownHooks(); // handles SIGTERM from Docker/K8s
  await app.listen(3000);
}

Layer Caching: Order Your Dockerfile Correctly

Docker caches each layer. A cache miss on one layer invalidates all subsequent layers. Put the most frequently changing instructions last.

  1. FROM (base image โ€” changes rarely)
  2. System dependencies with apt/apk (changes rarely)
  3. COPY package.json package-lock.json (changes when deps change)
  4. RUN npm ci (re-runs only when lock file changes)
  5. COPY . . (invalidated by any source file change)
  6. RUN npm run build

Written by

Md. Saniuzzaman Robin

Full-Stack Software Engineer

More Articles โ†’