env.dev

Kubernetes Env Variables: ConfigMaps, Secrets & Pod Spec

How to configure environment variables in Kubernetes: inline env, ConfigMaps, Secrets, the downward API, and best practices for managing configuration at scale.

By env.dev Updated

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 env for a few static values, envFrom to bulk-import a ConfigMap or Secret, and valueFrom for 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 envFrom first, inline env last — inline entries override bulk imports, but $(VAR) interpolation only resolves earlier inline vars, never envFrom keys.

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.

yaml
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.

yaml
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-config

Every 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.

yaml
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-secrets

To select a single key from a Secret instead of importing everything:

yaml
env:
  - name: DATABASE_URL
    valueFrom:
      secretKeyRef:
        name: app-secrets
        key: DATABASE_URL

Kubernetes 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.

yaml
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.serviceAccountName

These 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.

yaml
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.

yaml
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 forUse Secrets for
Feature flags and togglesDatabase credentials
Application configuration (log levels, timeouts)API keys and tokens
Service URLs and endpointsTLS 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 get or list Secrets at the namespace level. Avoid granting broad cluster-admin access.
  • Enable encryption at rest in etcd via EncryptionConfiguration. Without this, Secrets are stored as plaintext base64 in etcd.
yaml
# 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-key

What are the most common mistakes?

Incorrect base64 encoding in Secrets:
yaml
# 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"
Pods not picking up ConfigMap or Secret changes:

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.

yaml
# Trigger a rollout restart after ConfigMap changes
# kubectl rollout restart deployment/my-app

# Or use Reloader annotations
metadata:
  annotations:
    reloader.stakater.com/auto: "true"
Forgetting that values must be strings:

YAML interprets unquoted true, false, and numbers as their native types. Always quote environment variable values in the value field to avoid type errors.

Using envFrom without checking for key conflicts:

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.

Hitting the 1 MiB ConfigMap ceiling:

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.

References

Was this helpful?

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.

Stay up to date

Get notified about new guides, tools, and cheatsheets.