Containers, images, ports, and environment variables
What you'll learn
- Configure container images, pull policy, and image pull secrets
- Declare ports and the difference between containerPort, hostPort, and Service port
- Set environment variables from literals, ConfigMaps, and Secrets
- Apply security context at the container level
Prerequisites
Verified against Kubernetes 1.34.x · kubeadm 1.34.x · kubectl 1.34.x · etcd 3.6.x · CoreDNS 1.11.x · containerd 1.7.x / 2.x · 2026-08-16
This lesson drills into spec.containers — the field that
actually describes what runs in a Pod. Every production
workload touches these fields: image, ports, env, security
context. The patterns here are what separate a toy Pod from
a production Pod.
Image and image pull policy
containers:
- name: nginx
image: nginx:1.27.2
imagePullPolicy: IfNotPresent
The image field is <registry>/<repo>:<tag> (or
<registry>/<repo>@<digest>). The kubelet hands the image
reference to the CRI (containerd), which pulls the image
through the standard OCI distribution protocol.
Three pull policies:
Always: pull every time the Pod starts. Slow but guarantees the latest image (within the tag’s mutable semantics).IfNotPresent(default): pull only if the image is not on the node. Fast; assumes the image on the node is the right one.Never: never pull. The image must be pre-loaded on the node.
Image pull secrets
For private registries:
imagePullSecrets:
- name: regcred
regcred is a Secret of type kubernetes.io/dockerconfigjson
in the same namespace. The kubelet uses it to authenticate
to the registry. For cluster-wide auth, configure the CRI’s
runtime with hosts.toml (containerd) — Pods don’t need
imagePullSecrets if the runtime has them built in.
kubectl create secret docker-registry regcred \
--docker-server=registry.example.com \
--docker-username=app \
--docker-password=$REGISTRY_PASSWORD \
--docker-email=ops@example.com
Ports
ports:
- name: http
containerPort: 8080
protocol: TCP
hostPort: 0
Three port concepts:
containerPort: the port the application listens on inside the container. Declaring it is informational — it tells Kubernetes (and Service selectors, network policies) what port the container uses.hostPort: bind the container port to the same port on the host. The Pod becomes reachable on the node’s IP at that port. Rare; usually a mistake (Pods are supposed to be portable).Service port: declared on the Service, not the Pod. Maps an external port to thecontainerPorton the Pods selected by the Service.
flowchart LR
Service["Service :80"] -->|selector| Pod1["Pod: containerPort 8080"]
Service -->|selector| Pod2["Pod: containerPort 8080"]
Service -->|selector| Pod3["Pod: containerPort 8080"]
HostPort["Pod: hostPort 8080"] --> Node["Node IP:8080"]
The rule:
containerPort: declare for documentation and to be discoverable. Required for the Service to know what port to target.hostPort: avoid in production. It ties the Pod to a specific port on a specific node, breaking the cluster’s scheduling flexibility. Only use for host-network services that genuinely need to bind on the host (e.g., a node exporter).- Service port: configure on the Service, not the Pod.
Environment variables
Three sources:
env:
- name: LOG_LEVEL
value: info
- name: DB_HOST
valueFrom:
configMapKeyRef:
name: db-config
key: host
- name: DB_PASSWORD
valueFrom:
secretKeyRef:
name: db-secret
key: password
envFrom:
- configMapRef:
name: app-config
- secretRef:
name: app-secret
value: a literal string. Set at Pod creation; immutable without a Pod restart.valueFrom.configMapKeyRef: pull a single key from a ConfigMap. Updates to the ConfigMap after Pod creation do not propagate (you need a sidecar or a restart).valueFrom.secretKeyRef: pull a single key from a Secret. Same immutability.envFrom: import all keys from a ConfigMap or Secret as environment variables. Convenient; less control.
For dynamic config (config that changes without a restart),
mount the ConfigMap as a volume and read it from the file
system. The kubelet updates the volume’s projected files when
the ConfigMap changes (with a delay — see
spec.containers[*].volumeMounts.subPath caveats).
flowchart LR
CM[ConfigMap] -->|env.valueFrom| EnvVar1["env: LOG_LEVEL=info"]
CM -->|volumeMount| File1["file: /etc/config/log-level"]
File1 -.->|hot reload if app supports it| App[Application]
EnvVar1 -.->|immutable| App
Resources
resources:
requests:
cpu: 100m
memory: 128Mi
limits:
cpu: 500m
memory: 512Mi
requests: what the scheduler reserves. CPU is compressible (throttled under contention); memory is not (the Pod is OOMKilled if it exceeds the node’s memory).limits: the hard ceiling. CPU limit: throttling. Memory limit: OOMKill.
Production discipline:
- Set
requestsbased on typical usage; the scheduler uses this for bin-packing. - Set
limitsbased on worst-case usage; this prevents a single Pod from starving the node. - Memory limits should always be set. A Pod without a memory limit can be OOMKilled at the node level (not QoS-aware).
- CPU limits are debated. Some teams omit them (BestEffort for CPU) to avoid throttling; others set them strictly for predictability.
Part XII covers this in depth.
Security context
securityContext:
runAsNonRoot: true
runAsUser: 1000
runAsGroup: 3000
allowPrivilegeEscalation: false
capabilities:
drop: ["ALL"]
readOnlyRootFilesystem: true
seccompProfile:
type: RuntimeDefault
The security context is the difference between a Pod that can compromise the node and one that cannot. Production discipline:
runAsNonRoot: true— refuse to start if the image would run as root.allowPrivilegeEscalation: false— block setuid binaries.capabilities.drop: ["ALL"]— drop all Linux capabilities; add only what the application needs (rare).readOnlyRootFilesystem: true— make the container’s root filesystem read-only; useemptyDirvolumes for writable paths.seccompProfile.type: RuntimeDefault— use the runtime’s default seccomp filter; further restricts syscalls.
The full container spec
Combining everything:
containers:
- name: nginx
image: nginx@sha256:abc123...
imagePullPolicy: IfNotPresent
ports:
- name: http
containerPort: 8080
protocol: TCP
env:
- name: DB_HOST
valueFrom:
configMapKeyRef:
name: db-config
key: host
resources:
requests:
cpu: 100m
memory: 128Mi
limits:
cpu: 500m
memory: 512Mi
livenessProbe:
httpGet:
path: /healthz
port: http
readinessProbe:
httpGet:
path: /ready
port: http
securityContext:
runAsNonRoot: true
allowPrivilegeEscalation: false
capabilities:
drop: ["ALL"]
readOnlyRootFilesystem: true
volumeMounts:
- name: tmp
mountPath: /tmp
volumes:
- name: tmp
emptyDir: {}
This is the canonical production container. Every field earns its place.
Cross-course references
- The Docker course part
XXVIII-Docker-Imagescovers image registries and tags; the same discipline applies at the Pod level. - The Linux course part
XXIX-Linux-Hardeningcovers capability dropping and seccomp; the Pod securityContext exposes these primitives. - The Ansible course part
XLIX-Ansible-Compliancecovers configuration compliance; securityContext is the cluster equivalent.
Quiz
Knowledge check · 4 questions
Q1. Which image reference guarantees the kubelet pulls the exact same bytes every time, even if the registry tag is re-pointed?
Q2. `hostPort` is the recommended way to expose a Pod to external traffic because it bypasses the Service and connects directly to the node's network stack.
Q3. A team updates a ConfigMap to add a new environment variable that the application needs. They expect the running Pods to pick up the change automatically. Diagnose what happens and propose a fix.
ConfigMap `app-config` is mounted as `envFrom` in the Pod. The team adds `NEW_FEATURE_FLAG=enabled` to the ConfigMap. The application does not see the new environment variable because envFrom is read at Pod start, not dynamically.
Q4. List four fields in securityContext that should be set on every production container, and what each does.
Passing score: 75%. Answers are checked in this browser.
Production discipline
- Pin images by digest, not tag. Tags are mutable; digests are immutable.
- Declare
containerPortfor documentation. It is informational but required for Service selectors and network policies to discover the port. - Avoid
hostPort. It ties Pods to specific nodes and breaks scheduling. Use a Service for traffic; reserve hostPort for host agents. - Mount ConfigMaps as volumes for dynamic config. Env vars are immutable without a restart.
- Set
securityContexton every container. Drop capabilities, run as non-root, read-only root filesystem, default seccomp profile. Production means default-deny.