Kubernetes command and args, Environment Variables and Downward API

Learn Kubernetes command and args overrides, environment variables, ConfigMap and Secret references, and Downward API fields with practical examples.

Published

Updated

Read time 14 min read

Reviewed byDeepak Prasad

Kubernetes container command args environment variables and Downward API configuration
Tested on Rocky Linux 10.2 (Red Quartz) workstation
Package kubectl 1.36.3
Applies to Ubuntu, Debian, Kali Linux, Linux Mint, Pop!_OS, Raspberry Pi OS, elementary OS, Zorin OS, Parrot OS, MX Linux, RHEL, Rocky Linux, AlmaLinux, Oracle Linux, CentOS Stream, Fedora
Kubernetes cluster
Cert prep CKAD
Lab environment Multi-node kubeadm cluster with containerd — install Kubernetes with kubeadm
Privilege Normal user (no sudo required on the workstation)
Scope Container command and args, literal and imported environment variables, Kubernetes $(VAR) substitution versus shell expansion, ConfigMap and Secret references, Downward API through fieldRef and resourceFieldRef, Downward API volumes, an integrated Deployment example, and first-line troubleshooting. Does not cover full ConfigMap or Secret tutorials, ConfigMap or Secret volume mounts, or detailed update-propagation behaviour.

A Pod specification—or a controller's Pod template—controls which process starts inside each container, what arguments it receives, and which configuration values are available at runtime. This guide works through that startup sequence in container-config-demo: image defaults, Kubernetes overrides, environment construction, and Downward API injection.

kubectl logs and kubectl exec use separate Pod subresources. A normal workstation user without sudo can still lack the cluster permissions those commands need.


How Kubernetes Starts a Container

Container images define a default startup through ENTRYPOINT and CMD. The Pod spec maps those concepts to Kubernetes fields:

Container image Kubernetes Pod specification
ENTRYPOINT command
CMD args

Four combinations cover every override case:

Pod fields supplied Result
Neither Use image ENTRYPOINT and CMD
Only args Keep the image ENTRYPOINT when one exists and replace image CMD. If the image has no ENTRYPOINT, the supplied args form the process command
Only command Run the supplied command; image CMD is ignored
Both Run the supplied command with the supplied args

For how image defaults are built, see Build and push container images. The Kubernetes Pod lifecycle article covers Pod phases and restart behaviour when the started process exits.

Create the lab namespace:

bash
kubectl create namespace container-config-demo

Sample output:

output
namespace/container-config-demo created

Override command

Setting command replaces the image entrypoint. The kubelet runs the executable directly. It does not invoke a shell unless you list one.

This Pod runs printenv with a fixed argument instead of the busybox default:

yaml
apiVersion: v1
kind: Pod
metadata:
  name: cmd-only
  namespace: container-config-demo
spec:
  containers:
  - name: demo
    image: busybox:1.36.1
    command: ["printenv", "HOSTNAME"]
  restartPolicy: Never

Apply the manifest:

bash
kubectl apply -f cmd-only.yaml

Sample output:

output
pod/cmd-only created

Wait for the one-shot Pod to finish:

bash
kubectl wait --for=jsonpath='{.status.phase}'=Succeeded pod/cmd-only -n container-config-demo --timeout=60s

Sample output:

output
pod/cmd-only condition met

The log should show the container hostname. For this Pod, that value matches the Pod name.

bash
kubectl logs cmd-only -n container-config-demo

Sample output:

output
cmd-only

command is a JSON array. Each element is one argument to execve, and the executable must exist in the image. You cannot change command, args, env, or envFrom on a running Pod. Delete and recreate a standalone Pod, or update .spec.template on a Deployment so it rolls out replacement Pods.

Override args

The official BusyBox image has no ENTRYPOINT and defaults to CMD ["sh"]. When you set only args, Kubernetes replaces that CMD. This Pod therefore runs echo args-only-demo directly.

yaml
apiVersion: v1
kind: Pod
metadata:
  name: args-only
  namespace: container-config-demo
spec:
  containers:
  - name: demo
    image: busybox:1.36.1
    args: ["echo", "args-only-demo"]
  restartPolicy: Never

