Skip to Content
CheatsheetsContainers

Containers Cheat Sheet

Docker, Podman, Compose, Kubernetes, and Helm patterns for platform engineers. Covers images, networking, volumes, security, debugging, and production deployment patterns.

Versions: Docker 25+ / BuildKit 0.12+ · Kubernetes 1.29+ · Helm 3.14+ · kubectl 1.29+

Last reviewed: May 2026


Docker

Core container lifecycle

Bash
# Run
docker run -d \
  --name        my-app \
  --restart     unless-stopped \
  -p            8080:8080 \
  -e            APP_ENV=production \
  --memory      512m \
  --cpus        "1.5" \
  --read-only \
  --user        1000:1000 \
  --cap-drop    ALL \
  myregistry.io/my-app:1.2.3
 
# Exec into running container
docker exec -it my-app /bin/sh
 
# Logs
docker logs my-app --follow --tail 100
docker logs my-app --since 10m
 
# Lifecycle
docker stop  my-app && docker rm my-app
docker kill  my-app           # SIGKILL immediately
docker pause my-app           # freeze
docker unpause my-app
 
# Copy files
docker cp my-app:/etc/app/config.yaml ./config.yaml
docker cp ./config.yaml my-app:/etc/app/config.yaml
 
# Inspect
docker inspect my-app
docker inspect --format '{{.NetworkSettings.IPAddress}}' my-app
docker inspect --format '{{json .State.Health}}' my-app | jq
 
# Stats (live)
docker stats --no-stream
docker stats my-app

Images

Bash
# Pull / push
docker pull nginx:1.27-alpine
docker push myregistry.io/my-app:1.2.3
 
# Tag
docker tag myregistry.io/my-app:latest myregistry.io/my-app:1.2.3
 
# List
docker images
docker image ls --filter dangling=true
 
# Remove
docker rmi myregistry.io/my-app:old
docker image prune         # dangling only
docker image prune -a      # all unreferenced
 
# Save / load (for air-gapped transfers)
docker save myregistry.io/my-app:1.2.3 | gzip > my-app-1.2.3.tar.gz
docker load < my-app-1.2.3.tar.gz
 
# Inspect image layers
docker history my-app:latest
docker image inspect my-app:latest --format '{{json .RootFS.Layers}}' | jq
 
# Scan for vulnerabilities (Docker Scout)
docker scout cves my-app:latest
docker scout recommendations my-app:latest

Networking

Bash
# Create networks
docker network create --driver bridge app-net
docker network create --driver overlay --attachable swarm-net
 
# Connect / disconnect
docker network connect  app-net my-app
docker network disconnect app-net my-app
 
# List
docker network ls
docker network inspect app-net
 
# DNS between containers: use container name as hostname on shared network
docker run -d --name db    --network app-net postgres:16
docker run -d --name api   --network app-net -e DB_HOST=db my-api:latest
# api container resolves "db" via Docker DNS
 
# Host networking (no NAT - Linux only)
docker run --network host nginx

Volumes

Bash
# Named volumes
docker volume create my-data
docker volume ls
docker volume inspect my-data
docker volume rm my-data
docker volume prune
 
# Bind mounts
docker run -v /host/path:/container/path:ro my-app
 
# tmpfs (in-memory, not persisted)
docker run --tmpfs /tmp:size=100m,noexec my-app
 
# Backup a volume
docker run --rm \
  -v my-data:/data \
  -v "$(pwd)":/backup \
  alpine tar czf /backup/my-data-backup.tar.gz -C /data .
 
# Restore
docker run --rm \
  -v my-data:/data \
  -v "$(pwd)":/backup \
  alpine sh -c "cd /data && tar xzf /backup/my-data-backup.tar.gz"

System cleanup

Bash
docker system df                    # disk usage
docker system prune                 # stopped containers + dangling images + unused networks
docker system prune -a              # also removes unused images
docker system prune --volumes       # also removes unused volumes
docker system prune -af --volumes   # full nuclear cleanup

Dockerfile Patterns

Multi-stage build (Go)

Dockerfile
# syntax=docker/dockerfile:1
FROM golang:1.23-alpine AS builder
WORKDIR /src
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 GOOS=linux go build -ldflags="-s -w" -o /app ./cmd/server
 
FROM gcr.io/distroless/static-debian12:nonroot
COPY --from=builder /app /app
EXPOSE 8080
ENTRYPOINT ["/app"]

Multi-stage build (Node.js)

Dockerfile
# syntax=docker/dockerfile:1
FROM node:22-alpine AS deps
WORKDIR /app
COPY package*.json ./
RUN npm ci --omit=dev
 
FROM node:22-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
 
