| 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:
kubectl create namespace container-config-demoSample output:
namespace/container-config-demo createdOverride 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:
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: NeverApply the manifest:
kubectl apply -f cmd-only.yamlSample output:
pod/cmd-only createdWait for the one-shot Pod to finish:
kubectl wait --for=jsonpath='{.status.phase}'=Succeeded pod/cmd-only -n container-config-demo --timeout=60sSample output:
pod/cmd-only condition metThe log should show the container hostname. For this Pod, that value matches the Pod name.
kubectl logs cmd-only -n container-config-demoSample output:
cmd-onlycommand 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.
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: NeverApply the Pod:
kubectl apply -f args-only.yamlSample output:
pod/args-only createdWait for the Pod to finish:
kubectl wait --for=jsonpath='{.status.phase}'=Succeeded pod/args-only -n container-config-demo --timeout=60sSample output:
pod/args-only condition metThe log should print args-only-demo. That shows your args replaced the image default CMD.
kubectl logs args-only -n container-config-demoSample output:
args-only-demoCompare 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:
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: NeverApply the manifest:
kubectl apply -f sleep-demo.yamlSample output:
pod/sleep-demo createdWait until the Pod is ready:
kubectl wait --for=condition=Ready pod/sleep-demo -n container-config-demo --timeout=60sSample output:
pod/sleep-demo condition metConfirm the Pod stays running:
kubectl get pod sleep-demo -n container-config-demoSample 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.
command: ["/bin/sh"]
args:
- -c
- |
echo "Starting application"
exec /usr/local/bin/applicationUse 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:
env:
- name: APP_MODE
value: productionA later entry can reference an earlier one with Kubernetes substitution:
- name: MESSAGE
value: "$(APP_MODE)-service"env entries are processed in list order. $(APP_MODE) resolves because APP_MODE appears first:
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: NeverApply the Pod:
kubectl apply -f app-mode.yamlSample output:
pod/app-mode createdWait for the Pod to finish:
kubectl wait --for=jsonpath='{.status.phase}'=Succeeded pod/app-mode -n container-config-demo --timeout=60sSample output:
pod/app-mode condition metThe log should print production-service:
kubectl logs app-mode -n container-config-demoSample output:
production-serviceDo 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).
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: NeverApply the Pod:
kubectl apply -f k8s-subst.yamlSample output:
pod/k8s-subst createdWait for the Pod to finish:
kubectl wait --for=jsonpath='{.status.phase}'=Succeeded pod/k8s-subst -n container-config-demo --timeout=60sSample output:
pod/k8s-subst condition metThe log should print the resolved value, not the literal $(MESSAGE).
kubectl logs k8s-subst -n container-config-demoSample output:
substitutedIf 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:
env:
- name: DATABASE_HOST
valueFrom:
configMapKeyRef:
name: app-config
key: database-hostExample importing every Secret key with a prefix:
envFrom:
- prefix: SECRET_
secretRef:
name: app-secretThe 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.fieldReforvalueFrom.resourceFieldRef - Files on a
downwardAPIvolume
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:
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:
kubectl explain pod.spec.containers.env.valueFrom.fieldRef.fieldPathLabel 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:
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: 1MicontainerName 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:
volumes:
- name: pod-info
downwardAPI:
items:
- path: labels
fieldRef:
fieldPath: metadata.labels
- path: annotations
fieldRef:
fieldPath: metadata.annotations
- path: pod-name
fieldRef:
fieldPath: metadata.nameMount the volume read-only:
volumeMounts:
- name: pod-info
mountPath: /etc/podinfo
readOnly: trueThe 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:
kubectl create configmap app-config -n container-config-demo --from-literal=database-host=db.internalSample output:
configmap/app-config createdCreate the Secret the Deployment references for secretKeyRef:
kubectl create secret generic app-secret -n container-config-demo --from-literal=api-token=lab-token-123Sample output:
secret/app-secret createdSave web-deployment.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.nameApply the Deployment and wait for the rollout:
kubectl apply -f web-deployment.yamlSample output:
deployment.apps/web createdWait for the new Pod to become ready:
kubectl rollout status deployment/web -n container-config-demo --timeout=90sSample output:
deployment "web" successfully rolled outCheck the container logs for substituted values and resource fields:
kubectl logs deploy/web -n container-config-demoYour 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:
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=128MiConfirm individual variables inside the running container:
kubectl exec deploy/web -n container-config-demo -- printenv MESSAGE DATABASE_HOST API_TOKENSample output:
production-ready
db.internal
lab-token-123Read the projected Downward API files:
kubectl exec deploy/web -n container-config-demo -- cat /etc/podinfo/labelsSample output:
app="web"
pod-template-hash="<generated-hash>"
tier="frontend"The Deployment controller adds the pod-template-hash label automatically.
kubectl exec deploy/web -n container-config-demo -- cat /etc/podinfo/annotationsSample output:
example.com/owner="platform-team"kubectl exec deploy/web -n container-config-demo -- cat /etc/podinfo/pod-nameSample 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:
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: NeverApply the Pod:
kubectl apply -f bad-command.yamlSample output:
pod/bad-command createdWatch until the failure appears, then press Ctrl+C:
kubectl get pod bad-command -n container-config-demo --watchPress 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:
kubectl describe pod bad-command -n container-config-demoSample output (trimmed):
Warning Failed kubelet Error: failed to create containerd task: ... exec: "/bin/missing-binary": stat /bin/missing-binary: no such file or directoryWhat's Next
- Kubernetes Requests, Limits and QoS Classes
- Kubernetes ResourceQuota and LimitRange with Examples
- Kubernetes Authentication, Authorization and Admission Control
References
- Define a Command and Arguments for a Container — Kubernetes documentation
- Define Environment Variables for a Container — official task guide
- Define Dependent Environment Variables —
$(VAR)ordering inenv - Expose Pod Information to Containers Through Files — Downward API volume task guide
- Downward API — Pod metadata injection
- Container environment variables — runtime environment reference
- Pod API — immutability, expansion, and
envFromprecedence - BusyBox Official Image — image behavior used by the args-only demonstration
- Resize CPU and Memory Resources assigned to Containers —
resourceFieldRefupdate behavior after in-place resizing
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?
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?
subPath.6. How do I use environment variables inside command or args?
$(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.