Apply the Pod:

bash
kubectl apply -f args-only.yaml

Sample output:

output
pod/args-only created

Wait for the Pod to finish:

bash
kubectl wait --for=jsonpath='{.status.phase}'=Succeeded pod/args-only -n container-config-demo --timeout=60s

Sample output:

output
pod/args-only condition met

The log should print args-only-demo. That shows your args replaced the image default CMD.

bash
kubectl logs args-only -n container-config-demo

Sample output:

output
args-only-demo

Compare the two Pods above. command: ["printenv", "HOSTNAME"] runs printenv directly. args: ["echo", "..."] replaced BusyBox’s default CMD ["sh"] and ran echo instead. Know how your image defines ENTRYPOINT and CMD, or override command when you need a specific binary.

When you need both fields, split entrypoint and arguments:

yaml
apiVersion: v1
kind: Pod
metadata:
  name: sleep-demo
  namespace: container-config-demo
spec:
  containers:
  - name: demo
    image: busybox:1.36.1
    command: ["sleep"]
    args: ["3600"]
  restartPolicy: Never

Apply the manifest:

bash
kubectl apply -f sleep-demo.yaml

Sample output:

output
pod/sleep-demo created

Wait until the Pod is ready:

bash
kubectl wait --for=condition=Ready pod/sleep-demo -n container-config-demo --timeout=60s

Sample output:

output
pod/sleep-demo condition met

Confirm the Pod stays running:

bash
kubectl get pod sleep-demo -n container-config-demo

Sample output:

output
NAME         READY   STATUS    RESTARTS   AGE
sleep-demo   1/1     Running   0          <age>

The AGE column reflects when you ran the command.

Run Shell Expressions

Pipes, redirects, variable expansion, and compound commands require an explicit shell. Kubernetes does not wrap your command in /bin/sh automatically.

yaml
command: ["/bin/sh"]
args:
- -c
- |
  echo "Starting application"
  exec /usr/local/bin/application

Use exec for the final long-running process so PID 1 receives termination signals directly. Minimal or distroless images may not contain /bin/sh. Check the image before you rely on shell syntax.

Two expansion mechanisms look similar but run at different stages:

Syntax Processed by Typical use
$(VAR_NAME) in command or args Kubernetes before the container starts Expand from the completed container environment
$VAR_NAME or ${VAR_NAME} inside a shell script /bin/sh at runtime Logic inside sh -c scripts

Configure Environment Variables

Literal and Dependent Variables

Static values use env with a plain value:

yaml
env:
- name: APP_MODE
  value: production

A later entry can reference an earlier one with Kubernetes substitution:

yaml
- name: MESSAGE
  value: "$(APP_MODE)-service"

env entries are processed in list order. $(APP_MODE) resolves because APP_MODE appears first:

yaml
apiVersion: v1
kind: Pod
metadata:
  name: app-mode
  namespace: container-config-demo
spec:
  containers:
  - name: demo
    image: busybox:1.36.1
    command: ["printenv", "MESSAGE"]
    env:
    - name: APP_MODE
      value: production
    - name: MESSAGE
      value: "$(APP_MODE)-service"
  restartPolicy: Never

Apply the Pod:

bash
kubectl apply -f app-mode.yaml

Sample output:

output
pod/app-mode created

Wait for the Pod to finish:

bash
kubectl wait --for=jsonpath='{.status.phase}'=Succeeded pod/app-mode -n container-config-demo --timeout=60s

Sample output:

output
pod/app-mode condition met

The log should print production-service:

bash
kubectl logs app-mode -n container-config-demo

Sample output:

output
production-service

Do not confuse $(APP_MODE) with shell $APP_MODE. Kubernetes resolves $(APP_MODE) before the process starts.

Variables in command and args

Use $(VAR_NAME) in command or args and Kubernetes expands it from the completed container environment. When one env[].value references another env entry, list the referenced entry first. Use $$(VAR_NAME) when you need the literal text $(VAR_NAME).

yaml
apiVersion: v1
kind: Pod
metadata:
  name: k8s-subst
  namespace: container-config-demo
