Soomin Hwang
HR tech / Recruiting platform

Frontend Dockerization and CI/CD Optimization

Role
Deploy Pipeline & Build Structure
Timeline
Jul 2024 – Dec 2024
Stack
Next.js 14TypeScriptDockerJenkinspnpmGitHub Actionsnext-translate
Disclosure
Internal infrastructure
Docker image size
1.3GB500MB−62%
Deploy time
~15 min< 3 min−80%

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

MetricBeforeAfterChange
Docker image size1.3GB500MB−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:

LayerWithout 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_modules footprint 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.