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

推荐订阅源

云风的 BLOG
云风的 BLOG
雷峰网
雷峰网
让小产品的独立变现更简单 - ezindie.com
让小产品的独立变现更简单 - ezindie.com
Cyberwarzone
Cyberwarzone
Hacker News: Ask HN
Hacker News: Ask HN
C
Cisco Blogs
NISL@THU
NISL@THU
C
Cyber Attacks, Cyber Crime and Cyber Security
L
LINUX DO - 热门话题
A
Arctic Wolf
Simon Willison's Weblog
Simon Willison's Weblog
S
Schneier on Security
P
Palo Alto Networks Blog
Know Your Adversary
Know Your Adversary
C
Cybersecurity and Infrastructure Security Agency CISA
G
GRAHAM CLULEY
K
Kaspersky official blog
D
Darknet – Hacking Tools, Hacker News & Cyber Security
V
Vulnerabilities – Threatpost
小众软件
小众软件
博客园 - 司徒正美
腾讯CDC
AWS News Blog
AWS News Blog
Last Week in AI
Last Week in AI
T
Tenable Blog
I
Intezer
博客园_首页
IT之家
IT之家
阮一峰的网络日志
阮一峰的网络日志
AI
AI
V
V2EX
Hacker News - Newest:
Hacker News - Newest: "LLM"
博客园 - 三生石上(FineUI控件)
W
WeLiveSecurity
D
Docker
H
Hackread – Cybersecurity News, Data Breaches, AI and More
Cyber Security Advisories - MS-ISAC
Cyber Security Advisories - MS-ISAC
Security Latest
Security Latest
F
Fortinet All Blogs
S
Secure Thoughts
T
Troy Hunt's Blog
T
The Blog of Author Tim Ferriss
Recorded Future
Recorded Future
M
MIT News - Artificial intelligence
GbyAI
GbyAI
Microsoft Security Blog
Microsoft Security Blog
L
LINUX DO - 最新话题
B
Blog RSS Feed
U
Unit 42
TaoSecurity Blog
TaoSecurity Blog

The Practical Developer