spec:
  containers:
  - name: demo
    image: busybox:1.36.1
    command: ["/bin/echo"]
    args: ["$(MESSAGE)"]
    env:
    - name: MESSAGE
      value: substituted
  restartPolicy: Never

Apply the Pod:

bash
kubectl apply -f k8s-subst.yaml

Sample output:

output
pod/k8s-subst created

Wait for the Pod to finish:

bash
kubectl wait --for=jsonpath='{.status.phase}'=Succeeded pod/k8s-subst -n container-config-demo --timeout=60s

Sample output:

output
pod/k8s-subst condition met

The log should print the resolved value, not the literal $(MESSAGE).

bash
kubectl logs k8s-subst -n container-config-demo

Sample output:

output
substituted

If Kubernetes cannot resolve $(NAME), the reference stays in the final string as literal text. It does not become an empty value. When one env[].value depends on another entry, define the source first. Load valueFrom fields before you build dependent value strings.

ConfigMap and Secret References

This section shows how to reference ConfigMaps and Secrets. For creation steps, see Kubernetes ConfigMaps and Kubernetes Secrets.

Mechanism Syntax
One ConfigMap key env[].valueFrom.configMapKeyRef
One Secret key env[].valueFrom.secretKeyRef
All ConfigMap keys envFrom[].configMapRef
All Secret keys envFrom[].secretRef

Example pulling one key:

yaml
env:
- name: DATABASE_HOST
  valueFrom:
    configMapKeyRef:
      name: app-config
      key: database-host

Example importing every Secret key with a prefix:

yaml
envFrom:
- prefix: SECRET_
  secretRef:
    name: app-secret

The ConfigMap or Secret must exist in the same namespace before the Pod starts, unless you mark the reference optional: true.

When the same key appears in multiple envFrom sources, the last source wins. An explicit env entry overrides the same key from envFrom.


Use the Downward API

The Downward API exposes selected Pod and container fields to a process without calling the Kubernetes API from inside the container. Two delivery methods exist:

  • Environment variables through valueFrom.fieldRef or valueFrom.resourceFieldRef
  • Files on a downwardAPI volume

Environment variables are set once at container start. Volume files can refresh when Pod metadata changes. The same timing rules apply to ConfigMap and Secret env vars versus mounted data, as described in how ConfigMap and Secret updates reach running Pods.

Inject Pod Fields with fieldRef

fieldRef reads Pod metadata into an environment variable:

yaml
env:
- name: POD_NAME
  valueFrom:
    fieldRef:
      fieldPath: metadata.name
- name: POD_NAMESPACE
  valueFrom:
    fieldRef:
      fieldPath: metadata.namespace
- name: POD_UID
  valueFrom:
    fieldRef:
      fieldPath: metadata.uid
- name: POD_IP
  valueFrom:
    fieldRef:
      fieldPath: status.podIP
- name: NODE_NAME
  valueFrom:
    fieldRef:
      fieldPath: spec.nodeName
- name: APP_LABEL
  valueFrom:
    fieldRef:
      fieldPath: metadata.labels['app']
- name: OWNER
  valueFrom:
    fieldRef:
      fieldPath: metadata.annotations['example.com/owner']

Common paths and where they work:

fieldPath Environment variable Downward API volume
metadata.name Yes Yes
metadata.namespace Yes Yes
metadata.uid Yes Yes
metadata.labels['key'] Yes Yes
metadata.labels No Yes
metadata.annotations['key'] Yes Yes
metadata.annotations No Yes
status.podIP Yes No
spec.nodeName Yes No
spec.serviceAccountName Yes No

Check supported paths with kubectl explain while you write YAML:

bash
kubectl explain pod.spec.containers.env.valueFrom.fieldRef.fieldPath

Label and annotation keys with / or . must use bracket notation exactly as stored on the Pod.

Inject Resources with resourceFieldRef

resourceFieldRef exposes resource requests or limits from the same Pod spec:

yaml
resources:
  requests:
    cpu: 100m
    memory: 64Mi
  limits:
    cpu: 200m
    memory: 128Mi
