Back to CourseLesson 5 of 10

Docker Containerization

Containers give you reproducible builds and consistent environments from development through production. This lesson covers Dockerfile best practices for Next.js apps, multi-stage builds to minimize image size, docker-compose for local development, and container security essentials.

Dockerfile Best Practices

A production Dockerfile for a CoFounder Next.js application should use multi-stage builds, pin base image versions, leverage layer caching for dependencies, and run as a non-root user. Here is a battle-tested pattern:

# Dockerfile
FROM node:20-alpine AS base
RUN apk add --no-cache libc6-compat

# Stage 1: Install dependencies
FROM base AS deps
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci --only=production && cp -R node_modules /prod_modules
RUN npm ci

# Stage 2: Build the application
FROM base AS builder
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY . .
ENV NEXT_TELEMETRY_DISABLED=1
RUN npm run build

# Stage 3: Production image
FROM base AS runner
WORKDIR /app
ENV NODE_ENV=production
ENV NEXT_TELEMETRY_DISABLED=1

# Run as non-root
RUN addgroup --system --gid 1001 nodejs
RUN adduser --system --uid 1001 nextjs

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
ENV PORT=3000
ENV HOSTNAME="0.0.0.0"

HEALTHCHECK --interval=30s --timeout=3s --retries=3 \
  CMD wget -qO- http://localhost:3000/api/health || exit 1

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

Next.js Standalone Output

Enable standalone output in your Next.js config to produce a self-contained build that includes only the files needed for production. This dramatically reduces your Docker image size:

// next.config.js
/** @type {import('next').NextConfig} */
const nextConfig = {
  output: 'standalone',
  experimental: {
    instrumentationHook: true, // For OpenTelemetry
  },
};

module.exports = nextConfig;

Docker Compose for Local Development

CoFounder projects typically depend on Supabase (PostgreSQL), Redis, and sometimes additional services. Use docker-compose to spin up the full stack locally with a single command:

# docker-compose.yml
version: '3.8'

services:
  app:
    build:
      context: .
      dockerfile: Dockerfile
      target: deps  # Use deps stage for dev with hot reload
    ports:
      - "3000:3000"
    environment:
      - NODE_ENV=development
      - NEXT_PUBLIC_SUPABASE_URL=http://supabase-kong:8000
      - REDIS_URL=redis://redis:6379
    volumes:
      - .:/app
      - /app/node_modules
    depends_on:
      - redis
      - supabase-db
    command: npm run dev

  redis:
    image: redis:7-alpine
    ports:
      - "6379:6379"
    volumes:
      - redis_data:/data
    healthcheck:
      test: ["CMD", "redis-cli", "ping"]
      interval: 10s
      timeout: 5s
      retries: 3

  supabase-db:
    image: supabase/postgres:15.1.1.41
    ports:
      - "5432:5432"
    environment:
      POSTGRES_PASSWORD: postgres
      POSTGRES_DB: cofounder_dev
    volumes:
      - db_data:/var/lib/postgresql/data
      - ./supabase/migrations:/docker-entrypoint-initdb.d
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U postgres"]
      interval: 10s
      timeout: 5s
      retries: 5

volumes:
  redis_data:
  db_data:

Container Security

Containers are not inherently secure. Follow these practices to harden your production images:

  • Non-root user: Always run the application process as a non-root user (as shown in the Dockerfile above).
  • Minimal base image: Use Alpine or distroless images to reduce the attack surface.
  • No secrets in images: Pass secrets via environment variables at runtime, never bake them into the image.
  • Scan for vulnerabilities: Run docker scout cves or trivy image in your CI pipeline.
  • Read-only filesystem: Mount the container filesystem as read-only where possible.
# CI pipeline: scan image for vulnerabilities
docker build -t cofounder-app:latest .
docker scout cves cofounder-app:latest --only-severity critical,high

# Run with read-only filesystem and security options
docker run \
  --read-only \
  --tmpfs /tmp \
  --security-opt no-new-privileges \
  --cap-drop ALL \
  -e OPENAI_API_KEY="$OPENAI_API_KEY" \
  -p 3000:3000 \
  cofounder-app:latest