The Libuv Thread Pool Trap: Why Node.js Async APIs Stall Under Load Postgres Covering Indexes with INCLUDE: Eliminate Heap Fetches on Read-Heavy Workloads Postgres DISTINCT ON: The Fastest Way to Get the Latest Row Per Group Postgres Transaction Isolation: The Anomalies Your App Actually Faces in Production Linux TCP Tuning for Node.js Microservices: The Kernel Settings That Stop Silent Connection Drops Under Load Postgres HOT Updates and Fillfactor: Why Not All Writes Are Created Equal Database Connection Pool Leaks: Finding the Promise That Never Returns Its Seat Linux OOM Killer in Production: Why Your Node.js Containers Die Without a Stack Trace Postgres Materialized Views: Refresh Strategies That Do Not Lock Your Dashboards API Dependency Health Checks: Why /health Is Not Enough Authorization with Zanzibar Tuples: How Google Manages Permissions and How To Build the Same Check in Node.js Postgres Advisory Locks: The 20-Character Primitive That Replaces Redis for Coordination Dead Letter Queues: The Message Queue Pattern That Saves You at 2 a.m. File Descriptor Exhaustion: The Kernel Limit That Silently Drops Node.js Connections Graceful Degradation: The Pattern That Turns Total Outages into Partial Success PostgreSQL Full-Text Search: Dropping Elasticsearch for 90% of Use Cases S3 Presigned Multipart Uploads: Stop Your API Server from Being a File Upload Bottleneck MessagePack vs JSON: The Binary Serialization Switch That Cut Our Internal RPC Overhead by 40% DNS Caching in Node.js: The Silent Cause of Production Latency Spikes Reliable Cron Jobs: The Pattern That Stops Double Runs, Missed Executions, And The 2 AM Page GraphQL Query Complexity: Stop the OOM Query Before It Reaches Your Resolver Node.js Event Loop Lag: The Hidden Metric Behind Random Latency Spikes API Request Validation with Zod: The Schema That Catches Bad Input Before It Corrupts Your Database Load Shedding in Node.js: How to Reject Traffic Before You Drown Request Hedging: Cut Tail Latency In Half Without Overprovisioning Git Bisect: The Automated Binary Search That Finds Breaking Commits in Minutes Node.js Garbage Collection Tuning: Stop Letting V8 Pause Your Event Loop Node.js Server Timeouts: The Settings That Stop Slow Clients from Holding Sockets Hostage Postgres BRIN Indexes: The Time-Series Secret That Shrinks Indexes by 99% Event Sourcing with PostgreSQL: The Pragmatic 80% Solution Node.js Cluster Mode: Scaling the Event Loop Across CPU Cores Postgres Partial Indexes: Stopping Soft Deletes from Ruining Your Query Performance Request Coalescing with the Singleflight Pattern: Stop Drowning Your Database on Every Cache Miss The Bulkhead Pattern: Why One Slow Endpoint Should Not Drown Your Whole Service Node.js AsyncLocalStorage: End-to-End Request Context Without the Propagation Hell Postgres Deadlocks: Logging the Victim, Reproducing the Race, and Fixing the Lock Order Your Node.js HTTP Client Is the Bottleneck: Connection Pool Tuning That Works Optimistic Locking in Postgres: Stop Losing Data to Race Conditions Postgres Read Replicas: Stop Serving Stale Data to Your Users Cursor Pagination: Why Offset Queries Explode at Scale and How to Fix Them Node.js Worker Threads: 60 Lines That Stop a CSV Upload from Timing Out Every Other Request Reliable Webhook Delivery: Architecture for Outbound HTTP You Can Trust Request Timeouts and Deadline Propagation: Stop the Chain of Slowness Advanced Security Practices in Node.js Graceful Shutdown in Node.js: The 40 Lines That Stop 502s During Deploys Finding Node.js Memory Leaks with Heap Snapshots Idempotency Keys in 30 Lines: Stop Your Webhook From Charging Customers Twice Backpressure In Node.js: The Fix For Slow-Motion Queue Meltdowns Retries Done Right: Jitter, Budgets, and the Stampede You Did Not See Coming The Cache Stampede: Why Your "Just Add Redis" Layer Crashes Postgres at 3 a.m. Postgres SKIP LOCKED: An 80-Line Job Queue You Can Run Without Redis Stop Doing Work Nobody Wants: AbortController in Node.js, Done Right The N+1 Query Problem: We Found 23 In One Codebase And Killed Every One I Tried 5 AI Coding Tools for a Month. Here Is What I Actually Use CI/CD From Zero to Production in 30 Minutes With GitHub Actions Node.js vs Bun vs Deno: Which Runtime Should You Pick in 2025? Kubernetes Resource Requests And Limits: The Numbers That Decide If Your Cluster Is Stable The Three Pillars of Observability Are A Myth: What Actually Matters In Production pnpm Vs npm Vs yarn Vs Bun For Monorepos: Which One Earns The Migration In 2024 JSONB Indexing In Postgres: GIN Vs Expression Indexes, And When Each Is The Right Choice A Code Review Checklist That Ends The Same Three Arguments Every Sprint gRPC Vs REST In 2024: When The Switch Pays For Itself React Suspense For Data Fetching: The Pattern That Replaces Half Your Loading State Code The Five-Stage Rollout: How To Ship A Risky Change Without Holding Your Breath GitHub Actions In A Monorepo: Caching, Path Filters, And Secret Boundaries That Actually Work The Blameless Postmortem That Actually Improves Things: A Template And Six Hard-Won Rules Recursive CTEs In Postgres: How To Query A Tree Without N Round Trips Node.js Streams: When They Actually Help, And When They Just Add Complexity Playwright Vs Cypress In 2024: The Honest Comparison Of Which One Earns The Test Time React Server Components: The Mental Model That Makes The "use client" Boundary Obvious Pod Disruption Budgets: The K8s Object That Keeps Your Service Up During Cluster Maintenance Postgres LISTEN/NOTIFY: The Pub/Sub You Already Have And Are Not Using Chaos Engineering Starter Kit: The Five Drills That Don't Need Netflix-Scale Spec-Driven API Development With OpenAPI: How To Stop Drifting From Your Docs Kubernetes Autoscaling Beyond CPU: The Custom-Metric HPA Pattern That Actually Works Postgres Partitioning For Time-Series: The Boring Setup That Saves Your Database Distributed Locks With Redis: An Honest Look At Redlock And When You Don't Need It HTTP/2 vs HTTP/3: What Actually Changes For Your App, And What Doesn't Image Optimization For The Web In 2023: srcset, AVIF, And The Lighthouse Score You Actually Want Kafka vs RabbitMQ: A Decision Tree That Doesn't Hate You UUID vs Bigint Primary Keys In Postgres: The Index Math That Decides For You Flame Graphs: How To Find The Slow Function In 30 Seconds Without Profiling Theatre Postgres Streaming Vs. Logical Replication: Which One Solves Your Actual Problem ESLint Rules That Earn Their Keep: The Twelve I Enable On Every Project Pre-Commit Hooks That Pay For Themselves: Husky, lint-staged, And The Five Rules That Stick Zero-Downtime Database Migrations: The Six-Step Pattern That Rules Them All Circuit Breakers In Node.js: 50 Lines That Stop A Failing Dependency From Taking Down Your Service Postgres VACUUM Is Not Magic: How Your Hot Table Bloats To 80GB And How To Fix It Kubernetes Liveness And Readiness Probes: The Difference That Causes Half Your Outages Rate Limiting In Production: A Token Bucket In 30 Lines Of Redis The Outbox Pattern: How To Stop Losing Events When Postgres And Kafka Disagree Load Testing With k6: The Three Scenarios That Find Real Bugs (Not Synthetic Numbers) Postgres Row-Level Security For Multi-Tenant Apps: The Pattern That Stops You From Leaking Data Rebase vs. Merge: The Team Policy That Ends The Argument Forever OpenTelemetry in Node.js: Distributed Tracing That Actually Helps During an Incident Feature Flags That Pay Rent: The 4 Flag Types And When To Delete Each ETag, Last-Modified, and the Caching Headers Most APIs Get Wrong Connection Pooling Without the Cargo Cult: pgbouncer in 100 Lines of Config JSONB Is Not a Schema: When To Reach For It in Postgres, And When To Stop Bash Strict Mode: The Three Lines That Stop Your Deploy Script From Lying To You
Your Docker Image Is 1.2GB. Here Is How To Get It Under 80MB.
The Practica · 2026-05-01 · via The Practical Developer