env:
- name: CPU_REQUEST
  valueFrom:
    resourceFieldRef:
      containerName: web
      resource: requests.cpu
      divisor: 1m
- name: MEMORY_LIMIT
  valueFrom:
    resourceFieldRef:
      containerName: web
      resource: limits.memory
      divisor: 1Mi

containerName is optional for environment-variable resourceFieldRef on the same container, but required when exposing resources through a Downward API volume. divisor controls the unit (1m for millicores, 1Mi for mebibytes). Request and limit design belongs in Kubernetes resource requests and limits.

If you omit CPU or memory limits, the exposed limit can fall back to the node’s allocatable CPU or memory instead of zero. After an in-place resource resize, a resourceFieldRef volume can update, but the matching environment variable stays fixed until the container restarts.

Expose Metadata as Files

When metadata contains a variable number of entries, a Downward API volume is more practical than listing each key in env:

yaml
volumes:
- name: pod-info
  downwardAPI:
    items:
    - path: labels
      fieldRef:
        fieldPath: metadata.labels
    - path: annotations
      fieldRef:
        fieldPath: metadata.annotations
    - path: pod-name
      fieldRef:
        fieldPath: metadata.name

Mount the volume read-only:

yaml
volumeMounts:
- name: pod-info
  mountPath: /etc/podinfo
  readOnly: true

The kubelet can refresh these files when labels or annotations change. Environment variables remain fixed for each container instance. A restarted container resolves them again. For a Deployment configuration change, update the Pod template or start a new rollout so Kubernetes creates replacement Pods.

Downward API environment variables do not update in place, while corresponding volume data can refresh.


Build an Integrated Deployment Example

The Deployment below ties the patterns together: shell startup, literal and dependent env vars, ConfigMap and Secret references, Downward API fields, resource limits, and a Downward API volume.

Create the objects the Deployment references:

bash
kubectl create configmap app-config -n container-config-demo --from-literal=database-host=db.internal

Sample output:

output
configmap/app-config created

Create the Secret the Deployment references for secretKeyRef:

bash
kubectl create secret generic app-secret -n container-config-demo --from-literal=api-token=lab-token-123

Sample output:

output
secret/app-secret created

Save web-deployment.yaml:

yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: web
  namespace: container-config-demo
spec:
  replicas: 1
  selector:
    matchLabels:
      app: web
  template:
    metadata:
      labels:
        app: web
        tier: frontend
      annotations:
        example.com/owner: platform-team
    spec:
      containers:
      - name: web
        image: busybox:1.36.1
        command: ["sh", "-c"]
        args:
        - |
          echo "POD=${POD_NAME} NS=${POD_NAMESPACE} MSG=${MESSAGE} HOST=${DATABASE_HOST}"
          echo "UID=${POD_UID} IP=${POD_IP} NODE=${NODE_NAME} LABEL=${APP_LABEL} OWNER=${OWNER}"
          echo "CPU request=${CPU_REQUEST}m mem limit=${MEMORY_LIMIT}Mi"
          exec sleep 3600
        env:
        - name: APP_MODE
          value: production
        - name: MESSAGE
          value: "$(APP_MODE)-ready"
        - name: POD_NAME
          valueFrom:
            fieldRef:
              fieldPath: metadata.name
        - name: POD_NAMESPACE
          valueFrom:
            fieldRef:
              fieldPath: metadata.namespace
        - name: POD_UID
          valueFrom:
            fieldRef:
              fieldPath: metadata.uid
        - name: POD_IP
          valueFrom:
            fieldRef:
              fieldPath: status.podIP
        - name: NODE_NAME
          valueFrom:
            fieldRef:
              fieldPath: spec.nodeName
        - name: APP_LABEL
          valueFrom:
            fieldRef:
              fieldPath: metadata.labels['app']
        - name: OWNER
          valueFrom:
            fieldRef:
              fieldPath: metadata.annotations['example.com/owner']
        - name: DATABASE_HOST
          valueFrom:
            configMapKeyRef:
              name: app-config
              key: database-host
        - name: API_TOKEN
          valueFrom:
            secretKeyRef:
              name: app-secret
              key: api-token
        - name: CPU_REQUEST
          valueFrom:
            resourceFieldRef:
              containerName: web
              resource: requests.cpu
              divisor: 1m
        - name: MEMORY_LIMIT
          valueFrom:
            resourceFieldRef:
              containerName: web
              resource: limits.memory
              divisor: 1Mi
        resources:
          requests:
            cpu: 100m
            memory: 64Mi
          limits:
            cpu: 200m
            memory: 128Mi
        volumeMounts:
        - name: pod-info
          mountPath: /etc/podinfo
          readOnly: true
      volumes:
      - name: pod-info
        downwardAPI:
          items:
          - path: labels
            fieldRef:
              fieldPath: metadata.labels
          - path: annotations
            fieldRef:
              fieldPath: metadata.annotations
          - path: pod-name
            fieldRef:
              fieldPath: metadata.name

