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

推荐订阅源

V
V2EX - 技术
D
DataBreaches.Net
阮一峰的网络日志
阮一峰的网络日志
Recent Announcements
Recent Announcements
V
V2EX
Hugging Face - Blog
Hugging Face - Blog
T
The Exploit Database - CXSecurity.com
Simon Willison's Weblog
Simon Willison's Weblog
Cisco Talos Blog
Cisco Talos Blog
Microsoft Security Blog
Microsoft Security Blog
C
Cyber Attacks, Cyber Crime and Cyber Security
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
K
Kaspersky official blog
F
Fortinet All Blogs
GbyAI
GbyAI
Forbes - Security
Forbes - Security
The Cloudflare Blog
博客园 - 司徒正美
博客园_首页
量子位
Schneier on Security
Schneier on Security
G
GRAHAM CLULEY
Cyber Security Advisories - MS-ISAC
Cyber Security Advisories - MS-ISAC
P
Proofpoint News Feed
N
News | PayPal Newsroom
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
博客园 - 聂微东
T
Tor Project blog
V
Vulnerabilities – Threatpost
Y
Y Combinator Blog
Jina AI
Jina AI
Help Net Security
Help Net Security
T
Threat Research - Cisco Blogs
Recent Commits to openclaw:main
Recent Commits to openclaw:main
C
Cybersecurity and Infrastructure Security Agency CISA
Project Zero
Project Zero
N
News and Events Feed by Topic
I
Intezer
B
Blog
美团技术团队
C
CERT Recently Published Vulnerability Notes
NISL@THU
NISL@THU
L
LINUX DO - 最新话题
让小产品的独立变现更简单 - ezindie.com
让小产品的独立变现更简单 - ezindie.com
Blog — PlanetScale
Blog — PlanetScale
AWS News Blog
AWS News Blog
T
Tailwind CSS Blog
The Last Watchdog
The Last Watchdog
雷峰网
雷峰网
有赞技术团队
有赞技术团队

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
Helm Charts in Production: Packaging Kubernetes Deployments Without the Pain
The Practica · 2026-06-12 · via The Practical Developer

Your team’s web service needs a Deployment, a Service, an Ingress, a ConfigMap, a HorizontalPodAutoscaler, and a PodDisruptionBudget. That is six YAML files. Each has a replicas field, an image tag, a set of environment variables, and a half-dozen other values that differ between staging and production. The current process for a new release is: open the staging YAML, update the image tag, check the replicas, check the env vars, commit, push, wait for ArgoCD, and repeat for production while hoping nobody forgets to change the env: prod label in the ConfigMap.

Last week someone did forget. Staging got deployed with production’s database connection string. Nobody noticed for four hours because staging was using a read replica that happened to work. The fix was a forced restart and a rotation of a credential that should never have been shared.

This is the problem Helm solves. It packages all six YAML files into one chart, parameterizes the differences with a values.yaml file, and lets you deploy any environment with a single helm upgrade --install command. The image tag, the replicas, the environment variables, the ingress hostname, everything that changes between environments goes in a values file, and everything that stays the same lives in the templates.

This post is the production Helm setup: the chart structure that scales, the template patterns that do not make you hate Go templates, the environment strategy, the CI/CD integration, and the versioning convention that makes rollbacks a one-flag operation.

What Helm actually gives you

Helm is three things bundled into one CLI:

Templating. Your raw Kubernetes YAML becomes Go templates with values injected from a values.yaml file. One chart produces the right manifests for dev, staging, and production by swapping values files.

Packaging. A chart is a versioned, portable directory you can publish to a registry (OCI or classic Helm repo), download, and install. Your chart is your deployable unit.

Lifecycle management. helm upgrade --install creates or updates a release. helm rollback reverts to a previous revision. helm history shows every change. You get deployment versioning without setting up a separate tool.

If you are not using Helm, your deployment process is either a shell script that runs kubectl apply -f against a directory of YAML files, or a GitOps tool (ArgoCD, Flux) that points at a monorepo of raw YAML. Both work. Both become painful at around five services because every environment variation requires either a copy-pasted YAML file or a fragile sed command in the deploy script.

