Next.js Production Docker: Standalone Build SSR Deployment Guide
Next.js standalone output is like packing for a trip with only a carry-on. Instead of bringing your entire closet (all
node_modules), you pack exactly what you need. Your container starts faster, uses less memory, and is more secure.
What You Will Build
By the end of this tutorial, you will have a production-grade Next.js container that:
- Uses standalone output to shrink from 1.2GB to under 200MB
- Runs Server-Side Rendering (SSR) with data fetching from your Django API
- Handles environment variables correctly at build time and runtime
- Includes health checks for container orchestration
- Operates as a non-root user for security isolation
- Serves static assets with optimal caching headers
The Carry-On Analogy
Imagine you are going on a weekend trip. You have two options:
Option A: Bring your entire wardrobe. Every shirt, every pair of shoes, your winter coat, and three umbrellas. Your suitcase weighs 50kg. You pay excess baggage fees. Security takes forever to check every item. This is a standard Next.js Docker build.
Option B: Pack a carry-on. One pair of shoes, two shirts, essentials only. Your bag weighs 7kg. You walk straight through security. You are at your hotel in 20 minutes. This is Next.js standalone output.
The standalone build analyzes your application, traces every import, and includes only the code that actually executes. The rest — node_modules, dev tools, test files, source maps — stays behind.
Performance Wins
| Metric | Standard Build | Standalone Build | Improvement |
|---|---|---|---|
| Image size | 1.2GB+ | ~180MB | 6.7x smaller |
| Startup time | 15 seconds | 3 seconds | 5x faster |
| Memory usage | 400MB+ | 120MB | 3.3x less |
| Attack surface | Huge (all dependencies) | Minimal (traced only) | Dramatically reduced |
| Build time | 3 minutes | 2 minutes | 33% faster |
| Cold start | 8 seconds | 1.5 seconds | 5.3x faster |
A 1.2GB image takes 2 minutes to pull from a registry. An 180MB image takes 18 seconds. When your server crashes and auto-heals, that is the difference between 2 minutes of downtime and 18 seconds.
Standard Build vs Standalone Build
What Standard Build Includes (The Wrong Way)
1# WRONG: Standard build for production 2FROM node:20 3WORKDIR /app 4COPY package*.json ./ 5RUN npm install # Installs ALL dependencies including devDependencies 6COPY . . 7RUN npm run build # Builds the application 8CMD ["npm", "start"] # Needs node_modules at runtime
This image contains:
node_modules(800MB+) — every package including dev toolsjest,eslint,typescript— not needed in production- Source code (
src/) — not needed after build - Test files (
__tests__/) — never executed in production .next/cache/— build artifacts, not runtime assetspackage.json+ lock files — only needed fornpm start
Result: 1.2GB image that takes 15 seconds to start and exposes hundreds of unnecessary packages to attackers.
What Standalone Build Includes (The Right Way)
1# RIGHT: Standalone build for production 2FROM node:20-alpine AS builder 3WORKDIR /app 4COPY package*.json ./ 5RUN npm ci --only=production 6COPY . . 7RUN npm run build # Generates .next/standalone/ 8 9FROM node:20-alpine AS runner 10WORKDIR /app 11# Copy ONLY the standalone output — no node_modules needed! 12COPY /app/.next/standalone ./ 13COPY /app/.next/static ./.next/static 14COPY /app/public ./public 15CMD ["node", "server.js"]
This image contains:
.next/standalone/(server.js + traced dependencies) — the entire runtime.next/static/— built CSS and JS assetspublic/— public assets (images, fonts)- Node.js runtime — to execute
server.js
Result: 180MB image that starts in 3 seconds with only the code that runs.
How Standalone Output Works
When you set output: 'standalone' in next.config.js, Next.js performs dependency tracing during the build:
- Entry point analysis: Next.js identifies every server entry point (
page.tsx,route.ts, API routes) - Import tracing: It follows every
importandrequirerecursively - Dead code elimination: Packages never imported are excluded
- Server bundle generation: A single
server.jsfile is created with all traced dependencies inlined - Static asset collection: CSS, JS, and media files are organized in
.next/static/
The output .next/standalone/ directory is self-contained. You can copy it to any machine with Node.js installed, and it runs without npm install.
Complete Production Dockerfile
This is the definitive production Dockerfile for Next.js 14 with App Router.
1# ============================================ 2# STAGE 1: Dependencies 3# Install production dependencies only. 4# Uses Alpine Linux for minimal base image size. 5# ============================================ 6FROM node:20-alpine AS deps 7RUN apk add --no-cache libc6-compat 8WORKDIR /app 9 10# Copy package files 11COPY package.json package-lock.json* ./ 12 13# npm ci is faster and more reliable than npm install for CI/CD 14# --only=production skips devDependencies 15RUN npm ci --only=production 16 17# ============================================ 18# STAGE 2: Builder 19# Build the Next.js application. 20# This stage includes devDependencies needed for the build process 21# (TypeScript compiler, Tailwind CSS, etc.). 22# ============================================ 23FROM node:20-alpine AS builder 24WORKDIR /app 25 26# Copy dependencies from deps stage 27COPY /app/node_modules ./node_modules 28COPY . . 29 30# Build-time environment variables 31# These are baked into the JavaScript bundle at build time 32# They CANNOT be changed at runtime without rebuilding 33ARG NEXT_PUBLIC_API_URL 34ENV NEXT_PUBLIC_API_URL=${NEXT_PUBLIC_API_URL} 35 36ARG NEXT_PUBLIC_APP_NAME 37ENV NEXT_PUBLIC_APP_NAME=${NEXT_PUBLIC_APP_NAME} 38 39# Build the application 40# This generates .next/standalone/ when output: 'standalone' is set 41RUN npm run build 42 43# ============================================ 44# STAGE 3: Production Runner 45# The final image. Only contains what is needed to run. 46# No node_modules. No source code. No build tools. 47# ============================================ 48FROM node:20-alpine AS runner 49WORKDIR /app 50 51ENV NODE_ENV=production \ 52 NEXT_TELEMETRY_DISABLED=1 \ 53 PORT=3000 \ 54 HOSTNAME="0.0.0.0" 55 56# Security: Create non-root user 57# --system: system user (no login shell) 58# --gid 1001 / --uid 1001: standard non-privileged IDs 59RUN addgroup --system --gid 1001 nodejs && \ 60 adduser --system --uid 1001 nextjs 61 62# Copy standalone output from builder 63# This is the ENTIRE runtime — no node_modules needed 64COPY /app/.next/standalone ./ 65 66# Copy static assets 67COPY /app/.next/static ./.next/static 68 69# Copy public assets (images, fonts, etc.) 70COPY /app/public ./public 71 72# Switch to non-root user 73USER nextjs 74 75# Document the port 76EXPOSE 3000 77 78# Health check: verify the Next.js server is responsive 79HEALTHCHECK 808182 \ 83 CMD wget --no-verbose --tries=1 --spider http://localhost:3000/api/health || exit 1 84 85# Start the standalone server 86CMD ["node", "server.js"]
Why Three Stages?
| Stage | Purpose | What It Contains | Size |
|---|---|---|---|
| deps | Install production dependencies | node_modules (production only) | ~300MB |
| builder | Compile TypeScript, build Next.js | Source code + devDependencies + build output | ~500MB |
| runner | Serve the application | server.js + static assets + public | ~180MB |
The deps stage is separate from builder so that if you only change source code (not dependencies), Docker reuses the cached node_modules layer. Your builds go from 3 minutes to 45 seconds.
next.config.js: The Critical Configuration
1/** @type {import('next').NextConfig} */ 2const nextConfig = { 3 // ============================================ 4 // STANDALONE OUTPUT — THE MOST IMPORTANT LINE 5 // ============================================ 6 output: 'standalone', 7 8 // ============================================ 9 // IMAGE OPTIMIZATION 10 // ============================================ 11 images: { 12 // In Docker containers, the sharp library may not be available 13 // Set to true if you install sharp, false to disable optimization 14 unoptimized: true, 15 16 // Or configure remote patterns for external images 17 remotePatterns: [ 18 { 19 protocol: 'https', 20 hostname: 'api.deploymart.com', 21 pathname: '/media/**', 22 }, 23 { 24 protocol: 'https', 25 hostname: 'cdn.deploymart.com', 26 }, 27 ], 28 }, 29 30 // ============================================ 31 // SECURITY HEADERS 32 // ============================================ 33 poweredByHeader: false, // Remove X-Powered-By header 34 35 async headers() { 36 return [ 37 { 38 source: '/:path*', 39 headers: [ 40 { 41 key: 'X-Frame-Options', 42 value: 'SAMEORIGIN', 43 }, 44 { 45 key: 'X-Content-Type-Options', 46 value: 'nosniff', 47 }, 48 { 49 key: 'Referrer-Policy', 50 value: 'strict-origin-when-cross-origin', 51 }, 52 { 53 key: 'Permissions-Policy', 54 value: 'camera=(), microphone=(), geolocation=()', 55 }, 56 ], 57 }, 58 // Cache static assets aggressively 59 { 60 source: '/_next/static/:path*', 61 headers: [ 62 { 63 key: 'Cache-Control', 64 value: 'public, max-age=31536000, immutable', 65 }, 66 ], 67 }, 68 ]; 69 }, 70 71 // ============================================ 72 // TRAILING SLASH (SEO consistency) 73 // ============================================ 74 trailingSlash: false, 75 76 // ============================================ 77 // COMPRESS RESPONSES 78 // ============================================ 79 compress: true, 80 81 // ============================================ 82 // EXPERIMENTAL FEATURES (Next.js 14+) 83 // ============================================ 84 experimental: { 85 // Optimize package imports for common libraries 86 optimizePackageImports: ['lodash', 'date-fns', '@mui/material'], 87 }, 88}; 89 90module.exports = nextConfig;
Why output: 'standalone' Changes Everything
Without output: 'standalone':
.next/contains server code, client code, and build metadata- You need
node_modulesat runtime fornext start - The server requires the full Next.js framework installed
- Image size: 1.2GB+
With output: 'standalone':
.next/standalone/contains a self-containedserver.js- All dependencies are traced and inlined
- No
node_modulesneeded at runtime - Image size: ~180MB
Environment Variables: Build Time vs Runtime
This is the most confusing aspect of Dockerizing Next.js. Understanding the difference saves hours of debugging.
Build-Time Environment Variables (Public)
Variables prefixed with NEXT_PUBLIC_ are baked into the JavaScript bundle at build time. They become part of the static HTML and client-side JavaScript.
1// This works because NEXT_PUBLIC_API_URL is replaced at build time 2const apiUrl = process.env.NEXT_PUBLIC_API_URL; 3fetch(`${apiUrl}/api/products`);
Characteristics:
- Must be available during
docker build - Passed via
ARGin Dockerfile - Cannot be changed at runtime without rebuilding
- Visible in client-side JavaScript (never put secrets here)
Dockerfile:
1ARG NEXT_PUBLIC_API_URL 2ENV NEXT_PUBLIC_API_URL=${NEXT_PUBLIC_API_URL}
docker-compose.yml:
1services: 2 nextjs: 3 build: 4 context: ./nextjs 5 args: 6 NEXT_PUBLIC_API_URL: https://api.deploymart.com
Runtime Environment Variables (Server-Only)
Variables without NEXT_PUBLIC_ are only available on the server side. They are not included in the client bundle.
1// app/api/products/route.ts — server-side only 2export async function GET() { 3 // This is safe — only executes on the server 4 const dbPassword = process.env.DB_PASSWORD; 5 // ... 6}
Characteristics:
- Available at runtime inside the container
- Passed via
environmentin docker-compose.yml - Can be changed by restarting the container (no rebuild needed)
- Safe for secrets (never sent to the browser)
docker-compose.yml:
1services: 2 nextjs: 3 environment: 4 - DB_PASSWORD=${DB_PASSWORD} 5 - REDIS_URL=${REDIS_URL}
The Complete Environment Strategy
| Variable | Prefix | Build or Runtime | Safe for Secrets | Example |
|---|---|---|---|---|
| API base URL | NEXT_PUBLIC_ | Build | No | NEXT_PUBLIC_API_URL |
| App name | NEXT_PUBLIC_ | Build | No | NEXT_PUBLIC_APP_NAME |
| Database password | None | Runtime | Yes | DB_PASSWORD |
| Redis URL | None | Runtime | Yes | REDIS_URL |
| Stripe public key | NEXT_PUBLIC_ | Build | No | NEXT_PUBLIC_STRIPE_KEY |
| Stripe secret key | None | Runtime | Yes | STRIPE_SECRET_KEY |
Common Mistake: Runtime Public Variables
1// WRONG: Trying to use runtime env in client component 2'use client'; 3 4export default function ProductList() { 5 // This will be undefined! API_URL is not NEXT_PUBLIC_API_URL 6 const apiUrl = process.env.API_URL; 7 // apiUrl === undefined 8}
1// RIGHT: Use NEXT_PUBLIC_ prefix for client-side variables 2'use client'; 3 4export default function ProductList() { 5 const apiUrl = process.env.NEXT_PUBLIC_API_URL; 6 // apiUrl === "https://api.deploymart.com" 7}
1// RIGHT: Use server component for server-only variables 2// app/api/products/route.ts 3export async function GET() { 4 const dbPassword = process.env.DB_PASSWORD; // Works — server only 5 // ... 6}
Next.js Rendering Modes in Docker
Server-Side Rendering (SSR)
1// app/products/page.tsx — SSR by default in App Router 2async function getProducts() { 3 const res = await fetch( 4 `${process.env.NEXT_PUBLIC_API_URL}/api/products/`, 5 { cache: 'no-store' } // Always fetch fresh data 6 ); 7 if (!res.ok) throw new Error('Failed to fetch'); 8 return res.json(); 9} 10 11export default async function ProductsPage() { 12 const products = await getProducts(); 13 14 return ( 15 <main> 16 <h1>Products</h1> 17 {products.map((product) => ( 18 <ProductCard key={product.id} product={product} /> 19 ))} 20 </main> 21 ); 22}
Container implication: The Next.js container must stay running. It renders HTML on every request. This requires health checks and restart policies.
Static Site Generation (SSG)
1// app/about/page.tsx — SSG with generateStaticParams 2export default function AboutPage() { 3 return ( 4 <main> 5 <h1>About DeployMart</h1> 6 <p>Static content generated at build time.</p> 7 </main> 8 ); 9}
Container implication: HTML is generated during npm run build. The container can be a simple static file server. Nginx can serve these pages directly without hitting Node.js.
Incremental Static Regeneration (ISR)
1// app/products/page.tsx — ISR with revalidation 2async function getProducts() { 3 const res = await fetch( 4 `${process.env.NEXT_PUBLIC_API_URL}/api/products/`, 5 { next: { revalidate: 60 } } // Revalidate every 60 seconds 6 ); 7 return res.json(); 8} 9 10export default async function ProductsPage() { 11 const products = await getProducts(); 12 return ( 13 <main> 14 <h1>Products</h1> 15 <p>Updated in the background every 60 seconds</p> 16 {products.map((product) => ( 17 <ProductCard key={product.id} product={product} /> 18 ))} 19 </main> 20 ); 21}
Container implication: First request triggers SSR. Subsequent requests serve cached HTML. After 60 seconds, the next request triggers background revalidation. The container needs Node.js for revalidation but serves cached content most of the time.
API Routes
1// app/api/health/route.ts — Health check for Docker 2import { NextResponse } from 'next/server'; 3 4export async function GET() { 5 return NextResponse.json({ 6 status: 'healthy', 7 service: 'deploymart-web', 8 timestamp: new Date().toISOString(), 9 }); 10}
1// app/api/products/route.ts — Proxy to Django API 2import { NextResponse } from 'next/server'; 3 4export async function GET() { 5 const res = await fetch( 6 `${process.env.NEXT_PUBLIC_API_URL}/api/products/`, 7 { next: { revalidate: 60 } } 8 ); 9 const data = await res.json(); 10 return NextResponse.json(data); 11}
Docker Compose Integration
1# docker-compose.yml (production snippet) 2services: 3 nextjs: 4 build: 5 context: ./nextjs 6 dockerfile: Dockerfile.prod 7 args: 8 # Build-time variables (baked into the bundle) 9 NEXT_PUBLIC_API_URL: https://api.deploymart.com 10 NEXT_PUBLIC_APP_NAME: DeployMart 11 container_name: deploymart-web 12 restart: unless-stopped 13 14 # Runtime variables (server-side only) 15 environment: 16 - NODE_ENV=production 17 - PORT=3000 18 19 volumes: 20 # Share static files with Nginx 21 - nextjs_static:/app/.next/static 22 - nextjs_public:/app/public 23 24 networks: 25 - frontend 26 27 depends_on: 28 django: 29 condition: service_healthy 30 31 # Security hardening 32 user: "1000:1000" 33 read_only: true 34 tmpfs: 35 - /tmp:noexec,nosuid,size=100m 36 37 healthcheck: 38 test: ["CMD", "wget", "--quiet", "--tries=1", "--spider", "http://localhost:3000/api/health"] 39 interval: 30s 40 timeout: 10s 41 retries: 3 42 start_period: 40s 43 44 deploy: 45 resources: 46 limits: 47 cpus: '0.5' 48 memory: 256M 49 reservations: 50 cpus: '0.25' 51 memory: 128M
Hands-On Lab: Build and Verify
Step 1: Create the Project
1mkdir nextjs-production-lab && cd nextjs-production-lab 2npx create-next-app@14 . --typescript --tailwind --eslint --app --src-dir
Step 2: Configure next.config.js
1/** @type {import('next').NextConfig} */ 2const nextConfig = { 3 output: 'standalone', 4 images: { unoptimized: true }, 5 poweredByHeader: false, 6}; 7 8module.exports = nextConfig;
Step 3: Create Health Check API
1// src/app/api/health/route.ts 2import { NextResponse } from 'next/server'; 3 4export async function GET() { 5 return NextResponse.json({ 6 status: 'healthy', 7 service: 'nextjs-standalone', 8 timestamp: new Date().toISOString(), 9 }); 10}
Step 4: Create a Demo Page with SSR
1// src/app/page.tsx 2async function getData() { 3 const res = await fetch('https://jsonplaceholder.typicode.com/posts/1'); 4 return res.json(); 5} 6 7export default async function Home() { 8 const data = await getData(); 9 10 return ( 11 <main className="min-h-screen p-8"> 12 <h1 className="text-3xl font-bold">Next.js Standalone Demo</h1> 13 <div className="mt-6 p-4 bg-gray-100 rounded"> 14 <h2 className="font-semibold">{data.title}</h2> 15 <p className="mt-2 text-gray-700">{data.body}</p> 16 </div> 17 </main> 18 ); 19}
Step 5: Create the Dockerfile
Use the complete three-stage Dockerfile from earlier in this tutorial.
Step 6: Build and Verify
1# Build the image 2docker build -f Dockerfile.prod \ 3 --build-arg NEXT_PUBLIC_API_URL=https://api.example.com \ 4 -t nextjs-prod:1.0 . 5 6# Check image size 7docker images nextjs-prod 8# REPOSITORY TAG SIZE 9# nextjs-prod 1.0 187MB ← Should be under 200MB 10 11# Run the container 12docker run -d \ 13 --name nextjs-test \ 14 -p 3000:3000 \ 15 nextjs-prod:1.0 16 17# Wait for startup 18sleep 5 19 20# Test the health endpoint 21curl http://localhost:3000/api/health 22# {"status":"healthy","service":"nextjs-standalone","timestamp":"..."} 23 24# Test the SSR page 25curl -s http://localhost:3000/ | grep -o "Next.js Standalone Demo" 26# Next.js Standalone Demo 27 28# Verify non-root user 29docker exec nextjs-test whoami 30# nextjs 31 32# Check processes 33docker exec nextjs-test ps aux 34# PID 1: node server.js 35 36# Check memory usage 37docker stats nextjs-test --no-stream --format "table {{.Container}}\\t{{.MemUsage}}" 38# nextjs-test 120MiB / 256MiB
Step 7: Load Test
1# Install Apache Bench 2sudo apt-get install apache2-utils 3 4# Test concurrent requests 5ab -n 1000 -c 10 http://localhost:3000/ 6 7# Expected results: 8# Requests per second: ~800-1200 9# Failed requests: 0 10# Time per request: ~8ms
Common Mistakes and Solutions
Mistake 1: Forgetting output: 'standalone'
1// WRONG: No standalone output 2const nextConfig = { 3 // output is missing! 4};
Result: .next/standalone/ directory is not created. The production stage fails because server.js does not exist.
Fix:
1const nextConfig = { 2 output: 'standalone', 3};
Mistake 2: Missing NEXT_PUBLIC_ Prefix
1// WRONG: Runtime variable in client component 2'use client'; 3const apiUrl = process.env.API_URL; // undefined in browser
Result: API calls fail with undefined URL. The browser console shows fetch(undefined/api/products).
Fix:
1// RIGHT: Public variable for client 2const apiUrl = process.env.NEXT_PUBLIC_API_URL;
Mistake 3: Not Passing Build Args
1# WRONG: Build args not passed 2services: 3 nextjs: 4 build: ./nextjs 5 environment: 6 - NEXT_PUBLIC_API_URL=https://api.example.com # Too late!
Result: NEXT_PUBLIC_API_URL is undefined during build. The client bundle contains undefined.
Fix:
1# RIGHT: Build args passed at build time 2services: 3 nextjs: 4 build: 5 context: ./nextjs 6 args: 7 NEXT_PUBLIC_API_URL: https://api.example.com
Mistake 4: Using npm install Instead of npm ci
1# WRONG: npm install in production 2RUN npm install
Result: package-lock.json is ignored. Versions may differ from development. Builds are not reproducible.
Fix:
1# RIGHT: npm ci uses exact versions from lock file 2RUN npm ci --only=production
Mistake 5: Running as Root
1# WRONG: No USER directive 2FROM node:20-alpine 3COPY . /app 4CMD ["node", "server.js"]
Result: Container runs as root. If an attacker escapes, they have root on the host.
Fix:
1RUN addgroup --system --gid 1001 nodejs && \ 2 adduser --system --uid 1001 nextjs 3USER nextjs
Mistake 6: Missing Health Check
1# WRONG: No HEALTHCHECK 2CMD ["node", "server.js"]
Result: Docker thinks the container is healthy even if the Next.js server is stuck or cannot reach the API.
Fix:
1HEALTHCHECK \ 2 CMD wget --quiet --tries=1 --spider http://localhost:3000/api/health || exit 1
Mistake 7: Forgetting images.unoptimized
1// WRONG: Image optimization requires sharp 2const nextConfig = { 3 output: 'standalone', 4 // images not configured 5};
Result: Next.js tries to use the sharp library for image optimization. If not installed, the build fails or images do not load.
Fix:
1const nextConfig = { 2 output: 'standalone', 3 images: { 4 unoptimized: true, // Or install sharp in Dockerfile 5 }, 6};
Performance Optimization Checklist
| Optimization | Configuration | Impact |
|---|---|---|
| Standalone output | output: 'standalone' | 6.7x smaller image |
| Alpine base | node:20-alpine | 50MB smaller than Debian |
| npm ci | npm ci --only=production | Reproducible, faster builds |
| Multi-stage | 3-stage Dockerfile | Only runtime in final image |
| Non-root user | USER nextjs | Security isolation |
| Health checks | HEALTHCHECK | Auto-recovery |
| Static caching | Cache-Control: immutable | Zero backend requests for assets |
| Gzip | Nginx gzip on | 60-80% bandwidth reduction |
| Connection keepalive | Nginx upstream | Reuse TCP connections |
| Resource limits | Docker deploy.resources | Prevent container starvation |
Security Hardening Checklist
-
output: 'standalone'configured - Multi-stage build (3 stages minimum)
- Non-root user (
nextjs, UID 1001) -
poweredByHeader: false - Security headers (
X-Frame-Options,X-Content-Type-Options) -
NEXT_PUBLIC_variables contain no secrets - Runtime variables passed via
environment(not build args) -
.dockerignoreexcludes.env,node_modules,.next/cache - Health check endpoint at
/api/health -
read_only: truein Docker Compose -
tmpfsfor/tmpwithnoexec,nosuid - Resource limits (CPU and memory)
- No
latesttag on base image
Mini Project: DeployMart Frontend
Build a production-ready Next.js storefront with these specifications:
Requirements
-
Pages:
/— Homepage with product grid (ISR, revalidate 60s)/products/[id]/— Product detail page (SSR)/about/— Static page (SSG)/api/health/— Health check endpoint
-
Data Fetching:
- Fetch products from
NEXT_PUBLIC_API_URL - Use ISR for product listings
- Use SSR for product detail (dynamic data)
- Fetch products from
-
Dockerfile Requirements:
- Multi-stage build under 200MB
- Non-root user (
nextjs, UID 1001) - Standalone output
- Health check configured
- Build args for
NEXT_PUBLIC_API_URL
-
Verification:
1# Build image under 200MB 2docker build -f Dockerfile.prod --build-arg NEXT_PUBLIC_API_URL=http://localhost:8000 -t deploymart-web:1.0 . 3docker images deploymart-web 4# SIZE < 200MB 5 6# Run and verify 7docker run -d --name deploymart-web -p 3000:3000 deploymart-web:1.0 8 9curl http://localhost:3000/api/health 10# {"status":"healthy","service":"deploymart-web"} 11 12curl -s http://localhost:3000/ | grep "DeployMart" 13# Contains "DeployMart" 14 15# Verify non-root 16docker exec deploymart-web whoami 17# nextjs 18 19# Load test 20ab -n 1000 -c 10 http://localhost:3000/ 21# 0 failed requests
What You Learned
| Concept | What It Is | Why It Matters |
|---|---|---|
output: 'standalone' | Self-contained server bundle | 6.7x smaller images, no node_modules |
| Multi-stage build | Separate deps, build, and runtime | Faster builds, smaller images |
NEXT_PUBLIC_ | Build-time public variables | Client-side API URLs, app names |
| Runtime variables | Server-only environment | Secrets safe from browser |
| SSR | Server-Side Rendering | Fresh data on every request |
| SSG | Static Site Generation | Pre-rendered HTML, CDN cacheable |
| ISR | Incremental Static Regeneration | Best of both worlds |
| Health check | Container liveness probe | Auto-recovery, load balancer integration |
| Non-root user | USER nextjs | Prevents container breakout |
| Alpine Linux | Minimal base image | Smaller attack surface |
Next Module
In Module 7: SSL & Let's Encrypt, you will secure DeployMart with free SSL certificates. You will configure automatic HTTPS redirection, set up Certbot for certificate renewal, and harden Nginx with modern TLS settings — all running inside Docker Compose.