Concept
Containers vs Virtual Machines
- Virtual Machine (VM): Packs an entire Guest OS alongside the application, hypervisor, and system libraries. VMs take gigabytes of space and take minutes to start.
- Docker Container: Shares the host operating system's kernel, packaging only the application code, package dependencies, and light filesystem bins. Containers are highly compressed (megabytes) and launch in milliseconds.
┌────────────────────────────────────────────────────────┐
│ App Code ──▶ Node Runtime ──▶ Docker Image ──▶ running│
│ (JS/TS) (node_modules) (Immutable) Container│
└────────────────────────────────────────────────────────┘Multi-Stage Dockerfiles
Building a Next.js or Node app inside a container often results in massive image sizes (1GB+) because devDependencies and local build caches are packaged inside.
Multi-stage builds solve this by using multiple temporary container stages during compiling, copying only the final build outputs to the production image:
# Stage 1: Build dependencies
FROM node:20-alpine AS builder
WORKDIR /app
COPY package.json pnpm-lock.yaml ./
RUN npm install -g pnpm && pnpm install --frozen-lockfile
COPY . .
RUN pnpm build
# Stage 2: Final lightweight runner
FROM node:20-alpine AS runner
WORKDIR /app
ENV NODE_ENV=production
# Copy only the compiled build files
COPY --from=builder /app/.next/standalone ./
COPY --from=builder /app/.next/static ./.next/static
COPY --from=builder /app/public ./public
EXPOSE 3000
CMD ["node", "server.js"]Using Next.js's standalone mode extracts only the required node_modules chunks, reducing the final image size to ~100MB!
Layer Caching
Docker executes commands sequentially. If a command's dependencies haven't changed, Docker restores the layer from cache. To prevent re-running npm install on every code change, copy lockfiles and install dependencies before copying the rest of your application code:
# CORRECT: Layer caching optimized
COPY package.json pnpm-lock.yaml ./
RUN pnpm install --frozen-lockfile
COPY . . # code changes won't bust the npm install cache layerCommon Mistakes
1. Copying the entire directory before running dependency install
If you write COPY . . before RUN npm install, any code update (even updating a comment in a React file) invalidates the COPY layer cache. Docker is forced to re-download and re-install all node_modules from scratch, slowing down builds.
2. Running containers as the root user in production
By default, Docker executes commands inside containers as the root user. If an attacker breaches the app, they gain root access to the container files. Always use the built-in node user:
USER nodeBest Practices
- Use alpine or slim base images: Build from
node:20-alpineornode:20-slimto minimize image surface size and vulnerability vectors. - Implement
.dockerignore: Exclude files likenode_modules,.next, and.envto prevent local configs and build caches from bloat uploading to the Docker daemon. - Multi-Stage Builds: Separate dependencies installation and static compiling from the final runtime runner layer.
