Skip to main content
RunBook Academy

KubernetesVIII · PodsPods

Containers, images, ports, and environment variables

Intermediate⏱ ~18 minkubectl

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

Not yet marked complete on this device.

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 the containerPort on 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 requests based on typical usage; the scheduler uses this for bin-packing.
  • Set limits based 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; use emptyDir volumes 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-Images covers image registries and tags; the same discipline applies at the Pod level.
  • The Linux course part XXIX-Linux-Hardening covers capability dropping and seccomp; the Pod securityContext exposes these primitives.
  • The Ansible course part XLIX-Ansible-Compliance covers configuration compliance; securityContext is the cluster equivalent.

Quiz

Knowledge check · 4 questions

  1. Q1. Which image reference guarantees the kubelet pulls the exact same bytes every time, even if the registry tag is re-pointed?

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

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

  4. 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 containerPort for 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 securityContext on every container. Drop capabilities, run as non-root, read-only root filesystem, default seccomp profile. Production means default-deny.