# stern Cheat Sheet

> **Tool:** stern
> **Category:** Containers & Orchestration
> **Verified against:** stern v1.34.0, flags verified via `stern --help` / `stern --show-hidden-options` run
> locally, and exercised against a local kubeconfig to confirm real watch/retry behavior, 2026-08-29
> **Official docs:** https://github.com/stern/stern

## What it is and where it fits 🎯

Plain `kubectl logs` only tails one pod's one container at a time — the moment a Deployment has 3+ replicas,
or a Pod has a sidecar you also care about, you're back to opening multiple terminal panes and manually
matching pod names by hand, right when you're mid-incident and least want to. stern is a single-purpose,
non-interactive log multiplexer: give it a regex (or a Kubernetes resource reference) matching one or more
pods, and it tails every matching pod *and every matching container inside each one*, live, interleaved into
one stream with per-pod/per-container coloring — and it keeps watching for *new* matching pods that appear
after it started, which plain `kubectl logs -f` can never do. Where it differs from k9s: k9s is an
interactive TUI you browse resources in and one keybinding (`l`) happens to stream one pod's logs; stern is
built to be piped, redirected, and scripted — the log-tailing equivalent of `tail -f` across an entire
Deployment, ReplicaSet, or label selector at once.

## What actually happens when you run stern

```mermaid
sequenceDiagram
    participant U as You
    participant S as stern
    participant API as Kubernetes API
    participant P1 as Pod A (matches)
    participant P2 as Pod B (matches, starts later)

    U->>S: stern "web-\w+" -n prod
    S->>API: List + Watch pods (namespace, label/field selectors)
    API-->>S: Pod A exists, container running
    S->>P1: Open log stream
    P1-->>S: log lines
    S-->>U: colored, prefixed lines

    Note over S,API: Watch stays open — stern reacts to NEW matching pods
    API-->>S: Pod B just started (matches regex too)
    S->>P2: Open log stream
    P2-->>S: log lines
    S-->>U: colored, prefixed lines (interleaved with Pod A)
```

**The watch, not a one-shot list, is what makes stern genuinely different from scripting `kubectl logs` in a
loop** — a pod that gets created mid-tail (a rolling deploy, an autoscale event, a retry after a crash) is
picked up automatically without stern being restarted.

## Installation

```bash
brew install stern                                   # macOS/Linux Homebrew
kubectl krew install stern                            # as a kubectl plugin, via krew — invoke as `kubectl stern`
go install github.com/stern/stern@latest               # from source, if you have a Go toolchain

# or download the release binary directly (verified release: v1.34.0):
curl -sL -o stern.tar.gz \
  https://github.com/stern/stern/releases/download/v1.34.0/stern_1.34.0_linux_amd64.tar.gz
tar -xzf stern.tar.gz stern && sudo mv stern /usr/local/bin/

stern --version
```

Real captured `stern --version` output:

```
version: 1.34.0
commit: b6f1226e8eb72fb182cebfdcb5ef02c40813d6f3
built at: 2026-05-02T12:23:26Z
```

## The pod-query argument — regex or resource reference

The positional argument stern takes is either a regular expression matched against pod names, or a
`<resource>/<name>` reference for exact targeting against a specific workload:

```bash
stern "web-\w+" -n prod                 # regex — matches web-backend, web-frontend; NOT web-123 (word boundary)
stern .                                  # matches everything in the target namespace(s)
stern deployment/checkout-api            # resource reference — every pod owned by this Deployment, by name
stern statefulset/postgres               # supported resource kinds: pod, rc, svc, ds, deploy, rs, sts, job
```

## Command reference — scoping which pods/containers to tail

Real `stern --help` output (v1.34.0, the flags a day-to-day user reaches for most):