Apply the Deployment and wait for the rollout:

bash
kubectl apply -f web-deployment.yaml

Sample output:

output
deployment.apps/web created

Wait for the new Pod to become ready:

bash
kubectl rollout status deployment/web -n container-config-demo --timeout=90s

Sample output:

output
deployment "web" successfully rolled out

Check the container logs for substituted values and resource fields:

bash
kubectl logs deploy/web -n container-config-demo

Your cluster will not match these lines exactly. Pod names include a ReplicaSet hash and a random suffix. Admission controllers may add extra labels or annotations, and file order can differ. Each labels or annotations file shows one admitted metadata entry per line.

Sample output:

output
POD=web-<replicaset-hash>-<pod-suffix> NS=container-config-demo MSG=production-ready HOST=db.internal
UID=<pod-uid> IP=<pod-ip> NODE=<node-name> LABEL=web OWNER=platform-team
CPU request=100m mem limit=128Mi

Confirm individual variables inside the running container:

bash
kubectl exec deploy/web -n container-config-demo -- printenv MESSAGE DATABASE_HOST API_TOKEN

Sample output:

output
production-ready
db.internal
lab-token-123

Read the projected Downward API files:

bash
kubectl exec deploy/web -n container-config-demo -- cat /etc/podinfo/labels

Sample output:

output
app="web"
pod-template-hash="<generated-hash>"
tier="frontend"

The Deployment controller adds the pod-template-hash label automatically.

bash
kubectl exec deploy/web -n container-config-demo -- cat /etc/podinfo/annotations

Sample output:

output
example.com/owner="platform-team"
bash
kubectl exec deploy/web -n container-config-demo -- cat /etc/podinfo/pod-name

Sample output:

output
web-<replicaset-hash>-<pod-suffix>

The hash and suffix in the logs and in /etc/podinfo/pod-name will differ on your cluster.


Troubleshoot Container Configuration

Symptom Likely cause Fix
command not found / no such file or directory Executable missing from image or wrong path Verify image contents; use full path in command
Shell syntax fails No /bin/sh in minimal or distroless image Add a shell to the image or avoid shell-only syntax
Variable prints as $(VAR) literally Kubernetes substitution: VAR undefined, or dependent env[].value references an entry defined later in env Define referenced env entries before dependent value strings; use valueFrom sources before building dependent value entries
$VAR prints literally, or a command containing $VAR fails to start Shell expansion syntax used without a shell Use command: ["sh", "-c"] for $VAR, or use Kubernetes $(VAR) substitution
ConfigMap or Secret key missing Wrong name, namespace, or key kubectl describe the source object; check optional: true if absence is acceptable
fieldRef rejected Unsupported or mistyped fieldPath kubectl explain pod.spec.containers.env.valueFrom.fieldRef
Env var unchanged after Secret edit Env vars are fixed at container start Roll out new Pods. See the propagation guide for details
Volume metadata stale in env but fresh on disk Env vars do not refresh; volumes can Restart for env changes; use Downward API volume for file-based metadata

The next Pod points command at a binary that does not exist in the image:

yaml
apiVersion: v1
kind: Pod
metadata:
  name: bad-command
  namespace: container-config-demo
spec:
  containers:
  - name: demo
    image: busybox:1.36.1
    command: ["/bin/missing-binary"]
  restartPolicy: Never

