stern
.mdVerified 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#
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 --versionReal 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, jobCommand 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 onceTip
-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 timezoneConfig 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-proxystern . -n checkout --config ~/.config/stern/my-team-defaults.yaml # use a specific config instead of the default pathReal-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 5mBecause 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.txtWarning
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"matchesweb-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
-ETIP 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 withkubectl cluster-infofirst if the stream never produces any log lines at all. - Using
--sincewith 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--sincefor 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.