```
Flags:
  -A, --all-namespaces                  If present, tail across all namespaces.
      --context string                  The name of the kubeconfig context to use
  -c, --container string                Container name when multiple containers in pod. (regular expression) (default ".*")
  -E, --exclude-container stringArray   Container name to exclude when multiple containers in pod.
      --container-state strings         Tail containers with state in running, waiting, terminated, or all. (default [all])
  -n, --namespace strings               Kubernetes namespace to use. Repeat or comma-separate for multiple.
  -l, --selector string                 Selector (label query) to filter on.
      --field-selector string           Selector (field query) to filter on.
      --node string                     Node name to filter on.
  -e, --exclude stringArray             Log lines to exclude. (regular expression)
  -i, --include stringArray             Log lines to include. (regular expression)
  -H, --highlight stringArray           Log lines to highlight. (regular expression)
      --exclude-pod stringArray         Pod name to exclude. (regular expression)
  -s, --since duration                  Return logs newer than a relative duration. (default 48h0m0s)
      --tail int                        Number of lines from the end of the logs to show. (default -1, all)
      --no-follow                       Exit when all logs have been shown, instead of streaming.
```

```bash
stern . -n checkout                              # every pod in the checkout namespace
stern . -l app=checkout,tier=backend             # every pod matching a label selector, any namespace flag combo
stern . -n checkout -c app                       # only the "app" container in pods with a sidecar
stern . -n checkout -E istio-proxy               # every container EXCEPT the istio sidecar — very common combo
stern . -n checkout --since 15m                  # only the last 15 minutes — avoid a huge historical replay
stern . -n checkout --no-follow --tail 200       # last 200 lines then exit — the "one-shot dump" mode, not a live tail
stern . -A -l app=checkout                       # the same label selector across every namespace at once
```

> [!TIP]
> **`-E`/`--exclude-container` for sidecars is one of the highest-value flags here.** A service mesh sidecar
> (`istio-proxy`, `linkerd-proxy`) or a logging agent sidecar produces a constant stream of its own
> operational noise that has nothing to do with the application you're actually debugging — excluding it by
> name up front keeps the interleaved stream readable instead of drowning your app's own log lines.

## Filtering log content itself

```bash
stern . -n checkout -i "ERROR|FATAL"             # only lines matching this regex — everything else is dropped
stern . -n checkout -e "health check"            # drop lines matching this regex — everything else kept
stern . -n checkout -H "timeout"                 # keep every line, but visually highlight matches
```

`-i`/`--include` and `-e`/`--exclude` can combine — include narrows what shows up at all, exclude then drops
specific noise from what remains. `-H`/`--highlight` never drops anything; it just makes the matching text
stand out in the stream, useful when you want the full context around a match rather than only the matching
lines.

## Output formatting and templates

```bash
stern . -n checkout -o json                                        # structured JSON per log line — good for piping to jq
stern . -n checkout -o raw                                          # message only, no pod/container/color prefix at all
stern . -n checkout --template '{{.Message}} ({{.PodName}}/{{.ContainerName}})'
```

`--template` compiles a Go text/template against each log line's fields (`.Message`, `.PodName`,
`.ContainerName`, `.Namespace`, plus color helpers) — this is the mechanism behind the three predefined
`-o` values (`default`, `raw`, `json`), and you can write your own for a shape that fits your log-shipping
pipeline better than any of the built-ins.

```bash
stern . -n checkout --template \
  '{{.PodName}}/{{.ContainerName}} {{with $d := .Message | parseJSON}}[{{$d.level}}] {{$d.message}}{{end}}'
```

That template pattern — parse each line as JSON and pull out just `level` and `message` — is the standard
way to make a service that emits structured JSON logs readable in a live terminal tail without losing the
original JSON if you redirect the same command to a file instead.

## Timestamps and timezone

```bash
stern . -n checkout -t                            # timestamps, default format
stern . -n checkout --timestamps=short             # a shorter timestamp format
stern . -n checkout -t --timezone UTC              # force UTC regardless of local shell timezone
```

## Config file — `~/.config/stern/config.yaml`

Flags used on nearly every invocation belong in a config file instead of being retyped each time:

