Kubernetes injects environment variables into containers four ways: inline env name-value pairs, envFrom bulk imports from a ConfigMap or Secret, valueFrom for a single key, and the downward API for pod metadata and resource limits. The choice hinges on three things: whether the value is sensitive, how many variables you need, and whether they differ per environment. The detail that burns the most production hours is that none of these refresh after a ConfigMap or Secret edit — env vars are frozen at container start, so a config change needs a pod restart. If you are coming from Docker, see the Docker env variables guide for the natural progression into Kubernetes.
TL;DR
- Use inline
envfor a few static values,envFromto bulk-import a ConfigMap or Secret, andvalueFromfor a single key or downward-API metadata. - Env vars are set once at container start. Editing a ConfigMap or Secret does not update them — only a pod restart does. A volume mount of the same ConfigMap auto-updates within the kubelet sync period; env injection never does.
- A single ConfigMap caps at 1 MiB. Larger config belongs in a mounted volume, a file service, or a database.
- Load order is
envFromfirst, inlineenvlast — inline entries override bulk imports, but$(VAR)interpolation only resolves earlier inline vars, neverenvFromkeys.
How do you set inline environment variables in a Pod spec?
The simplest approach is the env field directly in a container spec. Each entry is a name-value pair available inside the container at runtime.
apiVersion: v1
kind: Pod
metadata:
name: my-app
spec:
containers:
- name: app
image: my-app:1.0
env:
- name: NODE_ENV
value: "production"
- name: PORT
value: "3000"
- name: LOG_LEVEL
value: "info"Values must always be strings. Even numeric values like 3000 need quoting in YAML. This method works for a handful of variables but becomes unwieldy when you have dozens of entries shared across multiple Deployments.
How do you load environment variables from a ConfigMap?
A ConfigMap stores non-sensitive key-value pairs. Use envFrom with a configMapRef to inject all keys at once.
apiVersion: v1
kind: ConfigMap
metadata:
name: app-config
data:
NODE_ENV: "production"
LOG_LEVEL: "info"
MAX_RETRIES: "5"
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: my-app
spec:
template:
spec:
containers:
- name: app
image: my-app:1.0
envFrom:
- configMapRef:
name: app-configEvery key in the ConfigMap becomes an environment variable. You can add an optional prefix field to namespace the variables, for example prefix: APP_ turns LOG_LEVEL into APP_LOG_LEVEL.
How do you inject secrets as environment variables?
Secrets work like ConfigMaps but store data as base64-encoded values. Use envFrom with a secretRef to load them all, or valueFrom.secretKeyRef for individual keys.
apiVersion: v1
kind: Secret
metadata:
name: app-secrets
type: Opaque
data:
DATABASE_URL: cG9zdGdyZXM6Ly91c2VyOnBhc3NAZGI6NTQzMi9teWFwcA==
API_KEY: c2stbGl2ZS1hYmMxMjM=
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: my-app
spec:
template:
spec:
containers:
- name: app
image: my-app:1.0
envFrom:
- secretRef:
name: app-secretsTo select a single key from a Secret instead of importing everything:
env:
- name: DATABASE_URL
valueFrom:
secretKeyRef:
name: app-secrets
key: DATABASE_URLKubernetes decodes the base64 values automatically before injecting them into the container. Your application receives the plaintext value.
How do you expose pod metadata using the downward API?
The fieldRef mechanism exposes pod-level metadata as environment variables. This is called the downward API because it passes cluster information down into the container.
env:
- name: POD_NAME
valueFrom:
fieldRef:
fieldPath: metadata.name
- name: POD_NAMESPACE
valueFrom:
fieldRef:
fieldPath: metadata.namespace
- name: POD_IP
valueFrom:
fieldRef:
fieldPath: status.podIP
- name: NODE_NAME
valueFrom:
fieldRef:
fieldPath: spec.nodeName
- name: SERVICE_ACCOUNT
valueFrom:
fieldRef:
fieldPath: spec.serviceAccountNameThese values are useful for logging, tracing, and service discovery. The pod name and namespace are especially common in observability stacks where you want to correlate logs with specific pod instances.
How do you expose CPU and memory limits to a container?
The resourceFieldRef field exposes resource requests and limits. This is critical for runtimes that need to know their memory ceiling, such as the JVM or Node.js.
containers:
- name: app
image: my-app:1.0
resources:
requests:
memory: "256Mi"
cpu: "250m"
limits:
memory: "512Mi"
cpu: "500m"
env:
- name: MEMORY_LIMIT
valueFrom:
resourceFieldRef:
containerName: app
resource: limits.memory
divisor: "1Mi"
- name: CPU_LIMIT
valueFrom:
resourceFieldRef:
containerName: app
resource: limits.cpu
divisor: "1m"The divisor field controls the unit of the output value. Without it, memory is returned in bytes and CPU in cores. Setting divisor: "1Mi" returns megabytes, and divisor: "1m" returns millicores.
How does variable ordering and dependency work?
Kubernetes evaluates env entries in the order they appear. A variable can reference a previously defined variable using $(VAR_NAME) syntax.
env:
- name: DB_HOST
value: "postgres.default.svc.cluster.local"
- name: DB_PORT
value: "5432"
- name: DB_NAME
value: "myapp"
- name: DATABASE_URL
value: "postgres://$(DB_HOST):$(DB_PORT)/$(DB_NAME)"The DATABASE_URL resolves to postgres://postgres.default.svc.cluster.local:5432/myapp. If you reference a variable that has not been defined yet (or is loaded from a ConfigMap via envFrom), the reference is left as a literal string. Variables from envFrom are loaded before inline env entries, so inline entries can override envFrom values but cannot reference them using the $() syntax.
When should you use Secrets vs ConfigMaps?
Use each for its intended purpose:
| Use ConfigMaps for | Use Secrets for |
|---|---|
| Feature flags and toggles | Database credentials |
| Application configuration (log levels, timeouts) | API keys and tokens |
| Service URLs and endpoints | TLS certificates and private keys |
| Configuration files (nginx.conf, app.properties) | OAuth client secrets |
Secrets are base64-encoded by default, which is not encryption. Anyone with RBAC access to read Secrets in a namespace can decode them — the same env var security rules that apply outside the cluster apply here. For real encryption at rest, enable EncryptionConfiguration on the API server or use an external secrets operator.
What are the best practices for managing secrets in Kubernetes?
Native Kubernetes Secrets have limited security guarantees. Production clusters should use an external secrets management solution. For team workflows around distributing the underlying credentials, see sharing env files securely.
- External Secrets Operator (ESO) syncs secrets from AWS Secrets Manager, GCP Secret Manager, Azure Key Vault, or HashiCorp Vault into Kubernetes Secrets automatically.
- Sealed Secrets by Bitnami encrypts Secrets client-side so you can safely commit them to Git. Only the cluster controller can decrypt them.
- SOPS with age or KMS encrypts secret YAML files at rest in your repository. Decrypt during CI/CD before applying — see the GitHub Actions CI/CD guide for runner-side decryption patterns.
- RBAC restrictions — limit who can
getorlistSecrets at the namespace level. Avoid granting broadcluster-adminaccess. - Enable encryption at rest in etcd via
EncryptionConfiguration. Without this, Secrets are stored as plaintext base64 in etcd.
# ExternalSecret (External Secrets Operator)
apiVersion: external-secrets.io/v1beta1
kind: ExternalSecret
metadata:
name: app-secrets
spec:
refreshInterval: 1h
secretStoreRef:
name: aws-secrets-manager
kind: ClusterSecretStore
target:
name: app-secrets
data:
- secretKey: DATABASE_URL
remoteRef:
key: prod/my-app/database-url
- secretKey: API_KEY
remoteRef:
key: prod/my-app/api-keyWhat are the most common mistakes?
# WRONG — double-encoded because the value already had a newline
data:
PASSWORD: cGFzc3dvcmQK # echo "password" | base64 (includes trailing newline)
# RIGHT — use echo -n to avoid the trailing newline
data:
PASSWORD: cGFzc3dvcmQ= # echo -n "password" | base64
# OR use stringData to let Kubernetes handle encoding
stringData:
PASSWORD: "password"Updating a ConfigMap or Secret does not automatically restart pods that reference it via envFrom or valueFrom. Environment variables are set at container start and never refreshed. This is asymmetric: the Kubernetes docs state that a ConfigMap consumed in a volume is eventually re-projected on the next kubelet periodic sync, but “ConfigMaps consumed as environment variables are not updated automatically and require a pod restart”. So if you need live config reloads, mount the ConfigMap as a file and have the app watch it — env injection is start-time only. Otherwise restart the pods manually or use a tool like stakater/Reloader to trigger rolling restarts when referenced resources change.
# Trigger a rollout restart after ConfigMap changes
# kubectl rollout restart deployment/my-app
# Or use Reloader annotations
metadata:
annotations:
reloader.stakater.com/auto: "true"YAML interprets unquoted true, false, and numbers as their native types. Always quote environment variable values in the value field to avoid type errors.
When you load multiple ConfigMaps or Secrets via envFrom, duplicate keys silently overwrite each other. The last envFrom entry wins, and inline env entries override everything.
A ConfigMap is not designed to hold large blobs — its data cannot exceed 1 MiB, a hard limit rooted in the default etcd request size. Teams that stuff a whole properties file or a baked-in dataset into one ConfigMap hit this wall and see the object rejected on apply. Store anything bigger in a mounted volume, a file service, or a database, and keep ConfigMaps to genuine key-value config.
Frequently Asked Questions
What is the difference between ConfigMaps and Secrets in Kubernetes?
ConfigMaps store non-sensitive configuration data as plain text key-value pairs. Secrets store sensitive data (passwords, tokens, keys) as base64-encoded values with additional access controls. Use ConfigMaps for feature flags and URLs, Secrets for credentials.
Do I need to restart pods after changing a ConfigMap?
Yes, if the ConfigMap is injected as environment variables. Kubernetes does not automatically restart pods when ConfigMaps change. Use kubectl rollout restart deployment/name or a config reloader like stakater/Reloader. A ConfigMap mounted as a volume does eventually re-project on the next kubelet sync, but env-injected values never refresh.
How do I pass pod metadata as environment variables?
Use the downward API with valueFrom.fieldRef. You can expose the pod name (metadata.name), namespace (metadata.namespace), node name (spec.nodeName), pod IP (status.podIP), and labels/annotations.
Can I reference a ConfigMap key in a dependent variable?
No. The $(VAR_NAME) syntax only works with variables defined earlier in the same env list. Variables loaded via envFrom cannot be referenced this way. To compose a URL from ConfigMap values, use an init container or entrypoint script.
Should I use stringData or data in Secrets?
Use stringData for convenience during development — it accepts plaintext and Kubernetes encodes it automatically. In production pipelines, the stored Secret always uses data with base64 values. Both are equivalent at runtime.
What happens if a referenced ConfigMap or Secret does not exist?
The pod will fail to start with a CreateContainerConfigError. To make a reference optional, set optional: true on the configMapRef or secretRef.