DPI Computing SocietyDPI Computing SocietyLearn, build, and grow together
Workshop

Hands-on Guide: Dockerizing Full-Stack Applications for Production

Step-by-step instructions on multi-stage builds, non-root user security, layer caching, and docker-compose configurations for modern web stacks.

TTahmid Hasan
1 min read
Hands-on Guide: Dockerizing Full-Stack Applications for Production

Why Containerization Matters

Deploying applications directly onto raw virtual machines often leads to the infamous "it works on my machine" syndrome. With Docker, we package the runtime, system libraries, configuration, and code into an immutable image that runs identically on local macOS, Ubuntu staging, or AWS ECS production.

In this workshop tutorial, we will write a production-ready, security-hardened multi-stage Dockerfile for a Next.js and Node.js application.


1. Multi-Stage Dockerfile Blueprint

Multi-stage builds allow us to use heavy compilation dependencies (Node headers, build tools, typescript) in a build stage, and copy only the optimized production bundle into the final execution image.

# -------------------------------------------------------------
# Stage 1: Dependency resolution & caching
# -------------------------------------------------------------
FROM node:22-alpine AS deps
RUN apk add --no-cache libc6-compat
WORKDIR /app

COPY package.json pnpm-lock.yaml ./
RUN corepack enable pnpm && pnpm install --frozen-lockfile

# -------------------------------------------------------------
# Stage 2: Builder
# -------------------------------------------------------------
FROM node:22-alpine AS builder
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY . .

# Generate Prisma Client & Build Next.js
ENV NEXT_TELEMETRY_DISABLED=1
ENV NODE_ENV=production
RUN npx prisma generate
RUN corepack enable pnpm && pnpm run build

# -------------------------------------------------------------
# Stage 3: Minimal Production Runner
# -------------------------------------------------------------
FROM node:22-alpine AS runner
WORKDIR /app

ENV NODE_ENV=production
ENV PORT=3000
ENV HOSTNAME="0.0.0.0"

# Security: Never run containers as root!
RUN addgroup --system --gid 1001 nodejs
RUN adduser --system --uid 1001 nextjs

# Copy standalone output
COPY --from=builder /app/public ./public
COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./
COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static

USER nextjs
EXPOSE 3000

CMD ["node", "server.js"]

2. Docker Compose for Local Development

To orchestrate your PostgreSQL database, Redis cache, and application simultaneously, use this docker-compose.yml:

version: "3.9"

services:
  app:
    build:
      context: .
      dockerfile: Dockerfile
    ports:
      - "3000:3000"
    environment:
      - DATABASE_URL=postgresql://dpics_user:secret@postgres:5432/dpics_db
    depends_on:
      postgres:
        condition: service_healthy

  postgres:
    image: postgres:16-alpine
    restart: always
    environment:
      POSTGRES_USER: dpics_user
      POSTGRES_PASSWORD: secret
      POSTGRES_DB: dpics_db
    volumes:
      - pgdata:/var/lib/postgresql/data
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U dpics_user -d dpics_db"]
      interval: 5s
      timeout: 5s
      retries: 5

volumes:
  pgdata:

3. Best Practices Checklist

  • Use Alpine or Distroless base images to reduce image attack surfaces and bandwidth (from 1GB down to ~80MB).
  • Enforce Non-Root Users: The USER nextjs directive prevents privilege escalation attacks inside the host kernel.
  • Leverage .dockerignore: Always ignore node_modules, .git, and local .env files!

Start containerizing your academic and client projects today to ensure clean and repeatable deployments.