```yaml
# ~/.config/stern/config.yaml
tail: 50
max-log-requests: 20
timestamps: short
exclude-container:
  - istio-proxy
```

```bash
stern . -n checkout --config ~/.config/stern/my-team-defaults.yaml    # use a specific config instead of the default path
```

## Real-world scenario: watching a rolling deploy across old and new pods at once

A team ships a new version of `checkout-api` and wants to watch both the old pods (draining) and new pods
(starting) simultaneously, to catch a bad new version before it's fully rolled out and the old, known-good
pods are gone:

```bash
stern deployment/checkout-api -n prod --since 5m
```

Because stern watches for new matching pods rather than snapshotting once, this single command keeps working
through the entire rollout — the new ReplicaSet's pods appear in the stream automatically as they come up,
interleaved with the still-terminating old ones, with per-pod coloring making it obvious at a glance which
generation each line came from.

> [!IMPORTANT]
> **A resource reference (`deployment/checkout-api`) tracks the Deployment's *current* pods only** — it
> resolves through the Deployment's live selector, so a rolling update's brand-new ReplicaSet pods are
> picked up correctly. A hand-written regex against a *specific* ReplicaSet's generated pod-name suffix
> would silently stop matching the instant a new ReplicaSet replaces it — prefer the resource-reference form
> over a regex whenever you specifically want "this Deployment's pods, whichever generation is currently
> live."

## Real-world scenario: capturing logs from a CI job's pods for a failing pipeline run

```yaml
# .github/workflows/e2e.yml
- name: Capture pod logs during test run
  run: |
    stern . -n e2e-${{ github.run_id }} --no-follow --tail -1 -o raw > pod-logs.txt &
    STERN_PID=$!
    npm run test:e2e
    kill $STERN_PID 2>/dev/null || true
  continue-on-error: true

- name: Upload logs on failure
  if: failure()
  uses: actions/upload-artifact@v4
  with:
    name: pod-logs
    path: pod-logs.txt
```

> [!WARNING]
> **A backgrounded `stern` process in CI is a live watch, not a one-shot command — it needs to be explicitly
> killed, or the job step never completes.** `--no-follow` alone doesn't help here because it only means
> "exit once the currently-visible logs are shown," which for a namespace that hasn't finished creating pods
> yet still amounts to an indefinite wait. Backgrounding it with `&` and killing it explicitly once the test
> run finishes (as above) is the standard pattern for capturing logs from a scoped, ephemeral CI namespace
> without hanging the pipeline.

## Common pitfalls

- **Forgetting the regex word-boundary behavior** — `stern "web"` matches `web-backend-abc123` (substring
  match by default, since it's a regex search, not an exact match) which is usually what you want, but a
  tighter pattern like `"^web-\w+$"` is needed if pod names could otherwise collide with an unrelated
  workload sharing a common substring.
- **Not excluding sidecars** and getting a stream dominated by mesh/logging-agent noise — see the `-E` TIP
  above.
- **Assuming stern retries silently forever on an unreachable cluster** without noticing — a bad context or
  network path produces a repeating `"Watch failed"` error on a retry loop rather than a single clear
  failure and exit; confirm connectivity with `kubectl cluster-info` first if the stream never produces any
  log lines at all.
- **Using `--since` with a very old duration (or the 48h default) against a high-volume namespace** and
  getting flooded with historical replay before the live tail even starts — narrow `--since` for anything
  you're watching live rather than investigating retroactively.

## Exit codes

`0` on a clean exit (including a normal `ctrl-c` or `--no-follow` completion) · non-zero on a connection/
config error (bad kubeconfig, unreachable API, invalid selector).

## When to reach for something else

For interactively browsing a resource's logs alongside its full state (events, YAML, exec) rather than
tailing logs in isolation, reach for k9s instead — its `l` keybinding covers the single-pod case with less
setup. For pure context/namespace positioning before tailing, kubectx/kubens remain the right tools to run
first; stern's own `--context`/`-n` flags exist so a one-off invocation doesn't require switching your shell's
default first.
