Docker & Containerization for Cloud Applications: Multi-Stage Builds & Compose
What You Will Master in This Tutorial
- Shrink container images from 1.2 GB down to under 85 MB using multi-stage build patterns
- Enforce container security by creating and switching to unprivileged non-root users
- Configure Docker Compose for multi-container stacks with Redis caches and PostgreSQL health checks
- Implement layer caching optimizations to cut CI/CD build times by over 70%
# Stage 1: Build & Compile
FROM node:22-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
# Stage 2: Minimal Production Runtime
FROM node:22-alpine AS runner
WORKDIR /app
ENV NODE_ENV=production
# Create non-root security user
USER node
COPY --chown=node:node package*.json ./
RUN npm ci --only=production && npm cache clean --force
COPY --chown=node:node --from=builder /app/dist ./dist
EXPOSE 3000
CMD ["node", "dist/server.js"]
Frequently Asked Questions
Going deeper: containerization in practice
The multi-stage Dockerfile in this tutorial β shrinking an image from 1.2 GB to under 85 MB β is only the visible half of production containerization. The other half is the habits around it: build one image and promote that same artifact through every environment, mirror production locally with Docker Compose stacks whose PostgreSQL health checks and Redis caches behave like the real thing, and order your Dockerfile layers so CI/CD builds stay cached and fast. Teams that internalize those habits stop having 'works in the container, fails in production' surprises, because the container definition becomes the environment contract for the whole pipeline.
Common mistakes
- Copying the whole repository before installing dependencies. A broad COPY invalidates the layer cache on every source change. The tutorial's Dockerfile copies package manifests first and runs npm ci before adding source, so dependency layers survive code edits.
- Running the production container as root. The runner stage creates and switches to the unprivileged node user for a reason: if the process is ever compromised, the attacker should inherit the smallest possible privileges inside the container.
- Shipping the builder stage to production. Compilers, dev dependencies, and build caches multiply image size and attack surface. Multi-stage builds exist so only the compiled output and production dependencies reach the final image.
- Treating containers as places to store data. Anything written to a container's own filesystem disappears when it is replaced. Durable data belongs in volumes or managed services, the way the Compose stack runs PostgreSQL as its own service with health checks.
Frequently asked questions
Why does the tutorial build on Alpine Linux instead of a Debian-based image?
As the tutorial's FAQ explains, Alpine base images start at just 5 MB and ship far fewer installed packages, which dramatically reduces the attack surface for CVE vulnerabilities. The trade-off to verify is native-module compatibility, since Alpine uses a different C library than Debian.
Should I use containers or virtual machines?
Containers share the host kernel, so they start in seconds and pack densely on one machine; virtual machines isolate a full guest operating system at higher cost. Most cloud applications run containers, very often on virtualized hosts underneath.
How do I know a Compose stack is actually ready to serve traffic?
Use health checks, as this tutorial does for PostgreSQL: dependents should wait until a service reports healthy, not merely started, before connecting β otherwise the first requests hit a database that is still initializing.