Running one container is easy. Running hundreds in production, reliably, at scale? That’s where patterns emerge.

These aren’t Kubernetes-specific (though that’s where you’ll see them most). They’re fundamental approaches to composing containers into systems that actually work.

The Sidecar Pattern

A sidecar is a helper container that runs alongside your main application container, sharing the same pod/network namespace.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
apiVersion: v1
kind: Pod
metadata:
  name: web-app
spec:
  containers:
    # Main application
    - name: app
      image: myapp:1.0
      ports:
        - containerPort: 8080
    
    # Sidecar: log shipper
    - name: log-shipper
      image: fluentd:latest
      volumeMounts:
        - name: logs
          mountPath: /var/log/app
  
  volumes:
    - name: logs
      emptyDir: {}

Common sidecar use cases:

  • Logging: Collect and ship logs (Fluentd, Filebeat)
  • Proxies: Service mesh sidecars (Envoy, Linkerd)
  • Security: mTLS termination, secret injection (Vault Agent)
  • Monitoring: Metrics exporters, health checkers

Why sidecars?

  • Single responsibility: your app doesn’t need logging logic
  • Independent updates: update the sidecar without touching the app
  • Language agnostic: works regardless of what your app is written in

The tradeoff: More containers = more resource overhead, more complexity in debugging.

The Ambassador Pattern

An ambassador is a specialized sidecar that proxies connections to external services, abstracting away the complexity.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
containers:
  - name: app
    image: myapp:1.0
    env:
      - name: REDIS_HOST
        value: "localhost"  # Ambassador handles the actual connection
      - name: REDIS_PORT
        value: "6379"
  
  - name: redis-ambassador
    image: redis-cluster-proxy:1.0
    # Handles connection pooling, failover, cluster routing

Your application connects to localhost:6379. The ambassador handles:

  • Connection pooling
  • Cluster-aware routing
  • Failover logic
  • TLS termination

Why ambassadors?

  • App stays simple (just connect to localhost)
  • Complex connection logic lives in a dedicated, reusable component
  • Easy to swap implementations (different Redis proxy, different cloud)

The Adapter Pattern

An adapter transforms the interface of your container to match what the system expects.

Classic example: metrics format conversion.

1
2
3
4
5
6
7
8
containers:
  - name: legacy-app
    image: old-java-app:1.0
    # Exposes metrics in some proprietary format

  - name: prometheus-adapter
    image: metrics-adapter:1.0
    # Scrapes proprietary metrics, exposes /metrics in Prometheus format

Why adapters?

  • Integrate legacy systems without modifying them
  • Standardize interfaces across heterogeneous systems
  • Separation of concerns (app doesn’t need to know about Prometheus)

Init Containers

Init containers run before your main containers start. They run to completion, one at a time, in order.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
apiVersion: v1
kind: Pod
spec:
  initContainers:
    - name: wait-for-db
      image: busybox
      command: ['sh', '-c', 'until nc -z postgres 5432; do sleep 2; done']
    
    - name: run-migrations
      image: myapp:1.0
      command: ['./migrate', 'up']
    
    - name: fetch-config
      image: vault-agent:1.0
      command: ['fetch-secrets.sh']
      volumeMounts:
        - name: secrets
          mountPath: /secrets
  
  containers:
    - name: app
      image: myapp:1.0
      volumeMounts:
        - name: secrets
          mountPath: /secrets

Common init container use cases:

  • Wait for dependencies to be ready
  • Run database migrations
  • Fetch secrets or configuration
  • Set up file permissions
  • Clone git repos

Key property: If any init container fails, Kubernetes restarts the pod. The main containers won’t start until all init containers succeed.

The Operator Pattern

Operators are custom controllers that encode domain knowledge about running a specific application.

Instead of:

1
2
3
4
5
# Manual PostgreSQL setup
kubectl create -f postgres-configmap.yaml
kubectl create -f postgres-statefulset.yaml
kubectl create -f postgres-service.yaml
# Then manually: set up replication, backups, failover...

With an operator:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
apiVersion: postgres-operator.crunchydata.com/v1beta1
kind: PostgresCluster
metadata:
  name: mydb
