This Dockerfile is optimized for the fastest possible builds in CI/CD environments where Docker images are rebuilt multiple times per day. It leverages advanced caching strategies, multi-stage builds, and modern Docker features to minimize build times on subsequent runs while keeping the final image size minimal.
Key optimizations include:
- β‘ BuildKit cache mounts for dependency and build artifact caching
- ποΈ Multi-stage builds to separate build dependencies from runtime
- π Bun for ultra-fast dependency installation (up to 25x faster than npm)
- π¦ Next.js standalone output for minimal runtime footprint
- π― Strategic layer ordering to maximize Docker layer caching
# syntax=docker/dockerfile:1.7 FROM oven/bun:1-slim AS base ENV NODE_ENV=productionPurpose: This section establishes the foundation for all subsequent build stages.
- # syntax=docker/dockerfile:1.7- Enables the latest Docker BuildKit features, including cache mounts and improved performance. This MUST be the first line.
- FROM oven/bun:1-slim AS base- Uses Bun's slim image as the base. Bun is a fast JavaScript runtime and package manager that significantly speeds up dependency installation. The- AS basecreates a named stage for reuse.
- ENV NODE_ENV=production- Sets the environment to production, which optimizes dependency installation (skips devDependencies in later stages if needed) and enables production optimizations.
FROM base AS builder WORKDIR /appPurpose: This stage handles the complete build process - dependency installation and Next.js compilation.
- FROM base AS builder- Creates a new build stage inheriting from our base layer. This stage will contain all build tools and will be discarded in the final image.
- WORKDIR /app- Sets the working directory for all subsequent commands. Creates the directory if it doesn't exist.
COPY package.json bun.lock ./ RUN --mount=type=cache,target=/root/.bun/install/cache \ bun install --frozen-lockfile --ignore-scriptsPurpose: Install dependencies with maximum caching efficiency to speed up rebuilds.
- COPY package.json bun.lock ./- Copies only the dependency manifests first. This is crucial for layer caching: if these files haven't changed, Docker will reuse this layer and skip reinstallation.
- RUN --mount=type=cache,target=/root/.bun/install/cache- Creates a persistent cache mount for Bun's download cache. This cache persists between builds, so packages don't need to be re-downloaded if they're already cached.
- bun install --frozen-lockfile- Installs dependencies exactly as specified in the lockfile (no version updates), ensuring reproducible builds.
- --ignore-scripts- Skips lifecycle scripts for faster, more secure installation.
COPY . . ENV NEXT_TELEMETRY_DISABLED=1 RUN --mount=type=cache,target=/app/.next/cache \ bun run build --no-lintPurpose: Copy the application code and build the Next.js application with caching for the build artifacts.
- COPY . .- Copies the entire application source code into the container. This comes AFTER dependency installation so that code changes don't invalidate the dependency cache layer.
- ENV NEXT_TELEMETRY_DISABLED=1- Disables Next.js telemetry to speed up builds and avoid network calls.
- RUN --mount=type=cache,target=/app/.next/cache- Mounts a persistent cache for Next.js build cache. This dramatically speeds up rebuilds when only small code changes are made.
- bun run build --no-lint- Runs the Next.js build process using Bun (faster than npm/yarn). The- --no-lintflag skips linting during build (assume it's done separately in CI).
FROM node:22-slim AS runner ARG GIT_REPOSITORY_URL ARG GIT_COMMIT_SHA WORKDIR /app ENV NODE_ENV=production ENV PORT=3000 ENV HOSTNAME=0.0.0.0Purpose: Create a minimal runtime environment with only what's needed to run the Next.js application.
- FROM node:22-slim AS runner- Starts a fresh stage from a minimal Node.js image. This creates a clean slate without any build tools, drastically reducing the final image size. We use Node (not Bun) because Next.js standalone output is optimized for Node.
- ARG GIT_REPOSITORY_URLand- ARG GIT_COMMIT_SHA- Defines build arguments that can be passed during build time for tracking which version of code is in the image.
- WORKDIR /app- Sets the working directory in the runtime container.
- ENV NODE_ENV=production- Ensures Next.js runs in production mode.
- ENV PORT=3000- Sets the default port the application will listen on.
- ENV HOSTNAME=0.0.0.0- Configures Next.js to listen on all network interfaces, necessary for container networking.
RUN groupadd -g 1001 nodejs || true RUN useradd -r -u 1001 -g nodejs service-user || truePurpose: Create a non-root user for running the application, following security best practices.
- RUN groupadd -g 1001 nodejs || true- Creates a group named "nodejs" with GID 1001. The- || trueensures the command succeeds even if the group already exists.
- RUN useradd -r -u 1001 -g nodejs service-user || true- Creates a system user (-r) named "service-user" with UID 1001 in the nodejs group. Running as non-root is a critical security practice.
COPY --from=builder /app/.next/standalone ./ COPY --from=builder /app/.next/static ./.next/static COPY --from=builder /app/public ./public USER service-user EXPOSE 3000 CMD ["node", "server.js"]Purpose: Copy the minimal runtime files from the builder and configure the container startup.
- COPY --from=builder /app/.next/standalone ./- Copies the standalone output from the builder stage. Next.js standalone output includes only the necessary files to run the app (no source code, no devDependencies).
- COPY --from=builder /app/.next/static ./.next/static- Copies the static assets (JS, CSS bundles) generated during build.
- COPY --from=builder /app/public ./public- Copies the public folder containing static files like images, fonts, etc.
- USER service-user- Switches to the non-root user for running the application. All subsequent commands and the runtime process will run as this user.
- EXPOSE 3000- Documents that the container listens on port 3000 (doesn't actually publish the port).
- CMD ["node", "server.js"]- Sets the default command to run when the container starts. The standalone output creates a server.js file that runs the Next.js app.
docker build \ --build-arg GIT_REPOSITORY_URL=$(git config --get remote.origin.url) \ --build-arg GIT_COMMIT_SHA=$(git rev-parse HEAD) \ -t my-nextjs-app .docker run -p 3000:3000 my-nextjs-appThis repository includes a comprehensive benchmark application in the example/ directory to test and demonstrate the Dockerfile's performance optimization.
Comparison Note: The default Dockerfile used for comparison is based on Vercel's official Next.js Docker example, adapted to support Bun lockfiles.
- Framework: Next.js 14 with App Router
- Pages: 2001 total (1 home page + 2000 dynamic routes)
- Pre-rendering: All pages statically generated at build time using generateStaticParams
- Build Delay: Each page includes an artificial 200-400ms delay (random) to simulate real-world API calls
- Route Pattern: /[id]where id ranges from 1 to 2000
This repository includes two Dockerfiles for comparison:
-  Dockerfile(Optimized) - The main optimized Dockerfile with:- BuildKit 1.7 syntax for advanced features
- Bun for ultra-fast dependency installation
- Persistent cache mounts for dependencies and build artifacts
- Strategic multi-stage builds
 
-  Dockerfile.default(Baseline) - Based on Vercel's official example:- Standard Node.js Alpine base
- Traditional npm/yarn/pnpm/bun package manager detection
- Multi-stage builds without cache mounts
- Industry-standard baseline for comparison
 
From the repository root:
# First, install dependencies in the example directory cd example bun install cd .. # Build with OPTIMIZED Dockerfile docker build -f Dockerfile -t nextjs-benchmark:optimized ./example # Build with DEFAULT Dockerfile (for comparison) docker build -f Dockerfile.default -t nextjs-benchmark:default ./example # Run the container docker run -p 3000:3000 nextjs-benchmark:optimizedThen visit http://localhost:3000 to see the benchmark app in action.
This repository includes a GitHub Actions workflow that automatically benchmarks both the optimized and default Dockerfiles on every push. The workflow runs two parallel jobs:
- Optimized Dockerfile - Uses BuildKit cache mounts, Bun, and multi-stage builds
- Default Dockerfile - Based on Vercel's official example
Each job performs three build stages:
- π§ Cold build - No cache (--no-cache), simulates first-time build
- π₯ Warm build - With cache after code change, simulates typical development rebuild
- π Hot build - Full cache with no changes, shows best-case caching performance
To view benchmark results:
- Check the Actions tab in this repository
- Look for "Docker Build Benchmark" workflow runs
- Each run includes a detailed summary comparing both Dockerfiles
To run benchmarks locally:
# Test optimized Dockerfile time docker build -f Dockerfile -t nextjs-benchmark:optimized ./example # Test default Dockerfile  time docker build -f Dockerfile.default -t nextjs-benchmark:default ./example- BuildKit Cache Mounts: The Bun install cache (~/.bun/install/cache) and Next.js build cache (.next/cache) persist between builds, eliminating redundant work
- Layer Ordering: Dependencies are installed before copying application code, so code changes don't invalidate the dependency layer
- Bun Speed: Dependency installation is significantly faster than npm/yarn
- Incremental Builds: Next.js caches compiled pages, so only changed pages need rebuilding
# Scenario 1: Cold build (no cache) docker builder prune -a -f time docker build -f Dockerfile -t nextjs-benchmark:cold ./example # Scenario 2: Warm build (code change with cache) # Edit a file, then rebuild echo "// Modified: $(date)" >> example/app/page.tsx time docker build -f Dockerfile -t nextjs-benchmark:warm ./example # Scenario 3: Hot build (no changes, full cache) time docker build -f Dockerfile -t nextjs-benchmark:hot ./example # Compare with default Dockerfile docker builder prune -a -f time docker build -f Dockerfile.default -t nextjs-benchmark:default-cold ./example echo "// Modified: $(date)" >> example/app/page.tsx time docker build -f Dockerfile.default -t nextjs-benchmark:default-warm ./example time docker build -f Dockerfile.default -t nextjs-benchmark:default-hot ./exampleThis Dockerfile is specifically optimized for CI/CD scenarios where:
- Cache mounts persist between builds (supported by most modern CI systems like GitHub Actions, GitLab CI, CircleCI)
- Layer caching is enabled and reused across builds
- Dependencies change infrequently compared to application code
- Multiple builds per day benefit from cached downloads and build artifacts
Expected performance (based on 2000-page benchmark app):
- π§ Cold build (first time): 5-10 minutes (depending on project size and page count)
- π₯ Warm build (code changes): 30-90 seconds (90%+ time savings)
- π Hot build (no changes): 5-15 seconds (99%+ time savings)
The optimized Dockerfile shows the greatest improvement in warm/hot builds thanks to BuildKit cache mounts. See the Benchmark Example App section for automated benchmark results and direct comparison with the default Dockerfile across all three scenarios.