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.

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 nextjsdirective prevents privilege escalation attacks inside the host kernel. - Leverage .dockerignore: Always ignore
node_modules,.git, and local.envfiles!
Start containerizing your academic and client projects today to ensure clean and repeatable deployments.