Most Docker images are obesity epidemics in disguise. A simple Node.js app ships with 900MB of operating system because someone based it on ubuntu:latest. The container is not a VM. Treating it like one defeats the entire purpose. A well-built image is under 50MB, starts in milliseconds, and contains exactly one process.
A container should do one thing and do it with the smallest possible footprint.
Multi-stage builds are the single most important Docker feature. You build in a fat image with all your compilers and dev dependencies, then copy only the compiled artifacts into a minimal runtime image. The result is production containers that contain nothing except what they need to run.
# Build stage
FROM node:20-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production

# Production stage — only the runtime
FROM node:20-alpine
WORKDIR /app
RUN addgroup -g 1001 -S nodejs && \
    adduser -S nodejs -u 1001

COPY --from=builder --chown=nodejs:nodejs /app/node_modules ./node_modules
COPY --chown=nodejs:nodejs . .

USER nodejs
EXPOSE 3000
CMD ["node", "server.js"]

# Final image: ~45MB instead of 900MB
Distroless images from Google take this further. They contain no shell, no package manager, no userland utilities. Just the runtime and your application. If an attacker compromises the container, there is nothing to exploit. No curl to download payloads. No sh to execute them.
# Distroless Node.js — the nuclear option
FROM node:20 AS builder
WORKDIR /app
COPY . .
RUN npm ci && npm run build

FROM gcr.io/distroless/nodejs20-debian12
WORKDIR /app
COPY --from=builder /app/dist ./dist
COPY --from=builder /app/node_modules ./node_modules
ENV NODE_ENV=production
CMD ["dist/server.js"]

# Image size: ~35MB
# Attack surface: essentially zero
The health check directive is underused. Instead of relying on external monitoring to notice your container is dead, Docker can restart it automatically. A proper health check hits your application's actual health endpoint, not just checks if the process exists.
Security is not an afterthought. Run as non-root. Use read-only filesystems where possible. Drop all capabilities except what you explicitly need. A container is not a security boundary by default — you make it one through discipline. Small images, minimal privileges, and explicit behavior. That is the Docker I believe in.