ConfigMaps & Secrets
Inject configuration and secrets into Node.js pods as environment variables or files without baking values into Docker images.
Recipe
Quick-reference recipe card - copy-paste ready.
apiVersion: v1
kind: ConfigMap
metadata:
name: api-config
data:
LOG_LEVEL: info
API_BASE_URL: https://api.acme.dev
---
apiVersion: v1
kind: Secret
metadata:
name: api-secrets
type: Opaque
stringData:
DATABASE_URL: postgres://user:pass@db:5432/appcontainers:
- name: api
envFrom:
- configMapRef:
name: api-config
- secretRef:
name: api-secretsWhen to reach for this: Every Kubernetes Deployment. Non-secret config in ConfigMap; credentials in Secret (or external secret operator).
Working Example
# k8s/config.yaml
apiVersion: v1
kind: ConfigMap
metadata:
name: api-config
data:
NODE_ENV: production
PORT: "3000"
LOG_LEVEL: info
FEATURE_EXPORT_CSV: "false"
---
apiVersion: v1
kind: Secret
metadata:
name: api-secrets
type: Opaque
stringData:
DATABASE_URL: postgres://app:CHANGE_ME@postgres:5432/app
JWT_SECRET: CHANGE_ME
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: api
spec:
template:
spec:
containers:
- name: api
image: ghcr.io/acme/api:sha-abc123
envFrom:
- configMapRef:
name: api-config
- secretRef:
name: api-secrets
env:
- name: POD_NAME
valueFrom:
fieldRef:
fieldPath: metadata.name// src/env.ts
import { z } from "zod";
const schema = z.object({
NODE_ENV: z.enum(["development", "production", "test"]),
PORT: z.coerce.number().default(3000),
LOG_LEVEL: z.enum(["debug", "info", "warn", "error"]).default("info"),
DATABASE_URL: z.string().url(),
JWT_SECRET: z.string().min(32),
FEATURE_EXPORT_CSV: z.coerce.boolean().default(false),
POD_NAME: z.string().optional(),
});
export const env = schema.parse(process.env);What this demonstrates:
- Non-sensitive toggles in ConfigMap
- Secrets as env vars (simple) parsed by Zod at boot
- Downward API
POD_NAMEfor structured logs
Deep Dive
Config vs Secret Decision
| Data | Store in | Example |
|---|---|---|
| Log level, feature flags | ConfigMap | LOG_LEVEL=info |
| DB URL, API keys | Secret | DATABASE_URL |
| TLS certs | Secret (tls type) or cert-manager | Ingress TLS |
| Public OAuth client id | ConfigMap | non-sensitive id |
Never put secrets in ConfigMap (base64 is not encryption).
File Mount Pattern
volumeMounts:
- name: config
mountPath: /app/config
readOnly: true
volumes:
- name: config
projected:
sources:
- configMap:
name: api-config
- secret:
name: api-secretsimport { readFileSync } from "node:fs";
const jwtSecret = readFileSync("/app/config/JWT_SECRET", "utf8").trim();Files avoid env var size limits and reduce accidental env logging. App must read files at boot.
External Secrets Operator
apiVersion: external-secrets.io/v1beta1
kind: ExternalSecret
metadata:
name: api-secrets
spec:
refreshInterval: 1h
secretStoreRef:
name: aws-ssm
kind: ClusterSecretStore
target:
name: api-secrets
data:
- secretKey: DATABASE_URL
remoteRef:
key: /prod/api/DATABASE_URLSyncs AWS SSM into K8s Secret automatically. See Secrets Managers.
ECS Equivalent
"secrets": [
{
"name": "DATABASE_URL",
"valueFrom": "arn:aws:ssm:us-east-1:123:parameter/prod/api/DATABASE_URL"
}
]Cloud Run: --set-secrets DATABASE_URL=api-database-url:latest.
Gotchas
- Secret in image
ENV- layer history leak. Fix: orchestrator injection only. - Logging
process.envon boot - credential leak. Fix: log keys only, never values. - ConfigMap change without rollout - pods keep stale env until restart. Fix: Reloader sidecar or trigger rollout on config change.
- Giant ConfigMap as env - 1 MiB limit per env source. Fix: file mounts or split config.
- Same Secret for dev and prod - blast radius. Fix: namespace-scoped secrets per environment.
- JWT in ConfigMap - wrong store type. Fix: Secret with RBAC limited to service account.
Alternatives
| Alternative | Use When | Don't Use When |
|---|---|---|
| envFrom ConfigMap + Secret | Simple 12-factor Node apps | Huge config trees |
| Projected volumes | Mix files + env | Tiny single-env services |
| External Secrets Operator | SSM/Vault source of truth | No cluster admin for CRDs |
| Doppler / Vault Agent | Centralized secret platform | K8s-native only shops |
FAQs
Are Kubernetes Secrets encrypted?
At rest encryption depends on cluster config (KMS envelope). RBAC still required; treat as sensitive.
How do we rotate DATABASE_URL?
Update SSM/ExternalSecret, rolling restart Deployment. Use connection pools that recover on auth error.
Feature flags in ConfigMap?
Yes for boolean env flags. Complex flag systems use LaunchDarkly or similar SDK, not giant ConfigMaps.
.env in local dev?
dotenv for laptop only. Production uses ConfigMap/Secret parity names - Configuration Basics.
Can NestJS ConfigModule read files?
Yes. ConfigModule.forRoot({ envFilePath: undefined }) relies on injected env in prod.
Who can read Secrets?
RBAC: only api ServiceAccount in production namespace via RoleBinding.
Related
- Secrets Managers - SSM and Vault fetch
- Configuration Basics - Zod env module
- Kubernetes Deployment - mount in Deployment
- ECS Fargate & Cloud Run - AWS/GCP secret injection
- Platform Deploy Best Practices - section checklist
Stack versions: This page was written for Node.js 24.18.0 (Active LTS), npm 10+, TypeScript 5.6+, Express 5, Fastify 5, and NestJS 11.