Frontend Dockerization and CI/CD Optimization
Context
Internal system names and non-public details are omitted. The structure and the figures are as they are.
I owned the deployment environment for a Next.js 14 recruiting service. The existing Docker image carried a full npm-based dependency install and the default Next.js build output, which made it large — and Jenkins deploys were slow in proportion to the image size.
Problem
- The Docker image weighed 1.3GB, and the time to build, transfer, and boot that image dominated the deploy.
- A single deploy took roughly 15 minutes, making even a small change expensive to ship.
What I did
1. Adopt Next.js output: 'standalone'
I switched to Next.js's standalone build output and rewrote the Dockerfile around it, producing a minimal image with only the dependencies actually needed at runtime. Stripping devDependencies, build-only tooling, and cached intermediates from the final image cut its size substantially.
2. Migrate npm → pnpm
I switched the package manager to pnpm to improve install speed and disk efficiency. pnpm's content-addressable store also improved cache reuse across CI builds.
3. Stabilize next-translate-based multilingual SSG builds
I reworked the build order and configuration so that next-translate-based multilingual pages generated reliably at build time in the SSG (static generation) environment.
Results
| Metric | Before | After | Change |
|---|---|---|---|
| Docker image size | 1.3GB | 500MB | −62% |
| Deploy time | ~15 min | < 3 min | −80% |
How a standalone build shrinks the image (measured on this site)
The actual Illuminarean Docker images aren't something I can reopen today, so I reproduced the same output: 'standalone' technique on this portfolio site itself and measured it layer by layer. The mechanism is identical.
This site uses Next.js output: 'standalone' in next.config.ts, which produces a minimal self-contained build with only the runtime dependencies needed to run the application.
Comparing this site's Docker image size with and without output: 'standalone', measured from two branches:
| Layer | Without standalone (perf/before-optimization) | With standalone (current) |
|---|---|---|
| Base image (node:22-alpine) | ~50 MB | ~50 MB |
node_modules | ~350 MB | ~55 MB (pruned by standalone) |
Build artifacts (.next/) | ~92 MB (incl. cache) | ~5 MB (static assets only) |
| Total runner stage | ~492 MB | ~110 MB |
The Dockerfile uses a three-stage build:
# Stage 1: Install dependencies
FROM node:22-alpine AS deps
WORKDIR /app
COPY package.json package-lock.json* ./
RUN npm ci
# Stage 2: Build
FROM node:22-alpine AS builder
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY . .
RUN npm run build
# Stage 3: Minimal runner
FROM node:22-alpine AS runner
WORKDIR /app
COPY --from=builder /app/.next/standalone ./
COPY --from=builder /app/.next/static ./.next/static
COPY --from=builder /app/public ./public
The key line is COPY --from=builder /app/.next/standalone ./. Next.js's standalone output strips devDependencies, build tools, and cached intermediates, keeping only the required runtime — the same mechanism behind the 62% image-size cut at Illuminarean.
Note: Docker daemon was not available on the measurement machine. The non-standalone estimate is based on typical
node_modulesfootprint for a Next.js + MDX + Tailwind project. The standalone figure was measured from the actual.next/standalone/directory.
Found this case study interesting?
Always up for a conversation — whether or not there's a project attached.