k9s
Rewritten for this edition in October 2026 for Kubernetes 1.37. Commands checked against the official documentation and source of k9s v0.51.0.
k9s is a terminal UI for Kubernetes. You start it with k9s, it reads the same kubeconfig as kubectl, and it shows your cluster as live tables that refresh on their own: pods, deployments, services, events, anything the API server knows about, custom resources included. Every row is one keystroke away from its logs, its YAML, a shell, a port-forward or a delete. You type : plus a resource name to jump between views and ? to see every key that works where you are. If you only remember three things from this chapter, make it those two keys and esc to go back.
What k9s adds on top of kubectl#
So far in the book you've typed kubectl get, read the output, then typed another command with a name copied from it. Good for learning. Slow once you're debugging. A pod restarts, you want its previous logs, then the events on its ReplicaSet, then the Deployment that owns it. Four commands, four names to paste.
k9s keeps that loop on one screen. The table under your cursor is always current (it polls the API every two seconds by default), and the actions you'd reach for next are bound to single letters. It doesn't hide Kubernetes from you either: what you see are the real resources with their real names, so the objects you met in the Deployment chapter show up exactly as they are.
The project's latest release is v0.51.0, published on 6 June 2026. You can check for newer ones on the k9s releases page.
Before you start#
You need a cluster you're allowed to break. A local kind cluster or a K3s node is ideal, since this chapter deletes and kills things on purpose. Check that kubectl get nodes answers before going further; k9s uses the same kubeconfig and current context, so if kubectl can't reach the cluster, k9s won't either.
Three small things save you trouble later:
- k9s draws in 256-colour mode. On Linux and macOS terminals that don't advertise it, export
TERM=xterm-256color. - Editing a resource from k9s opens the editor named in
KUBE_EDITOR(orEDITOR), so set one you know how to quit. - The CPU and memory columns only appear when the cluster serves the Metrics API. On a fresh kind cluster that means installing metrics-server. If it can't verify your kubelets' certificates, add the
--kubelet-insecure-tlsargument to its container, which the project marks as for testing only.
export TERM=xterm-256color
export KUBE_EDITOR=nano
# optional: CPU/MEM columns in k9s
kubectl apply -f https://github.com/kubernetes-sigs/metrics-server/releases/latest/download/components.yaml
Install k9s#
Pick the line for your system. All of these come from the project README; prebuilt tarballs for Linux, macOS and Windows are also attached to every release.
# macOS or Linux, Homebrew
brew install derailed/k9s/k9s
# macOS, MacPorts
sudo port install k9s
# Linux
snap install k9s --devmode # snap
pacman -S k9s # Arch
zypper install k9s # openSUSE
dnf install k9s # Fedora 42+
pkg install k9s # FreeBSD
# Ubuntu / Debian, .deb from the latest release
wget https://github.com/derailed/k9s/releases/latest/download/k9s_linux_amd64.deb \
&& sudo apt install ./k9s_linux_amd64.deb && rm k9s_linux_amd64.deb
# Windows
winget install k9s
scoop install k9s
choco install k9s
# Go toolchain (builds the development version)
go install github.com/derailed/k9s@latest
# no install at all: the official image with your kubeconfig mounted
docker run --rm -it -v ~/.kube/config:/root/.kube/config derailed/k9s
Confirm it's on your path with k9s version. The Go route compiles from source, and the README warns that it puts the development version in effect, so a package manager is the better default.
Launching k9s: the flags worth knowing#
Plain k9s opens in your current context and namespace. These flags change where you land:
k9s # current context, current namespace
k9s -n kube-system # start in one namespace
k9s -A # start in all namespaces
k9s --context my-context # start in another kubeconfig context
k9s -c deploy # open the deployments view first
k9s --readonly # disable delete, kill, edit and other changes
k9s -l debug # log at debug level
k9s info # print where config, logs and skins live
k9s help # every flag
--readonly deserves a habit. Open a production context with it and the destructive keys simply aren't there. There's also a --write flag that overrides a readOnly: true you've set in config, for the moments you do mean to change something.
Walkthrough: one Deployment, start to finish#
The fastest way to learn the keys is to use them on something you made. Create a small Deployment and a Service in front of it from a normal shell first.
Create something to look at#
kubectl create deployment web --image=nginx --replicas=2 --port=80
kubectl expose deployment web --port=80
k9s
k9s opens on the pods view. Two web-… pods should be there, with a STATUS column that turns Running once the image is pulled. If one sits in ImagePullBackOff or CrashLoopBackOff, that's your cue for the CrashLoopBackOff chapter; k9s is the quickest way to watch those fixes take effect.
Move around with the command bar#
Press : and type deploy, then Enter. The command bar takes singular, plural or short names, so :deployment, :deployments and :dp all land in the same place. Now try a few variations you'll use daily:
:pod defaultopens pods in thedefaultnamespace only.- With
:pod /webthe pods view opens already filtered to names matchingweb. :svc, then Enter on thewebrow: k9s shows the pods behind the Service.- Inside any table,
/filters what's shown. It accepts a regex,/!inverts it, and/-l app=webfilters by label.
Two navigation keys tie this together. esc backs out of a view, filter or prompt. And - jumps to the previous command, much like cd - in a shell. Press ? at any point to see what's bound in the current view, and ctrl-a for every resource alias k9s knows on this cluster.
Read logs and open a shell#
Go back to pods, put the cursor on a web pod and press l. You get the container's log stream. The number keys pick a time window (0 tails, 1 shows from the start, 2 to 6 mean the last 1m, 5m, 15m, 30m and 1h). w wraps long lines, t adds timestamps, s toggles autoscroll and ctrl-s saves the log to a file. When a container has restarted, p in the pods view shows the previous instance's logs, which is where the crash message usually is.
Press esc, then s on the same pod. k9s opens a shell inside the container; type exit to come back. d shows kubectl describe output, y shows the live YAML, and e opens it in your editor.
Scale, restart, delete#
Switch to :dp and select web. A few keys here change the cluster, so read the prompt each one shows:
sasks for a replica count. Set 3 and watch a third pod appear when you jump back to:pod.rruns a rolling restart, the same askubectl rollout restart.- Want to see the ReplicaSets this Deployment created? That's
z. ctrl-ddeletes after a confirmation (TAB to the OK button, then Enter).ctrl-kkills with no confirmation at all, the equivalent ofkubectl delete --now.
Try ctrl-k on one of the pods. It vanishes, and a replacement shows up seconds later because the ReplicaSet notices it's one short. Watching self-healing happen live is a good reason to keep k9s open for the rest of the book.
For a tree of how everything connects, run :xray dp default. It draws each Deployment as a tree, with the objects it owns nested underneath. :pulses is the wide-angle version: a one-screen dashboard of the workloads running on the cluster you're connected to.
Port-forward and benchmark#
From the pods view, select a web pod and press shift-f. A dialog comes up for the container port and the local port; set the local one to 8080. Point a browser at http://localhost:8080 and nginx answers, through a tunnel k9s holds open for as long as the session lasts.
Type :pf. Every active forward is a row in this view. Select yours and press b to run a benchmark against it (press b again to stop a run in progress). Results land in the benchmarks view, :be, and Enter on a result shows the request stats. Enter on a row in the port-forward view also takes you straight to its benchmarks.
Under the hood k9s uses the hey load generator. Without any config a run sends 200 GET requests to / with a concurrency of 1. To change that per container or per Service, edit the context's benchmarks.yaml, which lives under the k9s data directory at clusters/<cluster>/<context>/benchmarks.yaml. The k9s benchmark docs show the full format; here's a container entry for this walkthrough:
benchmarks:
defaults:
concurrency: 1
requests: 200
containers:
# namespace/pod-name:container-name
default/POD_NAME:CONTAINER_NAME:
concurrency: 4
requests: 1000
http:
path: /
method: GET
Fill in the pod name from the pods view and the container name from Enter on that pod. Services can be benchmarked the same way with b in the :svc view. That's meant for NodePort and LoadBalancer Services, and it needs a services: entry giving the node or load balancer address to hit.
k9s commands reference#
Commands start with : and finish with Enter. Any resource name, plural or short name works, including your CRDs.
| Command | What it does |
|---|---|
:pod, :po, :pods | Pods view (works for any resource name or alias) |
:pod kube-system | The resource, in one namespace |
:pod /web | The resource, pre-filtered by name |
:pod app=web,env=dev | The resource, filtered by labels |
:pod @my-context | The resource in another context (switches your k9s context) |
:ctx | List contexts and switch |
:ctx my-context | Switch straight to a context |
:ns | List namespaces; u on a row switches to it |
:dp, :svc, :sts, :ds, :jo, :sec | Common short names; dp, jo and sec come from the starter alias file k9s generates |
:xray RESOURCE [NAMESPACE] | Tree view of po, svc, dp, rs, sts or ds and what they own |
:pulses, :pu | Cluster summary dashboard |
:pf | Active port-forwards |
:be | Benchmark results |
:sd, :screendump | Saved screen dumps |
:popeye, :pop | Popeye cluster sanitizer report |
:q, :quit | Exit k9s |
k9s shortcuts reference#
Keys depend on the view you're in, and ? always shows the current set. These are the ones the README lists for k9s v0.51.0, grouped by where they work. Lowercase letters are plain keys; shift-x means the capital.
Anywhere#
These work in every view.
| Key | Action |
|---|---|
? | Help for the current view |
ctrl-a | All resource aliases |
: | Command mode |
/ | Filter (regex); /! inverse, /-l labels, /-f fuzzy |
esc | Back, or leave command/filter mode |
- | Previous command |
[ / ] | Back / forward in command history |
ctrl-r | Refresh the view |
ctrl-e | Toggle the header |
ctrl-g | Toggle breadcrumbs |
ctrl-c | Quit (unless noExitOnCtrlC is set) |
Any resource table#
Pods, deployments, secrets, CRDs: any list of resources accepts these, as long as the action makes sense for the type.
| Key | Action |
|---|---|
d | Describe |
y | View YAML |
e | Edit (not in read-only mode) |
l / p | Logs / previous logs, where the resource has them |
ctrl-d | Delete, with confirmation |
ctrl-k | Kill, no confirmation |
space | Mark a row; ctrl-space marks a range, ctrl-\ clears marks |
c / n | Copy resource name / namespace |
w | Warp to the row's namespace |
shift-j | Jump to the owner |
ctrl-w | Toggle wide columns |
ctrl-z | Toggle the faults/error display |
ctrl-s | Save the table to a file |
shift-n, shift-a, shift-s, shift-p | Sort by name, age, status, namespace |
shift-o, shift-←, shift-→ | Sort by selected column, move the column |
Specific views#
Some letters mean different things in different views. s is a shell on a pod and a scale prompt on a Deployment, so glance at the view title before you press it.
| View | Key | Action |
|---|---|---|
| Pods | s / a | Shell into / attach to the container |
| Pods, Services, Containers | shift-f / f | Start a port-forward / show port-forwards |
| Deployments, StatefulSets | s | Scale |
| Deployments, DaemonSets, StatefulSets | r | Rolling restart |
| Deployments | z | Show ReplicaSets |
| ReplicaSets | ctrl-l | Roll back to this ReplicaSet |
| Port-forwards, Services | b | Run / stop a benchmark |
| CronJobs | t | Trigger a job now |
| Nodes | u / r | Cordon or uncordon / drain |
| Namespaces | u | Use this namespace |
| ServiceAccounts, PVCs, Secrets, ConfigMaps | u | Show what uses this one |
| Logs | 0–6 | Tail, head, 1m, 5m, 15m, 30m, 1h |
| Logs | w, t, s, f | Wrap, timestamps, autoscroll, fullscreen |
Where k9s keeps its files#
k9s follows the XDG base-directory convention, so its files are spread over a config directory, a data directory and a state directory. The main config directory, per the k9s configuration docs:
| OS | Config directory |
|---|---|
| Linux / Unix | ~/.config/k9s |
| macOS | ~/Library/Application Support/k9s |
| Windows | %LOCALAPPDATA%\k9s |
That's where config.yaml, aliases.yaml, hotkeys.yaml, plugins.yaml, views.yaml and the skins/ folder go. Per-context settings (including benchmarks.yaml) sit in the data directory under clusters/, and logs, benchmark reports and screen dumps go to the state directory. If you've exported XDG_CONFIG_HOME and friends yourself, k9s uses those, which is why some Macs end up with ~/.config/k9s. Don't guess: ask k9s.
k9s info
On a Linux machine with no XDG variables set, the output looks like this (logo trimmed, home directory shortened):
Version v0.51.0
Config ~/.config/k9s/config.yaml
Custom Views ~/.config/k9s/views.yaml
Custom Jumps ~/.config/k9s/jumps.yaml
Plugins ~/.config/k9s/plugins.yaml
Hotkeys ~/.config/k9s/hotkeys.yaml
Aliases ~/.config/k9s/aliases.yaml
Skins ~/.config/k9s/skins
Context Configs ~/.local/share/k9s/clusters
Logs ~/.local/state/k9s/k9s.log
Benchmarks ~/.local/state/k9s/benchmarks
ScreenDumps ~/.local/state/k9s/screen-dumps
To keep everything in one folder, for a dotfiles repo say, set K9S_CONFIG_DIR. k9s then reads config, skins, plugins and per-context files from that one directory. Logs are the exception, with their own K9S_LOGS_DIR variable.
Make it yours: aliases, hotkeys, plugins, skins#
All four are plain YAML files in the config directory. Hotkeys reload on save. After editing the others, restart k9s.
Aliases#
An alias maps a short command to a resource (as group/version/resource) or to a whole command line. Add yours to aliases.yaml next to the starter entries:
aliases:
cm: v1/configmaps
webpods: pod /web
Now :webpods opens the pods view already filtered to the walkthrough's pods.
Hotkeys#
Hotkeys bind a key combination to a command, so the views you open fifty times a day take one press. Put them in hotkeys.yaml. Saved hotkeys appear in the help screen right away, and they can't take a key that k9s already uses for something else:
hotKeys:
shift-1:
shortCut: Shift-1
description: Pods
command: pods
shift-2:
shortCut: Shift-2
description: Port-forwards
command: pf
Plugins#
A plugin runs an external command against the selected row. k9s fills in variables such as $NAME, $NAMESPACE and $CONTEXT before it runs, so this plugins.yaml entry gives the pods view a shift-e key that streams the selected pod's events (ctrl-c stops the watch):
plugins:
pod-events:
shortCut: Shift-E
description: Pod events
scopes:
- pods
command: kubectl
background: false
args:
- events
- --context
- $CONTEXT
- --namespace
- $NAMESPACE
- --for
- pod/$NAME
- --watch
The plugin docs cover the remaining fields (confirm, dangerous to hide a plugin in read-only mode, and input prompts). Before writing your own, check the repository's community plugins folder. Helm, Flux and Argo are covered.
Skins#
A skin is a colour scheme in skins/<name>.yaml under the config directory. Grab one from the skins folder in the k9s repo (dracula, nord, solarized-light and around thirty more), drop it in, and name it without the extension:
# config.yaml
k9s:
ui:
skin: dracula
The same skin key in a context's own config gives that cluster a different look, which is a cheap guard against running a command in the wrong place. Set K9S_SKIN=dracula in your shell to override both. If the named file isn't found, k9s keeps its stock colours.
Common mistakes#
- Pressing ctrl-b to benchmark. Older guides, and one paragraph of the README, still say ctrl-b. In current k9s the key is a plain
b, listed as Benchmark Run/Stop in the port-forward view's help. - Wondering where your forward went. Port-forwards die with the session. A forward you need all afternoon belongs in a separate
kubectl port-forwardrunning in its own terminal. - No CPU or MEM columns. Nothing's broken: the cluster doesn't serve the Metrics API yet, and installing metrics-server from the prerequisites above brings the columns back.
- Using ctrl-k on a bare pod. Pods owned by a ReplicaSet come back. A pod you created with
kubectl rundoesn't. - Editing the wrong config.yaml. If changes don't stick, run
k9s info; you're probably editing a file in a directory k9s doesn't read.
Clean up#
Quit with :q (any port-forwards close with it), then remove the walkthrough objects:
kubectl delete service web
kubectl delete deployment web
Or do it from inside k9s: :svc, select web, ctrl-d, then the same in :dp. Leave metrics-server in place if you'll keep using k9s.
Common questions#
A few things people ask once they've used k9s for a day.
Is k9s safe to point at a production cluster?#
It can do exactly what your kubeconfig user can do, no more. Start it with --readonly, or set readOnly: true in that context's config, and the delete, kill, edit and other mutating actions are disabled. Plugins marked dangerous disappear too.
Does switching namespace in k9s change kubectl's namespace?#
No. k9s keeps the active namespace and view in its own per-context config, and switching contexts inside k9s happens in memory. It writes to your kubeconfig only when you rename or delete a context from the :ctx view.
Does it work with custom resources?#
Yes. Anything the API server serves is browsable by name, so once an operator installs its CRDs, typing the resource's plural or short name after : opens them like any built-in type. You can pick the columns for those views in views.yaml.
Next in the book#
Keep k9s open in a second terminal from here on. The next tools chapters install controllers that create a lot of objects you didn't write yourself, and watching them appear in :pulses or :xray makes them far easier to follow. GitOps and Argo is a good place to try that, and the Kubernetes Service chapter reads differently once :svc and Enter show you which pods each Service picks.
Further watching#
- Video by Anais Urlichs (the original 100 Days of Kubernetes series)