A production chart structure

Start with this directory layout:

chart/
├── Chart.yaml
├── values.yaml
├── values.staging.yaml
├── values.production.yaml
├── templates/
│   ├── _helpers.tpl
│   ├── deployment.yaml
│   ├── service.yaml
│   ├── ingress.yaml
│   ├── configmap.yaml
│   ├── hpa.yaml
│   └── pdb.yaml
└── charts/

The Chart.yaml file sets the metadata:

apiVersion: v2
name: checkout-api
description: Checkout service for the product platform
type: application
version: 1.4.0
appVersion: 1.4.0

The version field is the chart version. The appVersion is the application version. They can move independently, which matters when you update manifests without deploying new code.

The master values.yaml file holds every sensible default:

# values.yaml
replicaCount: 2

image:
  repository: ghcr.io/myorg/checkout-api
  tag: latest
  pullPolicy: IfNotPresent

service:
  type: ClusterIP
  port: 3000

ingress:
  enabled: true
  className: nginx
  host: checkout-api.staging.example.com
  annotations:
    cert-manager.io/cluster-issuer: letsencrypt-prod

config:
  env: staging
  logLevel: debug
  dbPoolMin: 2
  dbPoolMax: 10

resources:
  requests:
    cpu: 250m
    memory: 256Mi
  limits:
    memory: 512Mi

autoscaling:
  enabled: true
  minReplicas: 2
  maxReplicas: 10
  targetCPUUtilizationPercentage: 75

pdb:
  enabled: true
  minAvailable: 1

Every value has a default. Every value can be overridden by an environment-specific values file. A template that references a nonexistent value will fail at render time, not at runtime.

Templates that do not make you hate templates

Go templates in Helm are not pleasant to write. The syntax is ugly, the function list is long, and the error messages are unhelpful. The trick is to keep templates simple and push complexity into values.yaml.

A clean deployment template looks like this:

{{- template "checkout-api.name" . }}
apiVersion: apps/v1
kind: Deployment
metadata:
  name: {{ include "checkout-api.fullname" . }}
  labels:
    {{- include "checkout-api.labels" . | nindent 4 }}
spec:
  replicas: {{ .Values.replicaCount }}
  selector:
    matchLabels:
      {{- include "checkout-api.selectorLabels" . | nindent 6 }}
  template:
    metadata:
      labels:
        {{- include "checkout-api.selectorLabels" . | nindent 8 }}
    spec:
      containers:
        - name: {{ .Chart.Name }}
          image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}"
          imagePullPolicy: {{ .Values.image.pullPolicy }}
          ports:
            - name: http
              containerPort: {{ .Values.service.port }}
              protocol: TCP
          envFrom:
            - configMapRef:
                name: {{ include "checkout-api.fullname" . }}
          resources:
            {{- toYaml .Values.resources | nindent 12 }}

Three rules to follow:

Use the _helpers.tpl naming conventions. Helm has a standard set of named templates (name, fullname, labels, selectorLabels) that every chart should include. They handle truncation, unique naming, and label consistency.

Keep logic out of templates. Do not write {{- if and .Values.ingress.enabled (eq .Values.config.env "production") -}} in a template. Either put the complex condition in a helper, or encode it as a single boolean in values. Templates are hard to test. Values files are easy to read.

Use toYaml and nindent for structured blocks. Instead of listing every resource field in template YAML, pass structured blocks through from values:

# values.yaml
extraEnv:
  - name: NODE_OPTIONS
    value: "--max-old-space-size=512"
  - name: SENTRY_DSN
    value: "https://key@sentry.io/project"
# templates/deployment.yaml
{{- with .Values.extraEnv }}
env:
  {{- toYaml . | nindent 12 }}
{{- end }}

This pattern handles environment variables, sidecar containers, volumes, init containers, and any other variable-length list without template gymnastics.

The _helpers.tpl that every chart needs

Put this in your templates/_helpers.tpl:

{{- define "checkout-api.name" -}}
{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }}
{{- end }}