FROM node:22-alpine AS runner
WORKDIR /app
ENV NODE_ENV=production
COPY --from=deps    /app/node_modules ./node_modules
COPY --from=builder /app/dist        ./dist
COPY --from=builder /app/package.json .
USER node
EXPOSE 3000
CMD ["node", "dist/index.js"]

Python with uv

Dockerfile
# syntax=docker/dockerfile:1
FROM python:3.12-slim AS builder
RUN pip install uv
WORKDIR /app
COPY pyproject.toml uv.lock ./
RUN --mount=type=cache,target=/root/.cache/uv \
    uv sync --frozen --no-dev
 
FROM python:3.12-slim
WORKDIR /app
COPY --from=builder /app/.venv /app/.venv
COPY src ./src
ENV PATH="/app/.venv/bin:$PATH"
USER nobody
EXPOSE 8000
CMD ["gunicorn", "src.app:app", "-b", "0.0.0.0:8000"]

BuildKit secrets (no secrets in layers) 🔐

Dockerfile
# syntax=docker/dockerfile:1
FROM alpine AS downloader
RUN --mount=type=secret,id=gh_token \
    GH_TOKEN=$(cat /run/secrets/gh_token) \
    curl -H "Authorization: token $GH_TOKEN" \
         -L https://api.github.com/repos/myorg/private/releases/latest \
         -o /app/binary
Bash
docker buildx build \
  --secret id=gh_token,src=$HOME/.config/gh/token \
  -t my-image:latest .

.dockerignore

TEXT
.git
.gitignore
.env
.env.*
**/node_modules
**/__pycache__
**/*.pyc
**/dist
**/build
**/.pytest_cache
**/.mypy_cache
Dockerfile*
docker-compose*.yml
*.md
.github
terraform
tests

Dockerfile best practices

Dockerfile
# syntax=docker/dockerfile:1
 
# Pin base image by digest for reproducibility
FROM python:3.12.4-slim@sha256:abc123... AS base
 