Bloated Docker images are the silent tax on every team that ships containers. Slow CI. Slow deploys. Bigger attack surface. Bigger registry bill. And almost always, it’s fixable in an afternoon.

I took a real Node.js + TypeScript service we ship to production, started from the naive Dockerfile most teams write, and walked it down from 1.2GB to 78MB. Same app, same behavior, six steps, all measured on the same machine. Here is exactly what moved the needle.

The starting point: 1.2GB

This is the Dockerfile most teams begin with. It works. It is also wasteful in almost every line.

FROM node:22

WORKDIR /app
COPY . .
RUN npm install
RUN npm run build

EXPOSE 3000
CMD ["npm", "start"]

Build it and check the size:

$ docker build -t app:naive .
$ docker images app:naive
REPOSITORY   TAG     SIZE
app          naive   1.21GB

1.21GB to ship a service that produces about 4MB of compiled JavaScript. Let’s fix it.

Step 1: Switch the base image, 1.21GB to 412MB

The node:22 tag is Debian-based and includes a full toolchain you do not need at runtime. The slim variant strips most of it.

FROM node:22-slim
ImageSize
node:221.21GB
node:22-slim412MB
node:22-alpine178MB

Alpine is even smaller, but it uses musl libc instead of glibc. Most pure-JS apps run fine on it, but anything with native modules (bcrypt, sharp, node-gyp builds) needs extra care, and some packages have subtle musl bugs. I default to slim and reach for alpine only when I know the dependency tree is clean.

For this post, we will keep going with slim to stay realistic about real-world apps.

Step 2: Use a .dockerignore, 412MB to 388MB

COPY . . happily copies your node_modules, .git, build artifacts, local .env files, IDE folders, and test fixtures into the image. Even if a later step overwrites node_modules, the layer is already in the image history.

Create a .dockerignore:

node_modules
npm-debug.log
.git
.gitignore
.env*
.vscode
.idea
coverage
dist
build
*.md
test
__tests__
Dockerfile*
.dockerignore

Small win on size, big win on rebuild speed and security. Your local .env.development is no longer hiding inside a layer that gets pushed to a public registry.

Step 3: Multi-stage build, 388MB to 198MB

You need TypeScript, eslint, the test framework, and probably a hundred transitive dev dependencies to build the app. You do not need any of them to run it.

A multi-stage build compiles in one image and copies only the artifacts into a second, clean image:

# ---- builder ----
FROM node:22-slim AS builder
WORKDIR /app

COPY package*.json ./
RUN npm ci

COPY . .
RUN npm run build
RUN npm prune --omit=dev

# ---- runtime ----
FROM node:22-slim
WORKDIR /app

COPY --from=builder /app/node_modules ./node_modules
COPY --from=builder /app/dist ./dist
COPY --from=builder /app/package.json ./package.json

EXPOSE 3000
CMD ["node", "dist/index.js"]

Two things doing real work here:

npm ci instead of npm install. It uses package-lock.json directly, is faster, and is reproducible. Use it in CI and in Docker. Always.

npm prune --omit=dev strips dev dependencies after the build. On a typical TypeScript service, that is half of node_modules gone.

Step 4: Layer caching that actually works, same size, 5x faster rebuilds

This is not about size, but it is the single biggest CI win. Most Dockerfiles invalidate npm ci on every code change because they COPY . . before installing. Order matters.