{{- define "checkout-api.fullname" -}}
{{- if .Values.fullnameOverride }}
{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }}
{{- else }}
{{- $name := default .Chart.Name .Values.nameOverride }}
{{- if contains $name .Release.Name }}
{{- .Release.Name | trunc 63 | trimSuffix "-" }}
{{- else }}
{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }}
{{- end }}
{{- end }}
{{- end }}

{{- define "checkout-api.labels" -}}
helm.sh/chart: {{ include "checkout-api.name" . }}-{{ .Chart.Version | replace "+" "_" }}
{{ include "checkout-api.selectorLabels" . }}
{{- if .Chart.AppVersion }}
app.kubernetes.io/version: {{ .Chart.AppVersion | quote }}
{{- end }}
app.kubernetes.io/managed-by: {{ .Release.Service }}
{{- end }}

{{- define "checkout-api.selectorLabels" -}}
app.kubernetes.io/name: {{ include "checkout-api.name" . }}
app.kubernetes.io/instance: {{ .Release.Name }}
{{- end }}

These four named templates appear in almost every Helm chart. They are Helm’s version of a linter rule: predictable, standard, and they prevent silly naming collisions.

Environment-specific values files

The environment values files override only the values that change:

# values.staging.yaml
replicaCount: 2
image:
  tag: staging-abc1234
ingress:
  host: checkout-api.staging.example.com
config:
  env: staging
  logLevel: debug
  dbPoolMax: 10
# values.production.yaml
replicaCount: 6
image:
  tag: v1.4.0
ingress:
  host: checkout-api.example.com
config:
  env: production
  logLevel: info
  dbPoolMax: 25

Deploy with:

helm upgrade --install checkout-api ./chart \
  --namespace checkout \
  --values ./chart/values.yaml \
  --values ./chart/values.production.yaml

Helm merges values files in order. Later files override earlier ones. The base values.yaml provides every default, the environment file overrides what is different, and any one-off override can pass --set image.tag=v1.4.1 on the command line.

Do not put secrets in values files. Use --set with values from your CI secrets manager, or better, use External Secrets Operator or Sealed Secrets to pull secrets from Vault or AWS Secrets Manager at runtime.

CI/CD integration

A Helm deployment in CI is a four-step pipeline:

# .github/workflows/deploy.yml
deploy:
  runs-on: ubuntu-22.04
  steps:
    - uses: actions/checkout@v4

    - name: Lint chart
      run: helm lint ./chart

    - name: Render manifests
      run: |
        helm template checkout-api ./chart \
          --values ./chart/values.yaml \
          --values ./chart/values.${{ inputs.environment }}.yaml \
          > /tmp/rendered.yaml

    - name: Deploy
      run: |
        helm upgrade --install checkout-api ./chart \
          --namespace checkout \
          --create-namespace \
          --values ./chart/values.yaml \
          --values ./chart/values.${{ inputs.environment }}.yaml \
          --set image.tag=${{ inputs.tag }}

The helm lint step catches YAML syntax errors and missing required values. The helm template step lets you inspect the rendered output in CI logs without touching the cluster. The actual deploy uses helm upgrade --install, which is idempotent: run it a hundred times and the cluster state is the same as after one run.

For GitOps workflows (ArgoCD, Flux), the chart lives in a repository and the GitOps tool runs helm template and applies the result. The chart structure is the same; only the deployment mechanism changes.

Versioning and rollback

Every helm upgrade creates a new revision. Run helm history checkout-api -n checkout to see them:

REVISION    UPDATED                    STATUS        CHART              APP VERSION
1           Tue Jun 10 14:22:01 2026   superseded    checkout-api-1.3.0 1.3.0
2           Wed Jun 11 09:15:32 2026   superseded    checkout-api-1.3.1 1.3.1
3           Thu Jun 12 11:04:17 2026   deployed      checkout-api-1.4.0 1.4.0

If revision 3 breaks, roll back:

helm rollback checkout-api 2 -n checkout

The rollback is a new revision (4) that re-applies the revision-2 manifests. It is not a git revert. It is not a redeploy of an old image. It is Helm re-running the templates with the old values and applying the result.

