Docker Image Optimization: Halve CI/CD Build Time with Multi‑Stage Builds and Caching
This article demonstrates how to apply Docker's official recommendations—multi‑stage builds, build‑cache mounts, and non‑root users—to create production‑grade images, reduce layer count, and cut CI/CD build times roughly in half.
Official Recommended Dockerfile
The Docker documentation advises combining multi‑stage builds, build‑cache, and a non‑root user to produce production‑grade images.
# syntax=docker/dockerfile:1
FROM node:24-alpine AS build
WORKDIR /app
COPY package* ./
RUN npm install
COPY public ./public
COPY src ./src
RUN npm run build
FROM nginxinc/nginx-unprivileged:alpine3.23-perl
COPY --from=build --chown=nginx:nginx /app/build /usr/share/nginx/html
USER nginx
EXPOSE 8080Key points: use the new Dockerfile syntax, base the runtime on the official non‑root nginxinc/nginx-unprivileged image, and set file ownership with --chown=nginx:nginx.
Build Cache Optimization
Copy dependencies first so they are cached, then copy the source code. This avoids reinstalling packages on every code change.
# ✅ Correct: copy dependencies (rarely changed) first
COPY package.json package-lock.json* ./
RUN npm ci
COPY . .
# ❌ Wrong: copy code first, then reinstall dependencies each time
COPY . .
RUN npm ciEnable parallel builds with BuildKit:
DOCKER_BUILDKIT=1 docker compose build --parallelnpm/pip Cache Mounts
Mount a cache for package managers to avoid re‑downloading on each build.
# npm cache
RUN --mount=type=cache,target=/root/.npm npm ci
# pip cache
RUN --mount=type=cache,target=/root/.cache/pip pip install -r requirements.txtBenefit: packages are reused across builds.
Reducing Image Layers
Combine related commands into a single RUN to keep the image shallow. Using a requirements.txt file is the most efficient approach.
# Bad: multiple layers
RUN pip install fastapi
RUN pip install uvicorn
RUN pip install httpx
# Better: one layer
RUN pip install fastapi uvicorn httpx
# Best: use requirements.txt
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txtNon‑Root User (Security)
FROM nginxinc/nginx-unprivileged:alpine3.23-perl
# The image already includes the nginx user
USER nginx
EXPOSE 8080Running as a non‑root user improves container security.
Complete Optimized Example
# syntax=docker/dockerfile:1
# ===== Build stage =====
FROM node:24-alpine AS builder
WORKDIR /app
# Cache dependencies
COPY package.json package-lock.json* ./
RUN --mount=type=cache,target=/root/.npm npm ci
# Copy source and build
COPY src ./src
COPY public ./public
RUN npm run build
# ===== Runtime stage =====
FROM nginxinc/nginx-unprivileged:alpine3.23-perl AS runner
# Copy built assets with correct ownership
COPY --from=builder --chown=nginx:nginx /app/build /usr/share/nginx/html
USER nginx
EXPOSE 8080
ENTRYPOINT ["nginx", "-c", "/etc/nginx/nginx.conf"]
CMD ["-g", "daemon off;"].dockerignore to Reduce Build Context
node_modules/
.git/
*.log
dist/
build/Day 11 Summary
Multi‑stage builds keep only runtime files in the final image. --mount=type=cache caches npm/pip packages.
Copy package.json before COPY . to leverage layer caching. nginxinc/nginx-unprivileged provides an official non‑root Nginx image on port 8080. --chown=nginx:nginx ensures files belong to the correct user.
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
Tech Ocean
Focused on AI programming, sharing ready-to-use development efficiency solutions.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
