Rosa's Node.js API Docker image is 1.2GB. Deploys take 8 minutes because pushing and pulling that image is slow. The image includes build tools, dev dependencies, TypeScript compiler, and test files — none of which are needed at runtime. Multi-stage builds let her use a full Node image for building and copy only the production artifacts into a tiny runtime image.
Step 1: Before — The Bloated Dockerfile
# ❌ BAD: 1.2GB image with everything included
FROM node:20
WORKDIR /app
COPY . .
RUN npm install
RUN npm run build
CMD ["node", "dist/index.js"]
# Includes: devDependencies, .git, tests, source code, build tools
Step 2: After — Multi-Stage Build
# Dockerfile — Optimized multi-stage build
# Stage 1: Install dependencies
FROM node:20-alpine AS deps
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci --ignore-scripts
# Stage 2: Build
FROM node:20-alpine AS builder
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY . .
RUN npm run build
# Prune to production deps only
RUN npm ci --omit=dev --ignore-scripts && npm cache clean --force
# Stage 3: Production runtime
FROM node:20-alpine AS runner
WORKDIR /app
# Don't run as root
RUN addgroup --system --gid 1001 app && \
adduser --system --uid 1001 app
# Copy only what's needed to run
COPY --from=builder --chown=app:app /app/dist ./dist
COPY --from=builder --chown=app:app /app/node_modules ./node_modules
COPY --from=builder --chown=app:app /app/package.json ./
USER app
EXPOSE 3000
# Health check
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s \
CMD wget -qO- http://localhost:3000/health || exit 1
CMD ["node", "dist/index.js"]
# Result:
# Before: 1.2GB
# After: ~150MB (88% smaller)
Step 3: Even Smaller with Distroless
# For apps that don't need a shell (no npm, no bash)
FROM gcr.io/distroless/nodejs20-debian12 AS runner
WORKDIR /app
COPY --from=builder /app/dist ./dist
COPY --from=builder /app/node_modules ./node_modules
COPY --from=builder /app/package.json ./
USER nonroot
EXPOSE 3000
CMD ["dist/index.js"]
# ~90MB — no shell, no package manager, minimal attack surface
Step 4: Next.js Standalone Build
# Dockerfile for Next.js — uses standalone output mode
FROM node:20-alpine AS deps
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci
FROM node:20-alpine AS builder
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY . .
ENV NEXT_TELEMETRY_DISABLED=1
RUN npm run build
FROM node:20-alpine AS runner
WORKDIR /app
ENV NODE_ENV=production
ENV NEXT_TELEMETRY_DISABLED=1
RUN addgroup --system --gid 1001 app && adduser --system --uid 1001 app
# Next.js standalone mode copies only required files
COPY --from=builder /app/public ./public
COPY --from=builder --chown=app:app /app/.next/standalone ./
COPY --from=builder --chown=app:app /app/.next/static ./.next/static
USER app
EXPOSE 3000
ENV PORT=3000
ENV HOSTNAME="0.0.0.0"
CMD ["node", "server.js"]
# ~120MB instead of 800MB+
// next.config.js — Enable standalone output
module.exports = {
output: "standalone",
};
Step 5: Cache Optimization for CI
# Use BuildKit cache mounts for faster rebuilds
# syntax=docker/dockerfile:1
FROM node:20-alpine AS deps
WORKDIR /app
COPY package.json package-lock.json ./
RUN --mount=type=cache,target=/root/.npm \
npm ci --ignore-scripts
FROM node:20-alpine AS builder
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY . .
RUN --mount=type=cache,target=/app/.next/cache \
npm run build
# .github/workflows/build.yml
name: Build
on: push
jobs:
docker:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: docker/setup-buildx-action@v3
- uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- uses: docker/build-push-action@v6
with:
context: .
push: true
tags: ghcr.io/${{ github.repository }}:${{ github.sha }}
cache-from: type=gha
cache-to: type=gha,mode=max
# GHA cache makes subsequent builds 3-5x faster
Step 6: .dockerignore
# .dockerignore — Don't copy these into the build context
node_modules
.next
.git
.gitignore
*.md
docker-compose*.yml
.env*
coverage
tests
__tests__
.vscode
.idea
.turbo
dist
Summary
Rosa's image dropped from 1.2GB to 150MB (or 90MB with distroless). Deploy time went from 8 minutes to 2 minutes because the image pushes and pulls in seconds. The three-stage pattern (deps → build → runtime) ensures only production code runs in the container. BuildKit cache mounts make CI rebuilds 3-5x faster by caching npm packages and Next.js build artifacts across runs. The distroless variant has no shell — if an attacker gets in, there's nothing to exploit. The .dockerignore prevents the build context from including unnecessary files, making docker build itself faster.