Verified11 commandsAI-assisted

stern

.md

Verified against stern v1.34.0, flags verified via `stern --help` / `stern --show-hidden-options` run · official docs

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#

Diagram

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#

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:

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

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#

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.

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#

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:

# ~/.config/stern/config.yaml
tail: 50
max-log-requests: 20
timestamps: short
exclude-container:
  - istio-proxy
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:

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#

# .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 behaviorstern "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.