惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

V2EX - 技术
V2EX - 技术
Threat Intelligence Blog | Flashpoint
Threat Intelligence Blog | Flashpoint
T
Threat Research - Cisco Blogs
T
The Exploit Database - CXSecurity.com
S
Schneier on Security
S
Securelist
P
Privacy & Cybersecurity Law Blog
Scott Helme
Scott Helme
T
Threatpost
C
Cybersecurity and Infrastructure Security Agency CISA
L
LINUX DO - 热门话题
Cyberwarzone
Cyberwarzone
Cisco Talos Blog
Cisco Talos Blog
量子位
博客园 - Franky
cs.CL updates on arXiv.org
cs.CL updates on arXiv.org
Latest news
Latest news
T
Troy Hunt's Blog
N
News | PayPal Newsroom
Google Online Security Blog
Google Online Security Blog
Apple Machine Learning Research
Apple Machine Learning Research
N
Netflix TechBlog - Medium
小众软件
小众软件
P
Palo Alto Networks Blog
Spread Privacy
Spread Privacy
C
Cyber Attacks, Cyber Crime and Cyber Security
C
Check Point Blog
aimingoo的专栏
aimingoo的专栏
WordPress大学
WordPress大学
L
Lohrmann on Cybersecurity
L
LINUX DO - 最新话题
D
Darknet – Hacking Tools, Hacker News & Cyber Security
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
The Last Watchdog
The Last Watchdog
S
Security @ Cisco Blogs
P
Privacy International News Feed
Last Week in AI
Last Week in AI
Microsoft Security Blog
Microsoft Security Blog
T
Tailwind CSS Blog
博客园_首页
云风的 BLOG
云风的 BLOG
V
Vulnerabilities – Threatpost
D
DataBreaches.Net
Recent Announcements
Recent Announcements
酷 壳 – CoolShell
酷 壳 – CoolShell
CTFtime.org: upcoming CTF events
CTFtime.org: upcoming CTF events
罗磊的独立博客
Engineering at Meta
Engineering at Meta
Forbes - Security
Forbes - Security
T
Tenable Blog

DEV Community