# Combine RUN layers to minimise image size
RUN apt-get update && apt-get install -y --no-install-recommends \
    curl ca-certificates && \
    rm -rf /var/lib/apt/lists/*
 
# Use COPY --chown to avoid a separate chown layer
COPY --chown=nobody:nogroup src/ /app/src/
 
# Drop to non-root before CMD
USER nobody
 
# Use exec form for CMD/ENTRYPOINT (no shell, proper signal handling)
ENTRYPOINT ["/app/server"]
CMD ["--config", "/etc/app/config.yaml"]
 
# Healthcheck
HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 \
  CMD curl -f http://localhost:8080/healthz || exit 1

BuildKit & Multi-Platform

Bash
# Enable BuildKit (default in Docker 23+)
DOCKER_BUILDKIT=1 docker build .
 
# Create a multi-platform builder
docker buildx create --name multiarch --driver docker-container --use
docker buildx inspect --bootstrap
 
# Build for multiple platforms
docker buildx build \
  --platform linux/amd64,linux/arm64 \
  --push \
  -t myregistry.io/my-app:1.2.3 .
 
# Build and load locally (single platform only)
docker buildx build --load --platform linux/amd64 -t my-app:dev .
 
# Bake (declarative multi-target builds)
docker buildx bake --file docker-bake.hcl --push
HCL
# docker-bake.hcl
group "default" {
  targets = ["api", "worker"]
}
 
target "api" {
  context    = "."
  dockerfile = "Dockerfile.api"
  platforms  = ["linux/amd64", "linux/arm64"]
  tags       = ["myregistry.io/api:${GIT_SHA}"]
}
 
target "worker" {
  context    = "."
  dockerfile = "Dockerfile.worker"
  platforms  = ["linux/amd64", "linux/arm64"]
  tags       = ["myregistry.io/worker:${GIT_SHA}"]
}

Docker Compose

Production-grade compose file

YAML
# compose.yaml (v2 format - no top-level "version" key needed)
services:
  api:
    image: myregistry.io/api:${IMAGE_TAG:-latest}
    restart: unless-stopped
    ports:
      - "8080:8080"
    environment:
      - DATABASE_URL=postgresql://app:${DB_PASSWORD}@db:5432/app
      - REDIS_URL=redis://cache:6379
    env_file:
      - .env.local
    depends_on:
      db:
        condition: service_healthy
      cache:
        condition: service_started
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost:8080/healthz"]
      interval: 30s
      timeout:  5s
      retries:  3
      start_period: 15s
    deploy:
      resources:
        limits:
          cpus:   "1"
          memory: 512M
    networks:
      - backend
      - frontend
    read_only: true
    tmpfs:
      - /tmp
 
  db:
    image: postgres:16-alpine
    restart: unless-stopped
    environment:
      POSTGRES_DB:       app
      POSTGRES_USER:     app
      POSTGRES_PASSWORD: ${DB_PASSWORD}
    volumes:
      - db-data:/var/lib/postgresql/data
      - ./init.sql:/docker-entrypoint-initdb.d/init.sql:ro
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U app -d app"]
      interval: 10s
      timeout:  5s
      retries:  5
    networks:
      - backend
 
  cache:
    image: redis:7-alpine
    restart: unless-stopped
    command: redis-server --appendonly yes --maxmemory 256mb --maxmemory-policy allkeys-lru
    volumes:
      - redis-data:/data
    networks:
      - backend
 
volumes:
  db-data:
  redis-data:
 
networks:
  backend:
  frontend:

Compose CLI patterns

Bash
# Start / stop
docker compose up -d
docker compose up -d --build       # rebuild images first
docker compose down
docker compose down -v             # also remove volumes
 
# Scale a service
docker compose up -d --scale worker=4
 
# Logs
docker compose logs -f api
docker compose logs -f --tail 50 api db
 
# Exec
docker compose exec api sh
docker compose exec db psql -U app -d app
 
# Environment override files
docker compose -f compose.yaml -f compose.override.prod.yaml up -d
 
# Watch (auto-rebuild on file changes - compose 2.22+)
docker compose watch
 
# Run one-off command
docker compose run --rm api python manage.py migrate
 
# Config validation
docker compose config
 
# Pull all images
docker compose pull

Compose profiles (selective service startup)

YAML
services:
  api:
    image: myregistry.io/api:latest
    # no profiles key - service always starts regardless of --profile flag
 
  docs:
    image: myregistry.io/docs:latest
    profiles: ["full"]           # only with --profile full
 
  mock-server:
    image: mockserver/mockserver
    profiles: ["dev"]            # only in dev
Bash
docker compose --profile full up -d
docker compose --profile dev  up -d

Podman

Key differences from Docker

Bash
# Rootless by default - no daemon
podman run -d nginx
 
# Pods (shared network namespace)
podman pod create --name app-pod -p 8080:80
podman run -d --pod app-pod --name nginx nginx
podman run -d --pod app-pod --name api   my-api:latest
podman pod start  app-pod
podman pod stop   app-pod
podman pod rm -f  app-pod
 
# Docker CLI compatibility
alias docker=podman

Quadlet (systemd-native containers)

Quadlet (Podman 4.4+) replaces podman generate systemd: you write declarative unit files and a systemd generator turns them into services. Seven types - .container, .pod, .network, .volume, .kube, .build, .image (plus .artifact).

Place them where the generator looks:

  • Rootful (system): /etc/containers/systemd/
  • Rootless (user): ~/.config/containers/systemd/ (or /etc/containers/systemd/users/ for every user)

A name.container generates name.service; the container is named systemd-name unless you set ContainerName=.

INI
# ~/.config/containers/systemd/my-app.container
[Unit]
Description=My App Container
After=network-online.target
 
[Container]
Image=myregistry.io/my-app:latest
ContainerName=my-app
PublishPort=8080:8080
Environment=APP_ENV=production
EnvironmentFile=%h/.env.my-app
Volume=%h/data:/data:Z
AutoUpdate=registry
HealthCmd=curl -f http://localhost:8080/healthz
HealthInterval=30s
 
[Service]
Restart=always
TimeoutStartSec=60
 
[Install]
WantedBy=default.target
Bash
# Reload and start
systemctl --user daemon-reload
systemctl --user enable --now my-app.service
systemctl --user status my-app.service
journalctl --user -u my-app.service -f
 
# Auto-update (requires AutoUpdate=registry in .container)
systemctl --user enable podman-auto-update.timer
podman auto-update --dry-run

Networks, volumes, and secrets - reference other quadlets by unit name:

INI
# my-app.network
[Network]
Subnet=10.89.0.0/24
Gateway=10.89.0.1
 
# my-app.volume  (an empty [Volume] just creates a named volume)
[Volume]
 
# in my-app.container - reference the .network / .volume / a Podman secret by name
[Container]
Network=my-app.network
Volume=my-app.volume:/data:Z
Secret=db-password,type=env,target=DB_PASSWORD     # podman secret create db-password ...

Deploy a Kubernetes YAML as a quadlet (.kube):

INI
# my-app.kube
[Kube]
Yaml=my-app.yaml          # a `podman kube play` manifest in the same directory
AutoUpdate=registry
 
[Install]
WantedBy=default.target

Inspect generated units (Podman 5.x adds the podman quadlet subcommand):

Bash
podman quadlet list                       # installed quadlets and their status
podman quadlet print my-app.container     # show the resolved file
# Debug generation without installing:
/usr/lib/systemd/system-generators/podman-system-generator --dryrun

✅ Quadlets are the production way to run Podman as first-class systemd services - restart policies, ordering, journald logging, and podman-auto-update, with no daemon. Keep secrets in Podman secrets (Secret=) or an EnvironmentFile=, never inline in the unit, and run systemctl [--user] daemon-reload after every edit.

Podman in WSL2

Bash
# Fix shared mount for rootless Podman
sudo sh -c 'echo "mount --make-rshared /" > /etc/profile.d/02-shared-root.sh'
sudo chmod +x /etc/profile.d/02-shared-root.sh
 
# Enable socket (for Docker-compatible tools)
systemctl --user enable --now podman.socket
export DOCKER_HOST=unix://$XDG_RUNTIME_DIR/podman/podman.sock

Kubernetes (kubectl)

Context and namespace

Bash
# Contexts
kubectl config get-contexts
kubectl config use-context aks-myapp-prod-admin
kubectl config current-context
 
# Namespace shorthand (set default)
kubectl config set-context --current --namespace=my-namespace
 
# All namespaces flag
kubectl get pods -A
kubectl get pods --all-namespaces
 
# kubens / kubectx (krew plugin)
kubens my-namespace
kubectx aks-prod

Core resources - get, describe, delete

Bash
kubectl get pods          -n my-namespace -o wide
kubectl get deployments   -n my-namespace
kubectl get services      -n my-namespace
kubectl get ingress        -n my-namespace
kubectl get configmaps    -n my-namespace
kubectl get secrets        -n my-namespace
kubectl get pvc            -n my-namespace
kubectl get nodes          -o wide
kubectl get events         -n my-namespace --sort-by=.lastTimestamp
 
# Describe (full detail + events)
kubectl describe pod my-pod              -n my-namespace
kubectl describe node my-node
kubectl describe deployment my-app       -n my-namespace
 
# Delete
kubectl delete pod       my-pod          -n my-namespace
kubectl delete pod       my-pod          -n my-namespace --grace-period=0 --force
kubectl delete -f        manifest.yaml

Logs and debugging

Bash
# Logs
kubectl logs my-pod          -n my-namespace --follow --tail 100
kubectl logs my-pod          -n my-namespace -c my-container   # multi-container pod
kubectl logs -l app=my-app   -n my-namespace --follow           # label selector
 
# Previous container (if crashed)
kubectl logs my-pod -n my-namespace --previous
 
# Exec
kubectl exec -it my-pod -n my-namespace -- /bin/sh
kubectl exec -it my-pod -n my-namespace -c my-container -- bash
 
# Port forward
kubectl port-forward pod/my-pod          8080:8080 -n my-namespace
kubectl port-forward deployment/my-app   8080:8080 -n my-namespace
kubectl port-forward svc/my-service      8080:80   -n my-namespace
 
# Copy files
kubectl cp my-pod:/tmp/debug.log ./debug.log -n my-namespace
kubectl cp ./local-file my-pod:/tmp/local-file -n my-namespace
 
# Ephemeral debug container (Kubernetes 1.23+)
kubectl debug -it my-pod --image=busybox --target=my-container -n my-namespace
 
# Node shell (via privileged debug pod)
kubectl debug node/aks-nodepool1-abc123 -it --image=mcr.microsoft.com/cbl-mariner/busybox:2.0

Apply and rollouts

Bash
# Apply / delete
kubectl apply  -f manifest.yaml
kubectl apply  -f ./manifests/         # directory
kubectl apply  -k ./kustomize/         # kustomize
kubectl delete -f manifest.yaml
 
# Diff before applying
kubectl diff -f manifest.yaml
 
# Rollouts
kubectl rollout status  deployment/my-app -n my-namespace
kubectl rollout history deployment/my-app -n my-namespace
kubectl rollout undo    deployment/my-app -n my-namespace
kubectl rollout undo    deployment/my-app --to-revision=3 -n my-namespace
kubectl rollout restart deployment/my-app -n my-namespace   # rolling restart
 
# Scale
kubectl scale deployment my-app --replicas=5 -n my-namespace
 
# Autoscale (HPA)
kubectl autoscale deployment my-app --cpu-percent=70 --min=2 --max=10 -n my-namespace
kubectl get hpa -n my-namespace

ConfigMaps and Secrets

Bash
# ConfigMap from literal / file / directory
kubectl create configmap app-config \
  --from-literal=LOG_LEVEL=info \
  --from-literal=PORT=8080
 
kubectl create configmap nginx-conf --from-file=nginx.conf
 
# Secret from literal (base64 encoded automatically)
kubectl create secret generic db-creds \
  --from-literal=username=app \
  --from-literal=password=super-secret
 
# Secret from file (e.g., TLS cert)
kubectl create secret tls my-tls-secret \
  --cert=tls.crt --key=tls.key
 
# Docker registry secret (for private image pull)
kubectl create secret docker-registry regcred \
  --docker-server=myregistry.io \
  --docker-username=myuser \
  --docker-password="$REGISTRY_TOKEN"
 
# Decode a secret
kubectl get secret db-creds -n my-namespace -o jsonpath='{.data.password}' | base64 -d
 
# Edit live
kubectl edit configmap app-config -n my-namespace

Namespaces and RBAC

Bash
# Namespaces
kubectl create namespace my-namespace
kubectl get namespace
kubectl delete namespace my-namespace
 
# Service account
kubectl create serviceaccount my-sa -n my-namespace
 
# Role and RoleBinding (namespace-scoped)
kubectl create role pod-reader \
  --verb=get,list,watch \
  --resource=pods \
  -n my-namespace
 
kubectl create rolebinding read-pods \
  --role=pod-reader \
  --serviceaccount=my-namespace:my-sa \
  -n my-namespace
 
# ClusterRole and ClusterRoleBinding
kubectl create clusterrole node-reader --verb=get,list,watch --resource=nodes
kubectl create clusterrolebinding node-reader-binding \
  --clusterrole=node-reader \
  --serviceaccount=my-namespace:my-sa
 
# Check permissions
kubectl auth can-i list pods --as=system:serviceaccount:my-namespace:my-sa -n my-namespace
kubectl auth can-i '*' '*' -n my-namespace    # is current user cluster-admin?

Resource definitions (YAML patterns)

YAML
# Deployment
apiVersion: apps/v1
kind: Deployment
metadata:
  name: my-app
  namespace: my-namespace
  labels:
    app: my-app
    version: "1.2.3"
spec:
  replicas: 3
  selector:
    matchLabels:
      app: my-app
  strategy:
    type: RollingUpdate
    rollingUpdate:
      maxUnavailable: 0
      maxSurge:       1
  template:
    metadata:
      labels:
        app: my-app
    spec:
      serviceAccountName: my-sa
      securityContext:
        runAsNonRoot: true
        runAsUser:    1000
        fsGroup:      2000
        seccompProfile:
          type: RuntimeDefault
      containers:
        - name: my-app
          image: myregistry.io/my-app:1.2.3
          imagePullPolicy: IfNotPresent
          ports:
            - containerPort: 8080
              protocol: TCP
          securityContext:
            allowPrivilegeEscalation: false
            readOnlyRootFilesystem:   true
            capabilities:
              drop: [ALL]
          resources:
            requests:
              cpu:    "100m"
              memory: "128Mi"
            limits:
              cpu:    "500m"
              memory: "512Mi"
          envFrom:
            - configMapRef:
                name: app-config
          env:
            - name: DB_PASSWORD
              valueFrom:
                secretKeyRef:
                  name: db-creds
                  key:  password
          volumeMounts:
            - name: tmp
              mountPath: /tmp
          livenessProbe:
            httpGet:
              path: /healthz
              port: 8080
            initialDelaySeconds: 15
            periodSeconds:       20
          readinessProbe:
            httpGet:
              path: /ready
              port: 8080
            initialDelaySeconds: 5
            periodSeconds:       10
      volumes:
        - name: tmp
          emptyDir: {}
      topologySpreadConstraints:
        - maxSkew:           1
          topologyKey:       kubernetes.io/hostname
          whenUnsatisfiable: DoNotSchedule
          labelSelector:
            matchLabels:
              app: my-app
YAML
# Service
apiVersion: v1
kind: Service
metadata:
  name:      my-app
  namespace: my-namespace
spec:
  selector:
    app: my-app
  ports:
    - name:       http
      port:       80
      targetPort: 8080
      protocol:   TCP
  type: ClusterIP
YAML
# Ingress (nginx-ingress)
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name:      my-app
  namespace: my-namespace
  annotations:
    nginx.ingress.kubernetes.io/rewrite-target: /
    cert-manager.io/cluster-issuer:             letsencrypt-prod
spec:
  ingressClassName: nginx
  tls:
    - hosts:       [myapp.example.com]
      secretName:  myapp-tls
  rules:
    - host: myapp.example.com
      http:
        paths:
          - path:     /
            pathType: Prefix
            backend:
              service:
                name: my-app
                port:
                  number: 80
YAML
# HorizontalPodAutoscaler (with custom metrics)
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
  name:      my-app
  namespace: my-namespace
spec:
  scaleTargetRef:
    apiVersion: apps/v1
    kind:       Deployment
    name:       my-app
  minReplicas: 2
  maxReplicas: 20
  metrics:
    - type: Resource
      resource:
        name: cpu
        target:
          type:               Utilization
          averageUtilization: 70
    - type: Resource
      resource:
        name: memory
        target:
          type:          AverageValue
          averageValue:  400Mi
YAML
# CronJob
apiVersion: batch/v1
kind: CronJob
metadata:
  name:      db-backup
  namespace: my-namespace
spec:
  schedule:          "0 2 * * *"
  concurrencyPolicy: Forbid
  successfulJobsHistoryLimit: 3
  failedJobsHistoryLimit:     1
  jobTemplate:
    spec:
      template:
        spec:
          restartPolicy: OnFailure
          containers:
            - name:  backup
              image: myregistry.io/db-backup:latest
              env:
                - name: DB_PASSWORD
                  valueFrom:
                    secretKeyRef:
                      name: db-creds
                      key:  password
YAML
# PersistentVolumeClaim
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name:      my-app-data
  namespace: my-namespace
spec:
  accessModes:      [ReadWriteOnce]
  storageClassName: managed-csi-premium
  resources:
    requests:
      storage: 50Gi
YAML
# NetworkPolicy - default deny all ingress, allow only from same namespace
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name:      default-deny-ingress
  namespace: my-namespace
spec:
  podSelector: {}
  policyTypes:
    - Ingress
---
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name:      allow-same-namespace
  namespace: my-namespace
spec:
  podSelector: {}
  ingress:
    - from:
        - podSelector: {}

Helm

Installation and repo management

Bash
# Install Helm (Linux)
curl https://raw.githubusercontent.com/helm/helm/main/scripts/get-helm-3 | bash
 
# Repos
helm repo add stable       https://charts.helm.sh/stable
helm repo add ingress-nginx https://kubernetes.github.io/ingress-nginx
helm repo add cert-manager  https://charts.jetstack.io
helm repo add bitnami       https://charts.bitnami.com/bitnami
helm repo update
 
helm repo list
helm search repo nginx
helm search hub  nginx     # Artifact Hub

Install, upgrade, rollback

Bash
# Install
helm install my-release ingress-nginx/ingress-nginx \
  --namespace ingress-nginx \
  --create-namespace \
  --version 4.10.0 \
  --values    values.yaml \
  --set       controller.replicaCount=2
 
# Upgrade (or install if missing)
helm upgrade --install my-release ingress-nginx/ingress-nginx \
  --namespace ingress-nginx \
  --create-namespace \
  --values    values.yaml \
  --atomic \
  --timeout   5m0s \
  --wait
 
# Diff before upgrade (requires helm-diff plugin)
helm diff upgrade my-release ingress-nginx/ingress-nginx --values values.yaml
 
# Status / history
helm status   my-release -n ingress-nginx
helm history  my-release -n ingress-nginx
 
# Rollback
helm rollback my-release 2 -n ingress-nginx
 
# Uninstall
helm uninstall my-release -n ingress-nginx
 
# List all releases
helm list -A
helm list -n ingress-nginx
 
# Show chart values / README
helm show values  ingress-nginx/ingress-nginx
helm show readme  ingress-nginx/ingress-nginx
helm show chart   ingress-nginx/ingress-nginx

Render templates (dry-run)

Bash
# Render to stdout
helm template my-release ingress-nginx/ingress-nginx --values values.yaml
 
# Render and apply (GitOps pattern)
helm template my-release ./charts/my-app --values values.prod.yaml | kubectl apply -f -
 
# Server-side dry-run
helm upgrade --install my-release ./charts/my-app \
  --values values.yaml \
  --dry-run=server

Writing a Helm chart

Bash
# Scaffold
helm create my-chart
# Structure:
# my-chart/
#   Chart.yaml
#   values.yaml
#   templates/
#     deployment.yaml
#     service.yaml
#     ingress.yaml
#     _helpers.tpl    ← named template definitions
 
# Lint
helm lint ./my-chart
 
# Package
helm package ./my-chart --version 1.2.3
 
# Push to OCI registry (Helm 3.8+)
helm push my-chart-1.2.3.tgz oci://myregistry.io/helm
helm install my-release      oci://myregistry.io/helm/my-chart --version 1.2.3
YAML
# values.yaml
replicaCount: 2
image:
  repository: myregistry.io/my-app
  tag:        ""    # defaults to Chart.AppVersion
  pullPolicy: IfNotPresent
resources:
  limits:
    cpu:    500m
    memory: 512Mi
  requests:
    cpu:    100m
    memory: 128Mi
ingress:
  enabled: true
  host:    myapp.example.com
YAML
# templates/deployment.yaml (excerpt)
apiVersion: apps/v1
kind: Deployment
metadata:
  name: {{ include "my-chart.fullname" . }}
  labels:
    {{- include "my-chart.labels" . | nindent 4 }}
spec:
  replicas: {{ .Values.replicaCount }}
  template:
    spec:
      containers:
        - name: {{ .Chart.Name }}
          image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}"
          {{- with .Values.resources }}
          resources:
            {{- toYaml . | nindent 12 }}
          {{- end }}

Useful Helm plugins

Bash
helm plugin install https://github.com/databus23/helm-diff
helm plugin install https://github.com/jkroepke/helm-secrets
helm plugin install https://github.com/quintush/helm-unittest

See also: Azure - AKS for cluster provisioning and Day-2 operations. AWS - EKS for EKS-specific kubeconfig and access patterns.


Container Security 🔐

Pod Security Standards

YAML
# Apply Pod Security Admission to a namespace
apiVersion: v1
kind: Namespace
metadata:
  name: my-namespace
  labels:
    pod-security.kubernetes.io/enforce: restricted
    pod-security.kubernetes.io/warn:    restricted
    pod-security.kubernetes.io/audit:   restricted

Minimal secure pod spec

YAML
spec:
  automountServiceAccountToken: false
  securityContext:
    runAsNonRoot:   true
    runAsUser:      65534    # nobody
    runAsGroup:     65534
    fsGroup:        65534
    seccompProfile:
      type: RuntimeDefault
  containers:
    - name: app
      securityContext:
        allowPrivilegeEscalation: false
        readOnlyRootFilesystem:   true
        capabilities:
          drop: [ALL]
      volumeMounts:
        - name: tmp
          mountPath: /tmp
  volumes:
    - name: tmp
      emptyDir: {}

Image scanning

Bash
# Trivy (most common)
trivy image myregistry.io/my-app:latest
trivy image --severity HIGH,CRITICAL myregistry.io/my-app:latest
trivy image --ignore-unfixed myregistry.io/my-app:latest
trivy fs --scanners vuln,config .           # scan local filesystem + Dockerfile
 
# Grype
grype myregistry.io/my-app:latest
grype dir:.
 
# Docker Scout
docker scout cves myregistry.io/my-app:latest
docker scout compare myregistry.io/my-app:latest myregistry.io/my-app:1.2.2
 
# Syft (SBOM generation)
syft myregistry.io/my-app:latest -o spdx-json > sbom.spdx.json
syft myregistry.io/my-app:latest -o cyclonedx-json > sbom.cyclonedx.json

Image signing & verification (cosign) ✅

Scanning tells you what is in an image; signing proves who built it and that it has not been tampered with since. Prefer keyless signing (Sigstore Fulcio + the Rekor transparency log): the signature is bound to an OIDC identity such as a GitHub Actions workflow, so there is no long-lived key to leak.

Bash
# Keyless sign in CI - identity comes from the OIDC token, no key material to manage
cosign sign myregistry.io/my-app@sha256:<digest>
 
# Verify, pinning the expected signer identity and issuer (do this at deploy time)
cosign verify myregistry.io/my-app@sha256:<digest> \
  --certificate-identity-regexp '^https://github.com/my-org/.+' \
  --certificate-oidc-issuer https://token.actions.githubusercontent.com
 
# Attach an SBOM as a signed attestation, then verify it
cosign attest --predicate sbom.spdx.json --type spdxjson myregistry.io/my-app@sha256:<digest>
cosign verify-attestation --type spdxjson myregistry.io/my-app@sha256:<digest>
 
# Key-based signing (when OIDC is not available) - keep the private key in a KMS / Key Vault
cosign generate-key-pair
cosign sign   --key cosign.key myregistry.io/my-app@sha256:<digest>
cosign verify --key cosign.pub myregistry.io/my-app@sha256:<digest>

Always sign and verify by immutable digest (@sha256:...), never a mutable tag. Enforce signatures at admission with the Sigstore policy-controller or Kyverno so the cluster rejects unsigned images, rather than relying on a CI check that can be bypassed.

OPA / Gatekeeper policies

YAML
# Constraint template - deny latest tag
apiVersion: templates.gatekeeper.sh/v1
kind: ConstraintTemplate
metadata:
  name: k8snolatesttag
spec:
  crd:
    spec:
      names:
        kind: K8sNoLatestTag
  targets:
    - target: admission.k8s.gatekeeper.sh
      rego: |
        package k8snolatesttag
        violation[{"msg": msg}] {
          container := input.review.object.spec.containers[_]
          endswith(container.image, ":latest")
          msg := sprintf("Container '%v' uses :latest tag", [container.name])
        }
---
apiVersion: constraints.gatekeeper.sh/v1beta1
kind: K8sNoLatestTag
metadata:
  name: no-latest-tag
spec:
  match:
    kinds:
      - apiGroups: [""]
        kinds: ["Pod"]
    namespaces: ["production"]

See also: Security - broader hardening techniques including network scanning, TLS inspection, and Linux hardening that complement container security posture.


AKS-Specific Patterns

Workload identity for pods ✅ Current preferred pod-to-Azure auth pattern (as of 2026)

YAML
# Service account annotated with UAMI client ID
apiVersion: v1
kind: ServiceAccount
metadata:
  name:      my-sa
  namespace: my-namespace
  annotations:
    azure.workload.identity/client-id: "<UAMI-CLIENT-ID>"
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: my-app
  namespace: my-namespace
spec:
  template:
    metadata:
      labels:
        azure.workload.identity/use: "true"   # inject OIDC token
    spec:
      serviceAccountName: my-sa
      containers:
        - name: my-app
          image: myregistry.io/my-app:latest
          env:
            - name: AZURE_CLIENT_ID
              valueFrom:
                fieldRef:
                  fieldPath: metadata.annotations['azure.workload.identity/client-id']

Cluster access entries (replaces aws-auth ConfigMap - AKS equivalent: Azure RBAC)

Bash
# Enable Azure RBAC for Kubernetes
az aks update -n aks-myapp-prod -g rg-myapp-prod-uksouth --enable-azure-rbac
 
# Grant cluster access to a user
az role assignment create \
  --role "Azure Kubernetes Service Cluster User Role" \
  --assignee user@example.com \
  --scope "/subscriptions/$SUB/resourceGroups/rg-myapp-prod-uksouth/providers/Microsoft.ContainerService/managedClusters/aks-myapp-prod"
 
# Grant admin
az role assignment create \
  --role "Azure Kubernetes Service RBAC Cluster Admin" \
  --assignee user@example.com \
  --scope "..."

See also: Azure - AKS for OIDC issuer setup, managed identity federation, and cluster upgrades via Az CLI.


Useful One-Liners

Bash
# Delete all evicted pods across all namespaces
kubectl get pods -A | grep Evicted | awk '{print $1, $2}' | \
  xargs -n2 kubectl delete pod -n
 
# Force delete all pods in Terminating state
kubectl get pods -A | grep Terminating | awk '{print $1, $2}' | \
  xargs -n2 bash -c 'kubectl delete pod -n $0 $1 --grace-period=0 --force'
 
# Get resource requests vs limits for all pods in a namespace
kubectl get pods -n my-namespace -o json | jq -r '
  .items[] | .metadata.name as $name |
  .spec.containers[] |
  [$name, .name, (.resources.requests.cpu // "none"), (.resources.limits.cpu // "none")] |
  @tsv'
 
# List all images running in a namespace
kubectl get pods -n my-namespace -o jsonpath='{range .items[*]}{range .spec.containers[*]}{.image}{"\n"}{end}{end}' | sort -u
 
# Watch pods with auto-refresh
watch -n2 kubectl get pods -n my-namespace
 
# Get all non-running pods
kubectl get pods -A --field-selector=status.phase!=Running
 
# Drain a node (evicts pods gracefully)
kubectl drain my-node --ignore-daemonsets --delete-emptydir-data --force
kubectl uncordon my-node
 
# Top (resource usage - requires metrics-server)
kubectl top pods  -n my-namespace --sort-by=memory
kubectl top nodes
 
# Apply a patch without editing YAML
kubectl patch deployment my-app -n my-namespace \
  --type=json \
  -p='[{"op":"replace","path":"/spec/replicas","value":5}]'
 
# Annotate / label
kubectl label   pod my-pod app=my-app -n my-namespace
kubectl annotate pod my-pod my-annotation=value -n my-namespace

Anti-patterns

  • ⚠️ Using :latest tag in production - non-deterministic; a new pull can silently change behaviour. Pin to a specific version tag or digest (image@sha256:...).
  • 🚨 Running containers as root - unnecessary privilege; use USER nobody in Dockerfile and runAsNonRoot: true in pod spec. Most apps do not need root.
  • ⚠️ No resource requests/limits in Kubernetes - without requests, a container can be scheduled on an overloaded node; without limits, it can OOM the node. Always set both.
  • 🚨 Storing sensitive values in ConfigMap - ConfigMaps are not encrypted at rest or in etcd. Use Secret objects for credentials, and consider an external secrets operator (ESO, Vault Agent) for production.
  • 🚨 docker system prune -af --volumes on a shared host - irreversibly destroys all stopped containers, images, and volumes, including ones used by other projects or users on the same machine.
  • ⚠️ kubectl delete pod --force --grace-period=0 as a routine fix - bypasses graceful shutdown; the underlying cause (disk pressure, node issue, stuck finalizer) still needs investigation. Use this only for truly stuck pods.
  • ⚠️ No readinessProbe - without it, Kubernetes routes traffic to a pod immediately on startup, before the app is ready to serve. Always define readiness (and liveness) probes.
Last updated on