Return to Base
Infra Op #012
Docker CI/CD DevOps

Reducing CI/CD Build Times by 60%

Nov 28, 2024 Optimization Log

The deployment pipeline was the bottleneck. Developers were waiting 15-20 minutes for feedback on simple changes. The culprit wasn't the test suite—it was the build process itself re-installing dependencies on every commit.

The Bottleneck

In a monolithic Node.js application, our Dockerfile was simple but inefficient. Every time a developer changed a single line of code in `src/`, Docker would invalidate the cache for that layer and all subsequent layers.

Because the COPY . . command happened before the dependency installation, we were forcing npm install to run from scratch on every build, wasting bandwidth and time.

The "Naive" Approach

This was the state of the Dockerfile when we audited the repository. Notice how the source code copy invalidates the cache for the install step immediately after it.

Dockerfile (Before)
FROM node:18-alpine

WORKDIR /app

# Bad: Copying everything invalidates cache on ANY file change
COPY . .

# This runs on every commit, taking 4-5 minutes
RUN npm install

CMD ["node", "index.js"]

The Optimization: Layer Ordering

Docker caches layers based on the checksum of the files added. By copying only the package.json and package-lock.json files first, we can run the install step.

Since these two files change infrequently compared to source code, Docker will use the cached layer for npm install 90% of the time.

The Implementation

We also introduced a Multi-stage Build. This allows us to install "devDependencies" (like TypeScript compilers or linters) in a build stage, but leave them out of the final production image, reducing image size and attack surface.

Dockerfile (After)
# --- Stage 1: Builder ---
FROM node:18-alpine AS builder
WORKDIR /app

# 1. Copy Manifests First
COPY package*.json ./

# 2. Install dependencies (Cached unless package.json changes)
RUN npm ci

# 3. Copy Source Code (Cache bust happens here)
COPY . .

# 4. Build (TypeScript compilation, etc.)
RUN npm run build

# --- Stage 2: Runner ---
FROM node:18-alpine AS runner
WORKDIR /app

ENV NODE_ENV production

# Copy only necessary files from builder
COPY --from=builder /app/dist ./dist
COPY --from=builder /app/package*.json ./
COPY --from=builder /app/node_modules ./node_modules

CMD ["node", "dist/index.js"]

The Results

We benchmarked the pipeline over a week of active development.

  • Avg Build Time (Before): 14m 30s
  • Avg Build Time (After): 5m 10s
  • Storage Saved: Production images reduced from 1.2GB to 350MB

Lesson: Order matters. Treat your Dockerfile instructions like a waterfall; place the least frequently changed items at the top to maximize cache hits.

Is your CI/CD slowing you down?

Waiting for builds kills momentum. I optimize pipelines to keep your team shipping.

Request System Audit
End of Log