TL;DR
- .env — flat text file for local dev. No encryption. One process sees it.
- ConfigMap — Kubernetes object for non-sensitive config. Shareable across pods, namespaces.
- Secret — same shape as ConfigMap but base64-encoded and access-controlled via RBAC.
- Base64 is not encryption. Secrets are not secrets at rest unless you enable encryption-at-rest.
The mental model
When you move from "my laptop" to "Kubernetes," your .env gets split in two:
- Non-sensitive values (PORT, LOG_LEVEL, NODE_ENV, feature flags) → ConfigMap
- Sensitive values (DATABASE_URL, JWT_SECRET, API_KEY) → Secret
Both become environment variables inside your pod. Your app reads them the exact same way it read .env — process.env.DATABASE_URL works unchanged.
Mapping a .env to Kubernetes
# .env (local)
NODE_ENV=production
LOG_LEVEL=info
PORT=3000
DATABASE_URL=postgres://user:pass@db/app
JWT_SECRET=super-long-secret
Split into two Kubernetes objects:
# configmap.yaml — non-sensitive
apiVersion: v1
kind: ConfigMap
metadata:
name: app-config
data:
NODE_ENV: production
LOG_LEVEL: info
PORT: "3000"
---
# secret.yaml — sensitive (base64-encoded)
apiVersion: v1
kind: Secret
metadata:
name: app-secrets
type: Opaque
data:
DATABASE_URL: cG9zdGdyZXM6Ly91c2VyOnBhc3NAZGIvYXBw
JWT_SECRET: c3VwZXItbG9uZy1zZWNyZXQ=Mount both into your Deployment with envFrom::
spec:
containers:
- name: app
envFrom:
- configMapRef:
name: app-config
- secretRef:
name: app-secretsOur .env → Kubernetes converter does the base64 encoding and scaffolding for you — paste .env, pick "Kubernetes Secret", copy the YAML.
Base64 ≠ encryption
This is the single most misunderstood fact about Kubernetes Secrets. The base64 encoding exists because etcd needs valid UTF-8 bytes, not because Kubernetes is encrypting anything. Anyone with read access to the Secret object can decode the values with echo ... | base64 -d.
To get actual encryption at rest, you need one of:
- Encryption-at-rest for etcd (native K8s feature, requires config)
- Sealed Secrets (Bitnami controller — encrypts with a cluster-specific key so Secret YAML can be safely committed to git)
- External Secrets Operator (pulls live from Doppler, Vault, AWS Secrets Manager, etc. — Secret objects never contain the actual values)
When to use which
| Stage | Use |
|---|---|
| Local dev on your laptop | .env |
| docker-compose | .env + env_file: |
| Kubernetes — non-sensitive | ConfigMap |
| Kubernetes — sensitive | Secret + encryption-at-rest enabled |
| Kubernetes — multi-cluster / multi-team | External Secrets Operator pointing at Doppler/Vault |
| Kubernetes — GitOps / ArgoCD | Sealed Secrets (encrypted YAML in git) |
Common mistakes
- Committing a Secret YAML to git. Base64 is not encryption. Anyone cloning the repo reads the values. Use Sealed Secrets if you want them in git.
- Putting everything in Secrets "to be safe." Non-sensitive config in ConfigMaps is cheaper, clearer, and more auditable.
- Mounting Secrets as files instead of env vars "for security." The filesystem mount is a read-only view into the same data — it's not more or less secure, just different ergonomics.
- Forgetting to restart pods after updating a Secret. Kubernetes doesn't auto-reload env vars. You need a rollout restart or a sidecar that watches for changes.
Recommended production stack
For a mid-sized team running Kubernetes in 2026:
- Store secrets in Doppler or Infisical — see our comparison.
- Sync them into the cluster via External Secrets Operator. K8s Secret objects get auto-populated from the upstream.
- Use ConfigMaps for anything non-sensitive — environment name, feature flags, log levels.
- Enable etcd encryption-at-rest if self-hosting. Managed K8s (EKS, GKE, AKS) does this automatically.
Convert your .env in one click
If you're just migrating your first .env to Kubernetes, use the converter — paste the file, pick "Kubernetes Secret", copy the YAML. Good enough for dev clusters; for production, layer one of the solutions above.