Already shown above, but worth calling out: copy package*.json first, install, then copy the rest. Now npm ci is cached as long as your dependencies don’t change.

COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build

On a project with ~600 dependencies, this took our cold rebuild from 94 seconds to 18 seconds when only application code changed. Multiplied by every PR, every day, every developer.

Step 5: Switch to Alpine for the runtime stage, 198MB to 96MB

We can keep the Debian-based builder (compatible with everything) and switch only the runtime to Alpine. The compiled JS does not care about the base OS at runtime as long as no native binaries are calling glibc-specific symbols.

# ---- builder ----
FROM node:22-slim AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
RUN npm prune --omit=dev

# ---- runtime ----
FROM node:22-alpine
WORKDIR /app
COPY --from=builder /app/node_modules ./node_modules
COPY --from=builder /app/dist ./dist
COPY --from=builder /app/package.json ./package.json
EXPOSE 3000
CMD ["node", "dist/index.js"]

If you have native modules, build them in a stage that matches the runtime libc. For Alpine that means node:22-alpine as the builder too, plus apk add --no-cache python3 make g++ to compile, then a clean runtime stage.

Step 6: Drop Node entirely with distroless, 96MB to 78MB

Google’s distroless images contain just the Node runtime and its TLS roots. No shell, no package manager, no apt, no curl. If something pops a shell inside your container, there is no shell to pop.

# ---- runtime ----
FROM gcr.io/distroless/nodejs22-debian12
WORKDIR /app
COPY --from=builder /app/node_modules ./node_modules
COPY --from=builder /app/dist ./dist
COPY --from=builder /app/package.json ./package.json
EXPOSE 3000
CMD ["dist/index.js"]

Note the CMD syntax change. There is no shell in distroless, so you cannot use the shell form (CMD node dist/index.js). The exec form must be used, and the entrypoint is already node, so you just pass the script.

Trade-off: you cannot docker exec -it container sh for a quick poke around. For debugging, run the same image with the :debug tag, which adds a busybox shell. In production, you keep the locked-down version.

The full picture

StepImageSizeSaved
0. Naivenode:221.21GB-
1. slim basenode:22-slim412MB-67%
2. .dockerignorenode:22-slim388MB-6%
3. Multi-stage + prunenode:22-slim198MB-49%
4. Layer cachingnode:22-slim198MB(rebuild speed)
5. Alpine runtimenode:22-alpine96MB-52%
6. Distrolessdistroless/nodejs2278MB-19%

Total: 94% reduction. Roughly 15× smaller.

What this actually buys you

Disk and registry costs are the obvious win, but they are usually not the biggest one.

Faster cold starts on Kubernetes and serverless platforms. Pulling 78MB instead of 1.2GB on a node that does not have the layer cached is the difference between a pod ready in 4 seconds and one that takes 40.

Smaller attack surface. The CVE count on node:22 is in the hundreds. On distroless it is a handful. Your security scanner will stop screaming.

Faster CI. Pushing and pulling smaller images in your pipeline shaves real wall-clock time off every deploy.

Cheaper cross-region replication. If you mirror your registry across regions for DR, you are now moving 6% of the bytes you used to move.

A few things that did not make the cut

People keep recommending these, and they are usually not worth the complexity:

docker-slim (the auto-shrinking tool). It works, but it can silently strip files your app loads at runtime under conditions your test suite does not exercise. Debugging that in production is not a fun afternoon.

Static binaries with pkg or nexe. They produce small images, but they freeze you to a specific Node version and break dynamic imports. If you are willing to give up flexibility for size, you should probably be writing in Go or Rust, not bundling Node.

scratch images. Beautiful in theory. In practice, you spend a week tracking down missing CA certs, missing tzdata, and missing nameservers. Distroless gives you 95% of the benefit with none of the pain.

The takeaway

The naive Dockerfile is fine for a hackathon. For anything you ship more than once a week, the six steps above pay for themselves in a single afternoon. Most of the size comes off in the first three steps. The last three are the difference between “good enough” and “actually production-grade”.

Copy the final Dockerfile, set up your .dockerignore, point your CI at it, and stop paying the 1.2GB tax.


This article was put together by The Practical Developer, with notes from the Docker pipelines we run on real client products. If you are scaling a team that ships software like this, and would rather hire the practice than build it from scratch, Yojji is the international software development studio behind a lot of the work that ends up here. Founded in 2016, with offices across Europe, the US, and the UK, Yojji builds custom web and mobile products with senior engineers, dedicated teams, and a strong bias for actually shipping. Worth a mention if you are looking for the kind of partner who treats Dockerfiles, CI pipelines, and production hygiene as part of the job, not an afterthought.