To make rollbacks reliable, version your chart in lockstep with your application. When you tag a release v1.4.0, bump both version and appVersion in Chart.yaml:

version: 1.4.0
appVersion: "1.4.0"

Now helm list shows exactly which version of the application is deployed, and helm rollback restores both the manifests and the semantic intent.

The three mistakes that break production charts

Mistake 1: Missing values validation. Helm 3 has built-in schema validation with a values.schema.json file in the chart root. Use it to enforce types and required fields:

{
  "$schema": "https://json-schema.org/draft-07/schema#",
  "type": "object",
  "required": ["replicaCount", "image", "ingress", "config"],
  "properties": {
    "replicaCount": {
      "type": "integer",
      "minimum": 1
    },
    "image": {
      "type": "object",
      "required": ["repository", "tag"],
      "properties": {
        "repository": { "type": "string" },
        "tag": { "type": "string" }
      }
    }
  }
}

Apply the schema at deploy time:

$ helm template ./chart --values values.production.yaml --validate
Error: values don't meet the specifications of the schema(s) in the following chart(s):
checkout-api:
- replicaCount: Invalid type. Expected: integer, Given: string

Without this, a typo like replicaCount: "6" (string instead of integer) silently passes through and produces a broken Deployment.

Mistake 2: Storing secrets in values.yaml. A values file in git is not encrypted. A --set flag printed in CI logs is visible to anyone with access. Use External Secrets Operator, Sealed Secrets, or a Vault sidecar to inject secrets at runtime. The chart should reference a secret name, not a secret value.

Mistake 3: Overriding too much in --set. Every --set image.tag=v1.4.1 on the command line is a deployment that cannot be reproduced from the values files alone. If you rely on CI variables for critical values, your helm rollback might recreate the right chart version but the wrong image tag. Always encode the full release state in values files, and use --set only for one-off debugging.

Helm is good for single-service deployments and shared infrastructure components (ingress controllers, monitoring stacks, cert-manager). It is less good when:

  • Every service has a wildly different set of resources. If each service needs a unique combination of a dozen resource types, the template overhead outweighs the benefit. Raw YAML or a tool like kustomize (which overlays patches without templates) may be simpler.

  • You need to manage cross-service dependencies. Helm has a dependencies feature in Chart.yaml, but it is designed for third-party chart composition, not for wiring up your own services. For cross-service orchestration, use a higher-level tool like ArgoCD ApplicationSets or a Pulumi/CDK program.

  • Your team does not know Go templates. This is a real constraint. Helm templates are the worst part of Helm. If the team cannot read a {{- range $key, $value := .Values.configMap -}} block without a documentation tab open, you will accumulate technical debt in the templates. Consider kustomize as a template-free alternative, or invest in writing clean helpers that hide the template complexity.

The practical takeaway

A Helm chart is the packaging layer your Kubernetes deployments are missing. It standardizes the deploy process, eliminates copy-paste errors between environments, and gives you versioned rollbacks without adding another control plane.

Start small. Pick one service that has at least four YAML files. Build a chart for it. Deploy it to staging with helm upgrade --install. Add a values.production.yaml with the differences. Drop helm lint into your CI pipeline. Expand to the rest of the services when the team sees the difference between “edit six files” and helm upgrade --install --values values.production.yaml.

The chart templates in this post are ready to copy. Adjust the names, fill in your values, and delete one deployment script from your repo.

A note from Yojji

The gap between “it works on my machine” and “it deploys reliably across three environments” is where most teams lose time. Packaging infrastructure into versioned, testable charts is the kind of production discipline that does not make the feature roadmap but prevents the Friday afternoon deploy that takes down staging. Yojji’s engineering teams build this discipline into every cloud-native product they ship, from the first chart to the CI pipeline that deploys it.

Yojji is an international custom software development company founded in 2016, with offices in Europe, the US, and the UK. Their senior teams specialize in the JavaScript ecosystem, cloud platforms (AWS, Azure, Google Cloud), and Kubernetes-native architectures, handling the full product cycle from discovery through DevOps and production operations.