Authentication Security Deep Dive: From Brute Force to Salted Hashing (With Java Examples) Why AI Systems Don’t Fail — They Drift Spilling beans for how i learn for exam😁"Reinforcement Learning Cheat Sheet" I Replaced Chrome with Safari for AI Browser Automation. Here's What Broke (and What Finally Worked) How Python Borrows Other People's Work The $40 Architecture: Processing 1 Billion API Requests with 99.99% Uptime Vibe Coding: A Workflow Guide (From Zero to SaaS) Most webhook security guides protect the wrong side. The scary part is delivery. Headless CMS for TanStack Start: Build a Blog with Cosmic EU Age Verification App "Hacked in 2 Minutes" — What Actually Happened Comfy Cloud’s delete function does not actually remove files Running AI Models on GPU Cloud Servers: A Beginner Guide Event-driven media intelligence with AWS Step Functions and Bedrock I scored 500 AI prompts across 8 quality dimensions — here's what broke How to Call Google Gemini API from Next.js (Free Tier, No Backend Needed) The Portal Protocol: Reclaiming Human Connection in the Age of AI How to Fix Your Team's Scattered Knowledge Problem With a Self-Hosted Forum Intro to tc Cloud Functors: A Graph-First Mental Model for the Modern Cloud Designing Multi-Tenant Backends With Both Ownership and Team Access I Built a Neumorphic CSS Library with 77+ Components — Here's What I Learned PostgreSQL Performance Optimization: Why Connection Pooling Is Critical at Scale Cómo construí un SaaS multi-rubro para gestionar expensas en Argentina con FastAPI + Vue 3 🚀 I Built an Ethical Hacking Scanner Tool – Open Source Project I Replaced /usage and /context in Claude Code With a Single Statusline A Pythonic Way to Handle Emails (IMAP/SMTP) with Auto-Discovery and AI-Ready Design I Collected 8.9 Million Polymarket Price Points — Here's What I Found About How Markets Really Move EcoTrack AI — Carbon Footprint Tracker & Dashboard Everyone's Using AI. No One Agrees How. 5 self-hosted ebook managers worth trying in 2026 Building Your First AI Agent with LangChain: From Chatbot to Autonomous Assistant Common SOC 2 Failures (Real World) Stop Vibe-Checking Your AI App: A Practical Guide to Evals How to Use SonarQube and SonarScanner Locally to Level Up Your Code Quality Your Next To-Do App Is Dead — I Replaced Mine with an OpenClaw AI Sign a Nostr event in 60 lines of Python using coincurve — no nostr-sdk, no nbxplorer, no rust toolchain ITGC Audit Explained Like You’re in Big 4 Patch Tuesday abril 2026: Microsoft parcha 163 vulnerabilidades y un zero-day en SharePoint Stop scraping everything: a better way to track competitor price changes Listing on MCPize + the Official MCP Registry while routing payments OUTSIDE the marketplace — how I kept 100% of my x402 revenue Building an AI-Powered Risk Intelligence System Using Serverless Architecture Why We Ripped Function Overloading Out of Our AI Toolchain Testing AI-Generated Code: How to Actually Know If It Works SaaS Churn Is Killing Your Business. Here Is What to Do About It (Without a Support Team) The Speed of AI Is No Longer Linear - And Self-Improving Models Are Why How to Implement RBAC for MCP Tools: A Practical Guide for Engineering Teams From Standard Quote to Persuasive Proposal: AI Automation for Arborists I built a CLI that scaffolds complete multi-tenant SaaS apps Axios CVE-2025–62718: The Silent SSRF Bug That Could Be Hiding in Your Node.js App Right Now The dashboard that ended our friendship Data Pipelines Explained Simply (and How to Build Them with Python) The Hidden Cost of AI Systems Nobody Talks About. undefined vs undeclared, and how typeof behaves Switching from file-based jobs to NATS/Kafka in Rust without changing code io_uring Adventures: Rust Servers That Love Syscalls Why Agentic AI is Killing the Traditional Database The POUR principles of web accessibility for developers and designers Quantum Neural Network 3D — A Deep Dive into Interactive WebGL Visualization How To Install Caveman In Codex On macOS And Windows Automation Pipeline Reliability: Why Your Workflow Breaks When Nobody Is Watching I Built an 'Open World' AI Coding Agent — It Works From ANY Folder From Freelancing to Product: A Tech Service Company's SaaS Transformation China's AI Giants: Adding Tencent Hunyuan & ByteDance Doubao to AI University (74 Providers) On the Vibe Coders and Their Lies clerk: Auto-Summarize Your Claude Code Sessions AI Weekly — 2026/04/10–04/17 | The Model Lockdown Is Here, but the Toolchain Is the Real Battleground AI 週報 — 2026/04/10–2026/04/17 模型封鎖潮來了,但工具鏈才是真戰場 Maybe this is how Open-Source apps are born... 🚀 Fine-Tune LLMs with LoRA and QLoRA: 2026 Guide tRPC v11 + Next.js App Router: End-to-End Type Safety Without the Boilerplate ShadCN UI in 2026: Why I Stopped Installing Component Libraries and Started Owning My Components SaaS Billing in React Server Components: Stripe + Supabase Without a Single `useEffect` Join our DEV Weekend Challenge — $1,000 in Prizes Across TEN winners! Submissions Due April 20 at 6:59 AM UTC. Implementing FSRS Spaced Repetition in Flutter + Supabase — Adding Memory Science to an AI Learning App "I Texted My Localhost From the Train — Claude Code Fixed the Bug Before I Got Home" I Built a Sales Prep AI and It Went Deeper Than Expected Design to Code #2: One JSON, Eleven Outputs Solving the 100M-Row Problem: A Summary Table Pattern for High-Volume Push Notification Logs Flutter Web With Wasm: What Actually Changes For Developers I Built 50 Royalty-Free Soundtracks for My Side Project in a Weekend Using AI Music Generation The Vibe Coding Security Checklist: 7 Things to Check Before You Ship Stop Letting Googlebot Guess Fix Your React App's SEO Right Desconstruindo o Streaming do LinkedIn: Como Criar um Engine de Extração de Vídeo de Alta Performance com HLS e FFmpeg (EDA Part-1) EDA (Exploratory Data Analysis) Explained With Real Life — Why Looking at Your Data Is the Most Important Step in Machine Learning Brand Relationship Management at Scale: Our 4-Touch Outreach System for 200+ Brands Why String.fromEnvironment() Might Return an Empty String in Dart JGuardrails 1.0.0 — Hardening Java LLM Apps Against Jailbreaks, Toxicity, and Prompt Injection Plan and Schedule a Full Week of Threads Content From One Claude Conversation Coding Cat Oran Ep3, Five Tables Changed Everything Updated: BFF Pattern I'm done watching freelancers get buried by 200 proposals. So I'm building the alternative. This is my first post BFS Algorithm in Java Step by Step Tutorial with Examples Tracking LLM Pricing Monthly: An Open Dataset for 22 AI Models How We Measure Content ROI on a Comparison Site: Revenue Attribution Without Perfect Data Introducing Nova AI Ops: The AI-Native Operating System for SRE Teams I built a free desktop video downloader for Windows — Grabbit How Talkie OCR Helps Vision-Impaired & Dyslexic Users Read the World Around Them VRCFaceTracking安装和iPhone面捕配置教程,有bug Even CrowdStrike Can't See Your Agents The Automation Gold Rush: What n8n Workflows and Claude Are Opening Up for Developers Right Now
Dockerizing Next.js for production
Mahmoud Moka · 2026-05-03 · via DEV Community

