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

推荐订阅源

月光博客
月光博客
cs.CV updates on arXiv.org
cs.CV updates on arXiv.org
CTFtime.org: upcoming CTF events
CTFtime.org: upcoming CTF events
I
InfoQ
N
Netflix TechBlog - Medium
D
DataBreaches.Net
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
Threat Intelligence Blog | Flashpoint
Threat Intelligence Blog | Flashpoint
S
SegmentFault 最新的问题
奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
Hugging Face - Blog
Hugging Face - Blog
C
Cisco Blogs
T
Threat Research - Cisco Blogs
V
Visual Studio Blog
C
Cyber Attacks, Cyber Crime and Cyber Security
博客园_首页
Recorded Future
Recorded Future
J
Java Code Geeks
The Cloudflare Blog
S
Securelist
人人都是产品经理
人人都是产品经理
T
Tor Project blog
云风的 BLOG
云风的 BLOG
The GitHub Blog
The GitHub Blog
V
Vulnerabilities – Threatpost
V
V2EX
P
Palo Alto Networks Blog
I
Intezer
罗磊的独立博客
博客园 - 叶小钗
T
The Exploit Database - CXSecurity.com
博客园 - 【当耐特】
freeCodeCamp Programming Tutorials: Python, JavaScript, Git & More
The Hacker News
The Hacker News
T
The Blog of Author Tim Ferriss
Blog — PlanetScale
Blog — PlanetScale
P
Privacy International News Feed
P
Proofpoint News Feed
美团技术团队
Cisco Talos Blog
Cisco Talos Blog
博客园 - 司徒正美
Stack Overflow Blog
Stack Overflow Blog
L
LangChain Blog
L
LINUX DO - 热门话题
Simon Willison's Weblog
Simon Willison's Weblog
MyScale Blog
MyScale Blog
H
Help Net Security
W
WeLiveSecurity
Google Online Security Blog
Google Online Security Blog
让小产品的独立变现更简单 - ezindie.com
让小产品的独立变现更简单 - ezindie.com

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
Dev Containers: Reproducible Node.js Development Environments Your Whole Team Can Ship With
The Practica · 2026-06-26 · via The Practical Developer

You joined a new team. The README says “run npm install && npm run dev”. Twenty minutes later you are installing a specific version of Postgres via Homebrew, fighting with an obscure OpenSSL version mismatch, and discovering that the project depends on a system library nobody thought to document. By the time you ship your first PR, your dev environment already diverged from the last person who touched this project. Two weeks later a CI build fails on a Node.js version difference that nobody noticed until the pipeline caught it.

This is the default state of software development. Every engineer replicates the setup manually, and every replication introduces drift. The fix is not a better README. The fix is a single .devcontainer/devcontainer.json file that defines your environment as code, builds it in Docker, and opens VS Code inside the container. No manual steps. No drift. No “works on my machine.”

This post walks you through a production-grade dev container setup for a Node.js + Postgres project. You will end with a configuration your whole team can copy, commit, and forget.

What a dev container actually is

A dev container is a Docker container that VS Code (or GitHub Codespaces, or any devcontainer-compatible tool) opens as your full development environment. Your editor runs inside the container. The file system, terminal, extensions, language servers, and runtime tools all live there. Your laptop only runs VS Code itself and Docker.

The key files are:

  • .devcontainer/devcontainer.json — the configuration that tells VS Code how to build and open the container
  • .devcontainer/Dockerfile — (optional) custom image instructions on top of a base image
  • .devcontainer/docker-compose.yml — (optional) multi-service setup with databases, caches, and other dependencies

The workflow is simple: clone the repo, open the folder in VS Code, click “Reopen in Container” when prompted, and wait a few minutes for the first build. After that, every engineer on the team has an identical environment.

The naive devcontainer.json that most teams start with

Here is what most tutorials recommend:

{
  "name": "My Project",
  "image": "mcr.microsoft.com/devcontainers/typescript-node:1-22-bookworm",
  "forwardPorts": [3000],
  "postCreateCommand": "npm install",
  "customizations": {
    "vscode": {
      "extensions": ["dbaeumer.vscode-eslint", "esbenp.prettier-vscode"]
    }
  }
}