spec:
  postgresVersion: 15
  instances:
    - replicas: 3
  backups:
    pgbackrest:
      repos:
        - name: repo1
          s3:
            bucket: my-backups

The operator handles:

  • Provisioning primary and replicas
  • Configuring replication
  • Automated failover
  • Backup scheduling
  • Version upgrades

When to use operators:

  • Stateful applications with complex lifecycle (databases, message queues)
  • Applications requiring domain expertise to operate correctly
  • When “kubectl apply” isn’t enough

When to avoid:

  • Simple stateless apps (just use a Deployment)
  • When the complexity isn’t justified
  • When you don’t trust the operator’s quality

Job and CronJob Patterns

Not everything runs forever. Jobs handle run-to-completion workloads.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
# One-time job
apiVersion: batch/v1
kind: Job
metadata:
  name: data-migration
spec:
  template:
    spec:
      containers:
        - name: migrate
          image: migration-tool:1.0
      restartPolicy: Never
  backoffLimit: 3

---
# Recurring job
apiVersion: batch/v1
kind: CronJob
metadata:
  name: nightly-backup
spec:
  schedule: "0 2 * * *"  # 2 AM daily
  jobTemplate:
    spec:
      template:
        spec:
          containers:
            - name: backup
              image: backup-tool:1.0
          restartPolicy: OnFailure

Patterns for reliable jobs:

  • Idempotency: jobs might run multiple times (retries, duplicates)
  • Timeouts: set activeDeadlineSeconds to kill stuck jobs
  • Cleanup: ttlSecondsAfterFinished removes completed pods
  • Concurrency: concurrencyPolicy controls overlap (Allow/Forbid/Replace)

The DaemonSet Pattern

Run exactly one pod per node. Perfect for node-level services.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
apiVersion: apps/v1
kind: DaemonSet
metadata:
  name: node-exporter
spec:
  selector:
    matchLabels:
      app: node-exporter
  template:
    metadata:
      labels:
        app: node-exporter
    spec:
      containers:
        - name: exporter
          image: prom/node-exporter:latest
          ports:
            - containerPort: 9100

Common DaemonSet uses:

  • Log collectors (one per node to collect all logs)
  • Monitoring agents (node metrics, GPU metrics)
  • Network plugins (CNI)
  • Storage plugins (CSI node drivers)

Anti-Patterns to Avoid

The “Everything Container”

1
2
3
4
5
# Don't do this
FROM ubuntu:22.04
RUN apt-get install nginx php mysql supervisor
COPY supervisord.conf /etc/supervisor/conf.d/
CMD ["supervisord"]

This defeats the purpose of containers. You lose:

  • Independent scaling
  • Independent updates
  • Clear failure isolation
  • Resource limits per component

Instead: One process per container, composed via pods.

Sidecar Explosion

1
2
3
4
5
6
7
8
9
containers:
  - name: app
  - name: log-shipper
  - name: metrics-exporter
  - name: service-mesh-proxy
  - name: secret-injector
  - name: config-watcher
  - name: certificate-manager
  # ... 12 sidecars later

Each sidecar adds CPU, memory, and complexity. At some point, your sidecars cost more than your app.

Solutions:

  • Combine related functionality (one observability sidecar, not three)
  • Use node-level daemons where appropriate
  • Question whether each sidecar is actually necessary

Treating Pods Like VMs

1
2
3
4
5
6
containers:
  - name: app
    lifecycle:
      postStart:
        exec:
          command: ["/bin/sh", "-c", "apt-get update && apt-get install debug-tools"]

Containers are ephemeral. Don’t install things at runtime. Don’t SSH in to fix things. Build it into the image or don’t have it.

Choosing the Right Pattern

PatternUse WhenAvoid When
SidecarCross-cutting concerns (logging, proxy)Simple apps, resource-constrained
AmbassadorComplex external connectionsDirect connection is fine
AdapterInterface mismatchCan modify source
Init ContainerPre-start setup, orderingOngoing processes
OperatorComplex stateful appsStateless, simple lifecycle
DaemonSetPer-node functionalityPer-app functionality
JobRun-to-completionLong-running services

Patterns are tools, not rules. The best container architecture is the simplest one that meets your requirements. Start simple. Add patterns when the problem demands them, not before.