Most Dockerfiles for Next.js you'll find online ship a 1.2 GB image, leak environment variables at build time, and rebuild every layer on a one-line change. They work on the demo. They don't work in production.

This is the Dockerfile I actually run. Multi-stage, ~150 MB final image, build-time and runtime env vars cleanly separated, layer caching that survives a package.json change. I'll walk through every line, explain why each stage exists, and call out the four gotchas that account for most "it worked locally" production failures.

The full setup (Dockerfile plus docker-compose, GitHub Actions deploy pipeline, auth, testing) is in a production-grade Next.js + NestJS starter I'm building. Free for email subscribers — subscribe at mahmoud-mokaddem.com.


The Dockerfile, up front

If you're in a hurry, copy this and skip to Common gotchas. The rest of the post explains every line.

# Stage 1: deps
FROM node:20-alpine AS deps
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci

# Stage 2: builder
FROM node:20-alpine AS builder
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY . .
ENV NEXT_TELEMETRY_DISABLED=1
RUN npm run build

# Stage 3: runner
FROM node:20-alpine AS runner
WORKDIR /app
ENV NODE_ENV=production NEXT_TELEMETRY_DISABLED=1
RUN addgroup --system --gid 1001 nodejs && adduser --system --uid 1001 nextjs
COPY --from=builder /app/public ./public
COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./
COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static
USER nextjs
EXPOSE 3000
ENV PORT=3000 HOSTNAME=0.0.0.0
CMD ["node", "server.js"]

Enter fullscreen mode Exit fullscreen mode

Three stages: deps, builder, runner. The first two do work; only the third ships.


Why multi-stage

A naïve Dockerfile copies your source, installs dependencies, builds, and runs — all in one stage. The image you ship to production carries everything that helped you build it: the full Node toolchain, npm's cache, dev dependencies, build artifacts you don't need at runtime, your .git directory if you weren't careful with .dockerignore. Easily 1+ GB.

Multi-stage builds let you do all that work in a "fat" intermediate image, then copy only the artifacts that need to ship into a clean final image. Each FROM starts a fresh image; COPY --from= reaches back into a previous stage to grab specific files.

For Next.js, the practical result: ~150 MB final image vs ~1.2 GB single-stage. Why this matters in production:

  • Faster registry pulls on small VPSes or autoscaling platforms. Pulling 1.2 GB on a 100 Mbps link takes ~96 seconds; pulling 150 MB takes ~12.
  • Faster cold starts on platforms like Fly.io and Cloud Run, where containers start on demand.
  • Lower registry cost when you push every commit.
  • Smaller security surface — fewer packages carrying potential CVEs in production.

The mental shortcut: do the messy work in a fat intermediate image, ship only the artifacts that need to run.


Stage 1 — Dependencies

FROM node:20-alpine AS deps
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci

Enter fullscreen mode Exit fullscreen mode

node:20-alpine is a deliberate trade-off. Alpine Linux is ~50 MB; node:20-slim is ~340 MB; node:20 (Debian-based) is ~1 GB. Alpine wins on size and is fine for almost every Next.js app.

The catch: Alpine uses musl libc instead of glibc. Some npm packages with prebuilt native binaries (historically canvas, sharp, certain database drivers) ship glibc binaries that don't load on Alpine. If you hit a binary-compatibility error during npm ci, the fix is usually to switch this stage's base to node:20-slim and accept the larger image. For a vanilla Next.js app, you'll never see this.

Notice we copy only package.json and package-lock.json, not the source. This is layer-caching discipline. Docker caches each layer; if a layer's input hasn't changed, it reuses the cached output. By isolating the dependency install to the lockfile, we get full cache reuse on every commit that doesn't touch dependencies — which is most of them. If we copied the source first, every code change would re-run npm ci from scratch.