This works. It gives you Node.js 22, installs dependencies, and forwards port 3000. But it misses almost everything that makes dev containers actually useful in a real team: database dependencies, automatic tool configuration, lifecycle hooks for seeding data, proper volume mounts, and environment variable handling. In practice, this minimal setup will drift just as fast as a manual one, because nothing enforces that the Dockerfile matches production or that the extensions stay in sync.

Building a production-grade dev container

Let me walk through the full setup I use on every project now. It assumes a Node.js 22+ application with Postgres, but the pattern generalizes to any stack.

Step 1: The Dockerfile

Start with a custom Dockerfile that pins every system dependency your project needs. Do not rely on the base image having everything you need.

# .devcontainer/Dockerfile
ARG VARIANT=22-bookworm
FROM mcr.microsoft.com/devcontainers/typescript-node:1-${VARIANT}

# Install additional system packages your project needs
RUN apt-get update && export DEBIAN_FRONTEND=noninteractive \
    && apt-get -y install --no-install-recommends \
       postgresql-client \
       redis-tools \
       curl \
       jq \
       && apt-get autoremove -y && apt-get clean -y && rm -rf /var/lib/apt/lists/*

# Install a global tool your project uses
RUN npm install -g pnpm@latest-10

# Set up a non-root user for better file permissions
ARG USERNAME=node
ARG USER_UID=1000
ARG USER_GID=1000
RUN if [ "$USER_GID" != "1000" ] || [ "$USER_UID" != "1000" ]; then \
        groupmod --gid $USER_GID $USERNAME \
        && usermod --uid $USER_UID --gid $USER_GID $USERNAME; \
    fi

# Set the shell to bash with color prompt
ENV SHELL=/bin/bash \
    TERM=xterm-256color

The key detail here is postgresql-client and redis-tools. You should install client tools inside the container, not run them from your host machine. This guarantees every engineer has the exact same psql version for running migrations or inspecting data. The jq install is a quality-of-life win for anyone debugging JSON responses from the terminal.

Step 2: The docker-compose.yml

For anything beyond a single-process project, use Docker Compose to define your services. The app container mounts your source code and connects to a database container that lives on a private network.

# .devcontainer/docker-compose.yml
version: '3.8'

services:
  app:
    build:
      context: .
      dockerfile: Dockerfile
      args:
        VARIANT: 22-bookworm
    volumes:
      - ..:/workspace:cached
      - node_modules:/workspace/node_modules
    command: sleep infinity
    environment:
      DATABASE_URL: postgresql://app:app@db:5432/app
      REDIS_URL: redis://redis:6379
      NODE_ENV: development
    depends_on:
      db:
        condition: service_healthy
      redis:
        condition: service_healthy
    networks:
      - dev

  db:
    image: postgres:16-alpine
    restart: unless-stopped
    volumes:
      - postgres-data:/var/lib/postgresql/data
    environment:
      POSTGRES_USER: app
      POSTGRES_PASSWORD: app
      POSTGRES_DB: app
    healthcheck:
      test: ['CMD-SHELL', 'pg_isready -U app']
      interval: 5s
      timeout: 5s
      retries: 10
    networks:
      - dev

  redis:
    image: redis:7-alpine
    restart: unless-stopped
    healthcheck:
      test: ['CMD', 'redis-cli', 'ping']
      interval: 5s
      timeout: 3s
      retries: 5
    networks:
      - dev

volumes:
  node_modules:
  postgres-data:

networks:
  dev:

Three patterns here worth calling out:

Volume for node_modules. Mounting a named volume at /workspace/node_modules prevents the container from writing node_modules into your host filesystem. This avoids permission conflicts between the container user and your host user, and it speeds up file watchers because the host does not need to sync thousands of module files.

Health checks on dependencies. The depends_on.condition: service_healthy block means the app container waits for Postgres and Redis to actually accept connections before starting. Without this, your app starts, fails to connect, and you spend the next five minutes wondering why the database is unreachable.

No host port mapping. You do not need ports: "5432:5432" on the db service. Your app connects over the internal Docker network at db:5432. Exposing the port to the host is an optional convenience for running psql from your host machine, but it creates port conflicts when two projects both map 5432.

Step 3: The devcontainer.json

Now wire everything together with the config file that VS Code reads.

{
  "name": "My Project Dev Container",
  "dockerComposeFile": "docker-compose.yml",
  "service": "app",
  "workspaceFolder": "/workspace",

  "shutdownAction": "stopCompose",

  "postCreateCommand": "pnpm install && pnpm run db:migrate && pnpm run db:seed",
  "postStartCommand": "pnpm run dev",

  "forwardPorts": [3000, 5173],

  "containerEnv": {
    "SHELL": "/bin/bash"
  },

  "remoteUser": "node",

  "customizations": {
    "vscode": {
      "extensions": [
        "dbaeumer.vscode-eslint",
        "esbenp.prettier-vscode",
        "bradlc.vscode-tailwindcss",
        "EditorConfig.EditorConfig",
        "mtxr.sqltools",
        "mtxr.sqltools-driver-pg",
        "bierner.github-markdown-preview"
      ],
      "settings": {
        "editor.formatOnSave": true,
        "editor.defaultFormatter": "esbenp.prettier-vscode",
        "editor.codeActionsOnSave": {
          "source.fixAll.eslint": "explicit"
        },
        "files.eol": "\n",
        "[javascript][typescript][javascriptreact][typescriptreact]": {
          "editor.defaultFormatter": "esbenp.prettier-vscode"
        }
      }
    }
  }
}

Walk through the key fields:

  • dockerComposeFile and service. Tells VS Code to use the Docker Compose setup and attach to the app service. The db and redis services run alongside without needing their own VS Code connections.
  • shutdownAction: "stopCompose". When you close VS Code, it tears down all services including the database. Without this, the Postgres container keeps running and consuming resources.
  • postCreateCommand. Runs once after the container is built. This installs dependencies, runs migrations, and seeds test data. New engineers get a working database with sample data on their first build.
  • postStartCommand. Runs every time the container starts (including after a host reboot). This launches the dev server automatically. If you do not want the dev server to auto-start, remove this and run it manually from the integrated terminal.
  • remoteUser: "node". Runs inside the container as the node user, not root. Files created from within the container will have correct permissions on the host.
  • forwardPorts. Maps container ports to your local machine so you can open localhost:3000 in your browser and hit the dev server running inside the container.
  • customizations.vscode.extensions. The list of extensions that install automatically inside the container. These live in the container, not on your host VS Code, so your global extension set stays clean.
  • customizations.vscode.settings. Editor settings that apply only when working in this project. Format-on-save, ESLint auto-fix, and line endings are the ones that cause the most “but it looked fine in my editor” CI failures.

Step 4: The .env handling

Do not hardcode secrets in devcontainer.json. Use a .env file that sits outside the container but gets loaded on startup.

# .devcontainer/.env (gitignored)
DATABASE_URL=postgresql://app:app@db:5432/app
REDIS_URL=redis://redis:6379
SESSION_SECRET=dev-only-not-a-real-secret

Reference it in devcontainer.json with "remoteEnv":

"remoteEnv": {
  "DATABASE_URL": "${localEnv:DATABASE_URL:postgresql://app:app@db:5432/app}",
  "REDIS_URL": "${localEnv:REDIS_URL:redis://redis:6379}"
}

The ${localEnv:VAR:default} syntax reads from a .env file in the devcontainer folder, or falls back to the default value if no .env is present. This means the committed config works out of the box with sensible defaults, but individual engineers can override anything by creating a .env file.

Handling the real frictions

Dev containers solve the “works on my machine” problem, but they introduce new friction points. Here is how to handle the three I have hit most often.

Friction 1: Git credential forwarding

You need to authenticate with Git from inside the container. VS Code handles this automatically when you use the credential helper forwarding, but only if you configure it.

Add this to your devcontainer.json:

"mounts": [
  "source=/run/host-services/ssh-auth.sock,target=/run/host-services/ssh-auth.sock,type=bind"
],
"containerEnv": {
  "SSH_AUTH_SOCK": "/run/host-services/ssh-auth.sock"
}

On macOS you may need a different path. The VS Code Dev Containers extension handles most of this automatically if you accept the “forward SSH agent” prompt when the container starts.

Friction 2: File watcher limits

Node.js tooling (Vite, Webpack, Jest in watch mode) uses file watchers that hit the inotify limit on Linux hosts. Inside a container on macOS or Windows, this is even worse because of the filesystem translation layer.

Add this to the Dockerfile:

# Increase file watcher limit for dev tooling
RUN echo 'fs.inotify.max_user_watches=524288' | tee -a /etc/sysctl.conf

Some setups require --privileged mode or cap_add in docker-compose.yml to let the container set sysctl values. An alternative that avoids the permission dance is to configure your tools to use polling instead of file-system events:

// vite.config.ts or similar
export default defineConfig({
  server: {
    watch: {
      usePolling: true,
      interval: 1000,
    },
  },
});

Polling is slower, but it is reliable on every filesystem and does not require special capabilities.

Friction 3: Docker-in-Docker for integration tests

If your test suite spins up test containers (using testcontainers, dockest, or similar), you need Docker access from inside the dev container. The cleanest approach is to mount the host Docker socket:

"mounts": [
  "source=/var/run/docker.sock,target=/var/run/docker.sock,type=bind"
],
"remoteUser": "node",
"containerEnv": {
  "DOCKER_HOST": "unix:///var/run/docker.sock"
}

This gives the container access to the host Docker daemon. Containers started by the test suite run on the host, not nested inside the dev container. It is not true isolation, but for local development it is fast and simple. If you need real Docker-in-Docker (for a CI simulation, for example), add a docker service to your compose file using the docker:dind image.

What this actually changes for a team

I have set up this pattern on four different projects. The measurable effects are consistent:

  • Onboarding time drops from hours to minutes. A new engineer clones, opens, waits for the build, and has a running dev environment with data. No README-based installation ritual. No “actually, that Postgres version is too old, install 16 not 15.”
  • CI failures from environment drift disappear. Every engineer develops against the same Postgres 16-alpine, the same Node.js 22-slim, the same Redis 7. If the pipeline passes locally, it passes in CI, because CI uses the same Dev Container spec.
  • “Works on my machine” becomes a firingable phrase. When the environment is defined as code and shared by everyone, there is no machine-specific variance. If it does not work, the config is wrong, not the laptop.

The counterargument I hear most often is build time. The first build takes 3-5 minutes on a good internet connection. After that, Docker caches the layers and subsequent opens are under 30 seconds. Compared to the 45 minutes of manual setup a new hire goes through on a team without dev containers, the initial build is a bargain.

The one-sentence pitch to your team leader

Write this into your team chat or PR description:

Dev containers define the dev environment as code, give every engineer identical tooling with one click, and eliminate the single largest source of “but it worked locally” CI failures.

If someone pushes back on the Docker overhead, offer this compromise: put the devcontainer.json and Dockerfile in the repo and leave it optional. Engineers who prefer a native setup can ignore the .devcontainer folder entirely. The config costs nothing to maintain and saves hours the first time someone needs to reproduce a bug on an unfamiliar OS.

The complete file listing

Here is every file you need, relative to the project root:

.devcontainer/
  devcontainer.json
  Dockerfile
  docker-compose.yml
  .env (gitignored)

Four files. One JSON, one YAML, one Dockerfile, one gitignored env file. Commit the first three and the whole team gets the same environment.


A note from Yojji

The kind of infrastructure discipline this post describes (defining environments as code, eliminating manual setup drift, making onboarding a one-click operation) is exactly the operational maturity that separates teams that ship reliably from teams that spend every sprint fighting environment bugs. Yojji is an international custom software development company with offices in Europe, the US, and the UK, and their teams use Dev Containers and Docker Compose as standard practice across the Node.js, TypeScript, and cloud projects they deliver for clients.

If your team is spending more time fixing environment issues than shipping features, Yojji runs dedicated senior engineering teams that can bring this kind of reproducible setup to your project from day one. They handle the architecture, the CI/CD wiring, and the developer experience so your engineers focus on product code, not container configs.