Kubernetes offers several mechanisms for injecting environment variables into containers: inline env fields, envFrom bulk imports from ConfigMaps and Secrets, and the downward API for exposing pod metadata and resource limits. Choosing the right approach depends on whether the values are sensitive, how many variables you need, and whether they change between environments. This guide covers every method, explains ordering and dependency rules, and highlights the mistakes that cause the most debugging time in production clusters. If you are coming from Docker, see the Docker env variables guide for the natural progression into Kubernetes.
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. 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. You must 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.