About npm ci vs npm install: ci is deterministic, installs exactly what's in the lockfile, fails if the lockfile is out of date, and is faster. Always ci in Docker. (Yarn: yarn install --frozen-lockfile. pnpm: pnpm install --frozen-lockfile.)


Stage 2 — Builder

FROM node:20-alpine AS builder
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY . .
ENV NEXT_TELEMETRY_DISABLED=1
RUN npm run build

Enter fullscreen mode Exit fullscreen mode

Fresh stage, fresh Alpine, node_modules pulled forward from stage 1. COPY . . brings in the source tree (filtered by .dockerignore, covered below).

The standalone output mode is the one Next.js config flag you actually need. Add it to your next.config.js:

module.exports = {
  output: 'standalone',
};

Enter fullscreen mode Exit fullscreen mode

Without this flag, npm run build produces the standard Next.js build output and your final image has to ship the entire node_modules tree (~300 MB+). With it, Next.js traces every dependency actually used by your built routes and emits a self-contained server.js plus only those traced packages in .next/standalone/node_modules, typically ~15 MB. That one flag is the biggest size win in this Dockerfile.

npm run build produces three things we care about:

  • .next/standalone/ — the self-contained server plus traced node_modules
  • .next/static/ — built static assets (JS bundles, CSS) for _next/static/* routes
  • public/ — static files you put in the public folder, which Next.js doesn't bundle into standalone

Stage 3 copies these three things and nothing else.

Build-time vs runtime env vars

This is the most common Next.js + Docker bug I see, so it gets its own callout.

Variables prefixed NEXT_PUBLIC_ are baked into the client-side JavaScript bundle at build time. They are not read at runtime from the container's environment. If you set NEXT_PUBLIC_API_URL only at runtime via docker run -e, your client code will see whatever value it had at build time (usually empty), not what you set at runtime.

Two ways to handle it.

(a) Pass NEXT_PUBLIC_* as --build-arg and rebuild per environment:

ARG NEXT_PUBLIC_API_URL
ENV NEXT_PUBLIC_API_URL=$NEXT_PUBLIC_API_URL
RUN npm run build

Enter fullscreen mode Exit fullscreen mode

docker build \
  --build-arg NEXT_PUBLIC_API_URL=https://api.example.com \
  -t my-app .

Enter fullscreen mode Exit fullscreen mode

(b) Keep NEXT_PUBLIC_* for things that don't change per deploy (your domain, public Stripe key, public Sentry DSN), and put environment-specific config behind server-side data fetching where you can read process.env at runtime.

I prefer (b). Fewer images, simpler pipeline. Use (a) only when you genuinely need the value baked into the client bundle.


Stage 3 — Runner

FROM node:20-alpine AS runner
WORKDIR /app
ENV NODE_ENV=production NEXT_TELEMETRY_DISABLED=1
RUN addgroup --system --gid 1001 nodejs && adduser --system --uid 1001 nextjs
COPY --from=builder /app/public ./public
COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./
COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static
USER nextjs
EXPOSE 3000
ENV PORT=3000 HOSTNAME=0.0.0.0
CMD ["node", "server.js"]

Enter fullscreen mode Exit fullscreen mode

Final stage. Fresh Alpine, no toolchain, no dev dependencies. This is what ships.

NODE_ENV=production matters. Next.js skips dev-only logging and telemetry, and many libraries optimize behavior based on it.

The non-root user: addgroup creates a system group, adduser creates a user in it, USER nextjs switches the runtime to that user. Many container platforms (Kubernetes, ECS, Fly with strict modes) refuse to run containers as root by default. Even when they don't, running as root expands the impact of any container-escape CVE. This costs nothing; do it now.

The three copies are where the standalone output pays off:

  1. COPY --from=builder /app/public ./publicpublic/ is not part of the standalone output. Forget this line and all your favicons, robots.txt, and static images return 404. The first time. Always.
  2. COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./ — the actual server. --chown makes the non-root user own the files, otherwise it can't read its own runtime.
  3. COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static — also not in standalone. Forgetting this gives you a site with no JS or CSS.

EXPOSE 3000 is documentation, not a port-open. It tells docker run -p and orchestrators "this app expects to be reachable on 3000."

HOSTNAME=0.0.0.0 is required to accept connections from outside the container. Next.js's standalone server defaults to localhost, which means your container would only accept traffic from itself.

Use CMD ["node", "server.js"], not npm start. npm wraps the process and intercepts signals, so your container won't gracefully shut down on SIGTERM. Orchestrator-driven restarts hang for 30+ seconds before the kernel kills it. node server.js handles signals correctly.


The .dockerignore file

This file gets skipped a lot, and it's often the answer to "why is my build context 2 GB?"

node_modules
.next
.git
.env*
README.md
*.log
coverage
.vscode
.idea
.DS_Store

Enter fullscreen mode Exit fullscreen mode

Why each entry:

  • node_modules — gets reinstalled in the deps stage.
  • .next — build artifacts get rebuilt; carrying old ones in confuses Next.js's cache.
  • .git — your version history shouldn't ship in the container.
  • .env*never bake secrets into images. Pass at runtime.
  • Logs, IDE folders, coverage reports — clutter.

The .env* line is a security concern worth dwelling on. If you've ever had an .env.local sitting in your working directory, .dockerignore is what keeps it out of the image. An image with .env.production baked in can be pulled by anyone with read access to your registry. Put real secrets in your runtime environment, not in the image.


Image size walkthrough

Approach Final image
Naïve single-stage on node:20 ~1.2 GB
Multi-stage on node:20 (no standalone) ~600 MB
Multi-stage on node:20-alpine (no standalone) ~400 MB
Multi-stage on node:20-alpine + standalone ~150 MB

Numbers are approximate; your app's specific dependencies move them ±20%.

What this saves you: deploy time drops from ~96 seconds to ~12 on a 100 Mbps registry pull. Cold start time on Fly or Cloud Run becomes meaningful at the standalone size. The biggest win is the standalone output flag. The Alpine base is second. Multi-stage is the structural decision that makes both composable.


docker-compose for local dev

This Dockerfile builds the production image. For local dev you usually want hot reload, a local Postgres, maybe Redis. A minimal compose file:

services:
  app:
    build: .
    ports: ['3000:3000']
    environment:
      DATABASE_URL: postgres://user:pass@db:5432/myapp
    depends_on: [db]
  db:
    image: postgres:16-alpine
    environment:
      POSTGRES_USER: user
      POSTGRES_PASSWORD: pass
      POSTGRES_DB: myapp
    volumes: ['db_data:/var/lib/postgresql/data']
volumes:
  db_data:

Enter fullscreen mode Exit fullscreen mode

This runs the production build locally, which is useful for catching prod-only bugs but not for hot reload. For real dev work you want a separate docker-compose.dev.yml with the source mounted as a volume and next dev running. That's a full post in itself — coming next in this series.


Common gotchas

The four bugs that account for most "it worked locally" production failures with this setup:

1. Public folder not appearing. You forgot COPY --from=builder /app/public ./public. Symptom: 404s on every static asset. Fix: add the line.

2. NEXT_PUBLIC_* env vars not reaching the client. They were set at runtime, not build time. Symptom: client-side code reads undefined or stale values. Fix: pass via --build-arg (per the Stage 2 section) or restructure so the value isn't needed in the client bundle.

3. Container exits immediately, no logs. You're using npm start instead of node server.js. npm wraps the process and hides what's happening. Fix: CMD ["node", "server.js"].

4. OOM during npm run build on a small VPS. Hetzner CX11 / DigitalOcean $4 droplets often can't fit a Next.js build in RAM. Symptom: build fails with JavaScript heap out of memory or gets SIGKILLed. Fix: build in CI/CD and push the image to your registry, then pull on the VPS; or add a swap file on the VPS.

Each one has happened to me. Each one looks unrelated to Docker until you find it.


Where to deploy this

The Dockerfile doesn't change; the deploy target does.

  • Hetzner VPS + Coolify or Dokploy — cheapest, most control. What I'd pick for indie projects. Push the image to GitHub Container Registry; Coolify pulls and runs it.
  • DigitalOcean App Platform — push the Dockerfile, get a URL. Good middle ground.
  • Fly.io — global edge deploy, generous free tier for hobby work. fly launch auto-detects Next.js and writes a fly.toml for you.
  • AWS ECS / Fargate — enterprise default. More setup overhead, but the right call if you're already in AWS.

Each of these gets its own deploy walkthrough later in this series. The Dockerfile above works on all of them unchanged.


What's next

Two follow-ups in this series:

  • docker-compose for Next.js + NestJS local dev — the full dev-mode compose file with hot reload, Postgres, and Redis.
  • How I structure a NestJS project for production — the architecture conventions in the starter, with rationale.

If you've shipped this Dockerfile to a deploy target I didn't cover, I'd be curious what platform you picked and what bit you.

The full setup (Dockerfile, .dockerignore, docker-compose, GitHub Actions deploy pipeline, auth, testing) is in a production-grade Next.js + NestJS starter I'm building. Free for email subscribers — subscribe at mahmoud-mokaddem.com.