Apply the Pod:

bash
kubectl apply -f bad-command.yaml

Sample output:

output
pod/bad-command created

Watch until the failure appears, then press Ctrl+C:

bash
kubectl get pod bad-command -n container-config-demo --watch

Press Ctrl+C after CreateContainerError or RunContainerError appears. The exact reason and runtime error text can vary by container runtime.

Describe the Pod to read the failure event:

bash
kubectl describe pod bad-command -n container-config-demo

Sample output (trimmed):

output
Warning  Failed  kubelet  Error: failed to create containerd task: ... exec: "/bin/missing-binary": stat /bin/missing-binary: no such file or directory

What's Next


References


Summary

You worked through the container startup sequence from image defaults through Kubernetes overrides. command maps to ENTRYPOINT and args to CMD. Setting only command discards image CMD. Setting only args keeps the image ENTRYPOINT when one exists; if the image has no ENTRYPOINT, the supplied args form the process command. Shell features need an explicit sh -c wrapper because the kubelet never adds a shell for you.

Environment variables are built in list order. $(VAR) in command or args expands from the completed container environment. In env[].value, ordering matters when one entry references another. $VAR inside a shell script is runtime expansion. ConfigMap and Secret keys arrive through configMapKeyRef, secretKeyRef, or envFrom. The Downward API exposes Pod metadata through fieldRef and resource settings through resourceFieldRef, either as env vars fixed at start or as volume files that can refresh when metadata changes.

The integrated web Deployment combined these patterns in one manifest. When startup fails, check Pod events for command not found first, then verify expansion syntax and that referenced ConfigMaps, Secrets, and fieldPath values exist before the Pod is scheduled.


Frequently Asked Questions

1. What is the difference between command and args in Kubernetes?

command overrides the image ENTRYPOINT and args overrides CMD. When both are omitted, the image defaults apply. Setting command alone runs only that executable and discards the image CMD. Setting args alone preserves the image ENTRYPOINT when one exists and replaces CMD. If the image has no ENTRYPOINT, as in the BusyBox example, the supplied args form the process command. Kubernetes defines args as arguments to the entrypoint and uses the image defaults when overrides are absent; the BusyBox-specific behavior depends on that image's startup configuration.

2. Does Kubernetes run container commands through a shell automatically?

No. The kubelet executes the binary you list in command directly. Use command: ["sh", "-c"] when you need pipes, redirects, multiple commands, or shell variable expansion with $VAR.

3. What is the difference between env and envFrom?

env defines one variable at a time with value or valueFrom. envFrom imports every key from a ConfigMap or Secret, optionally with a prefix. Use valueFrom when you need a single key or a Downward API field.

4. What is the difference between fieldRef and resourceFieldRef?

fieldRef reads Pod metadata such as name, namespace, labels, or annotations. resourceFieldRef reads container resource requests or limits from the same Pod spec. containerName is optional on an environment-variable resourceFieldRef, but required when exposing container resources through a Downward API volume.

5. Do Downward API environment variables update while a container is running?

No. Environment variables are set at container start and stay fixed until the container restarts. Downward API volumes can refresh file contents when Pod metadata changes, unless the mount uses subPath.

6. How do I use environment variables inside command or args?

Use $(VAR_NAME) in command or args; Kubernetes expands it from the container environment. Ordering matters when one env[].value references another env entry—the referenced entry must appear earlier in the env list. Use $$(VAR_NAME) when the literal text $(VAR_NAME) is required. Use $VAR_NAME or ${VAR_NAME} only inside a shell script started with sh -c.
Deepak Prasad

R&D Engineer

Founder of GoLinuxCloud with more than 15 years of expertise in Linux, Python, Go, Laravel, DevOps, Kubernetes, Git, Shell scripting, OpenShift, AWS, Networking, and Security. With extensive experience, he excels across development, DevOps, networking, and security, delivering robust and efficient solutions for diverse projects.

  • Go (programming language)
  • Python (programming language)
  • DevOps
  • Computer Security
  • Cloud Computing
  • Kubernetes
  • Linux
  • Ansible (software)