Watching a rollout with plain kubectl usually means three terminal panes and a lot of pasted pod names. One pane runs get pods -w, another tails logs, and the third is where you copy names with random suffixes. k9s puts all of that in one terminal screen that refreshes every two seconds, so you can move from a crashing pod to its logs, its describe output and a shell with single keys. The hard part is not the tool. It is remembering which key does what, and knowing how to install k9s so the binary on your PATH is the one you think it is.
This page is both the install guide and the k9s cheat sheet. We cover every install path we could prove (the release .deb, the tarball, Homebrew, webi, Chocolatey and Scoop), then the commands and hotkeys taken straight from the ? screen, readonly mode for production, skins, and plugins with the new input fields. I ran through this in November 2026 on Ubuntu 26.04 with k9s v0.51.0 against a single-node k3s cluster, and it works end to end.
k9s cheat sheet: commands, hotkeys and filters
Every row below was pressed or typed in k9s v0.51.0 on the lab cluster, and the hotkey tables match what the built-in help screen prints in the pod view. Keys change per view, so the ? key is always the final word on the screen you are looking at. A deployment view, for example, offers scale and restart where the pod view offers shell and logs.
Launch flags
| Command | What it does |
|---|---|
k9s | Start in the current kubeconfig context and the last namespace used |
k9s -n shop | Start in one namespace |
k9s -A | Start in all namespaces |
k9s --context prod | Pick a kubeconfig context at launch |
k9s -c pods | Open a resource view first instead of the last one used |
k9s --readonly | Hide every key that changes the cluster |
k9s --write | Override a readOnly setting from config for this run |
k9s -r 5 | Refresh every 5 seconds (default 2) |
k9s info | Print config, plugin, skin and log file paths |
k9s version -s | Short version, commit and build date |
Command mode (press colon first)
| Command | What it does |
|---|---|
:pods | Pods in the current namespace (also :po) |
:deploy shop | Deployments in the shop namespace |
:pods kube-system | Jump to another namespace in one command |
:ns | Namespaces |
:ctx | Kubeconfig contexts; Enter switches |
:nodes | Nodes with CPU and memory use |
:pvc | Persistent volume claims |
:xray deploy shop | Tree of deployments, pods and containers |
:pulses | Live resource counters and CPU, memory graphs |
:pf | Active port-forwards |
:aliases | Every resource and its short names (or Ctrl-A) |
:q | Quit k9s |
Pod view hotkeys
| Key | What it does |
|---|---|
0 1 2 | Switch to a favourite namespace (0 is all) |
Enter | Open the pod’s containers |
d | Describe |
y | Show the YAML |
e | Edit in $EDITOR |
l | Logs |
p | Logs of the previous container run |
s | Shell into the container |
a | Attach to the container |
Shift-F | Port-forward dialog |
f | Show the pod’s port-forward |
Ctrl-D | Delete (asks first) |
Ctrl-K | Kill at once, no grace period and no prompt |
i | Set a new container image |
t | Transfer files to or from the pod |
o | Jump to the node the pod runs on |
Shift-J | Jump to the owner (ReplicaSet, Job) |
w | Warp to all pods in this pod’s namespace |
c / n | Copy the name / the namespace |
Ctrl-Z | Toggle faults: show only unhealthy pods |
Ctrl-W | Toggle wide columns |
Shift-S / Shift-A | Sort by status / by age |
General and navigation keys
| Key | What it does |
|---|---|
? | Help for the current view |
: | Command mode |
/ | Filter mode |
Esc | Back, or clear the filter |
Ctrl-A | Aliases list |
Space | Mark a row (bulk delete or kill) |
Ctrl-\ | Clear all marks |
j / k | Down / up |
g / Shift-G | Top / bottom of the list |
Ctrl-F / Ctrl-B | Page down / page up |
[ / ] | History back / forward |
- | Last used command |
Ctrl-S | Save the current view to a file |
Ctrl-E | Toggle the header |
Filters (press slash first)
| Filter | What it matches |
|---|---|
/web | Rows containing “web” |
/!web | Rows not containing “web” |
/Error | Any column text, so status works too |
/-l app=web | Label selector |
/-l app!=web | Negative label selector |
/-f wrk | Fuzzy match on the name |
Logs view keys
| Key | What it does |
|---|---|
0 | Tail (the default) |
1 | Head |
2 to 6 | Last 1m, 5m, 15m, 30m, 1h |
s | Toggle autoscroll |
w | Toggle line wrap |
t | Toggle timestamps |
f | Toggle full screen |
Ctrl-S | Save the log buffer to a file |
Shift-C | Clear the screen |
Node view keys (careful)
| Key | What it does |
|---|---|
c | Cordon the node |
u | Uncordon the node |
r | Open the drain dialog |
Those three node keys are why the readonly section below matters. On a production cluster a stray r in the node view is one Enter away from evicting every pod on that node, and we came close to doing exactly that in the lab (the story is in the last section). For plain kubectl one-liners that k9s does not cover, keep our kubectl cheat sheet open next to this one.
Step 1: Install k9s on Ubuntu, Debian and other Linux
Ubuntu and Debian do not package k9s in their own archives (apt-cache policy k9s returned nothing on Ubuntu 26.04), so on Linux you pick between four upstream routes. All four gave us the same v0.51.0 binary. The one route we would avoid is the snap, because snap info k9s still lists v0.27.4 from June 2023 as latest/stable, which is about three years behind the current release.
Option A: the release .deb (Ubuntu, Debian)
The GitHub release ships .deb, .rpm and .apk packages for amd64, arm64 and a few other architectures, which makes this the cleanest path on Debian-family systems because apt tracks the file and can remove it later. Pull the newest tag from the GitHub API so the commands do not need editing on the next release:
VER=$(curl -s https://api.github.com/repos/derailed/k9s/releases/latest | grep -Po '"tag_name": "\K[^"]+')
echo $VER
curl -sLO https://github.com/derailed/k9s/releases/download/${VER}/k9s_linux_amd64.deb
curl -sLO https://github.com/derailed/k9s/releases/download/${VER}/checksums.sha256
Check the download against the published checksum before installing it. The --ignore-missing flag skips the 30-odd other assets listed in the file:
sha256sum --ignore-missing -c checksums.sha256
sudo apt install -y ./k9s_linux_amd64.deb
k9s version -s
The checksum line has to say OK, and the version output should match the tag you downloaded. On our VM the package installs a single 132 MB binary at /usr/bin/k9s and nothing else apart from a copyright file:

On an arm64 box such as a Raspberry Pi 5 or an Ampere VM, swap amd64 for arm64 in the file name. Fedora, Rocky and openSUSE users can grab k9s_linux_amd64.rpm from the same release page and install it with dnf or zypper; we did not run the rpm in this lab.
Option B: the tarball (any distro)
The tarball works on every distribution, including immutable ones where you would rather not touch the package database. It reuses the VER variable and the checksum file from above:
curl -sLO https://github.com/derailed/k9s/releases/download/${VER}/k9s_Linux_amd64.tar.gz
sha256sum --ignore-missing -c checksums.sha256
tar -xzf k9s_Linux_amd64.tar.gz k9s
sudo install -m 0755 k9s /usr/local/bin/k9s
The archive names use Linux with a capital L while the packages use linux, and only the capitalised name appears in checksums.sha256. GitHub serves the lowercase URL too, but the file then lands as k9s_linux_amd64.tar.gz, and the checksum step stops with sha256sum: checksums.sha256: no file was verified and exit code 1. Keep the capital L and the check covers the archive you are about to extract.
Option C: Homebrew on Linux (Linuxbrew)
If you already run Homebrew on Linux for other CLI tools, the k9s tap is the easiest way to keep it updated with everything else. Load brew into the shell, then install from the derailed tap:
eval "$(/home/linuxbrew/.linuxbrew/bin/brew shellenv)"
brew install derailed/k9s/k9s
The install took 19 seconds on our VM and finished with the usual Homebrew summary line.
🍺 /home/linuxbrew/.linuxbrew/Cellar/k9s/0.51.0: 6 files, 132.2MB, built in 2 seconds
Homebrew core also carries a bottled k9s formula at the same version (brew info homebrew/core/k9s printed stable 0.51.0), so plain brew install k9s works too, and brew warns that the tap formula shadows the core one when both are present. One catch for Proxmox users: Homebrew refused to start at all on our first try because the VM used the default kvm64 CPU type, which has no SSSE3. The fix is in the troubleshooting section at the end.
Option D: webi (no sudo)
Webi installs into your home directory, which is handy on a shared jump host where you have no root. We download the script first rather than piping it into a shell, so you can read it before it runs:
curl -sS -o webi-k9s.sh https://webi.sh/k9s
sh webi-k9s.sh
source ~/.config/envman/PATH.env
Webi fetched the same upstream tarball from the GitHub release, and the end of its output shows where it put things.
Saved as ~/Downloads/webi/k9s/0.51.0/k9s_Linux_amd64.tar.gz
Extracting ~/Downloads/webi/k9s/0.51.0/k9s_Linux_amd64.tar.gz
Installing to ~/.local/opt/k9s-v0.51.0/bin/k9s
Installed 'k9s v0.51.0' as ~/.local/bin/k9s
Whichever route you take, pick one and stick to it. We installed all four on the same VM to compare them, and which -a k9s then listed /usr/local/bin/k9s ahead of /usr/bin/k9s, so an apt upgrade of the .deb would have changed nothing you actually run. That kind of shadowing is how people end up “upgrading” k9s and still seeing the old version.
Step 2: Install k9s on Windows and macOS
Windows gets k9s from Chocolatey or Scoop. We ran both on a Windows Server 2025 VM through the Proxmox guest agent, which runs PowerShell as the SYSTEM account with admin rights. WinGet ships with App Installer on Windows 10, 11 and Server 2025, but it registers per user at first logon and was not available to SYSTEM, so we did not test the winget route here. If you already have Chocolatey, one command does it from an elevated PowerShell.
choco install k9s -y
Chocolatey marks the package as approved, extracts the official Windows zip and creates a shim so k9s works from any prompt:
k9s v0.51.0 [Approved]
k9s package files install completed. Performing other installation steps.
Extracting 64-bit C:\ProgramData\chocolatey\lib\k9s\tools\k9s_Windows_amd64.zip to C:\ProgramData\chocolatey\lib\k9s\tools...
C:\ProgramData\chocolatey\lib\k9s\tools
ShimGen has successfully created a shim for k9s.exe
The install of k9s was successful.
Deployed to 'C:\ProgramData\chocolatey\lib\k9s\tools'
Scoop is the better fit for a developer laptop, because it installs per user and needs no admin rights once Scoop itself is set up from scoop.sh. k9s lives in the default main bucket:
scoop install k9s
Scoop checks the zip’s hash, then sets two user environment variables that you will care about later.
Installing 'k9s' (0.51.0) [64bit] from 'main' bucket
Downloading https://github.com/derailed/k9s/releases/download/v0.51.0/k9s_Windows_amd64.zip (40.4 MB)...
Checking hash of k9s_Windows_amd64.zip... OK.
Extracting k9s_Windows_amd64.zip... Done.
Linking ~\scoop\apps\k9s\current => ~\scoop\apps\k9s\0.51.0
Creating shim for 'k9s'.
Setting user environment variable: K9S_CONFIG_DIR = C:\WINDOWS\system32\config\systemprofile\scoop\persist\k9s/config
Setting user environment variable: K9S_LOGS_DIR = C:\WINDOWS\system32\config\systemprofile\scoop\persist\k9s/logs
'k9s' (0.51.0) was installed successfully!
That K9S_CONFIG_DIR line is the part to remember. A Scoop install keeps config.yaml, plugins and skins under scoop\persist\k9s\config, not in the usual per-user k9s folder, so run k9s info before you copy plugin files across. Our path shows the SYSTEM profile because we drove the VM through the Proxmox guest agent; under a normal account it sits in your own profile. The installed binary reported v0.51.0. The Scoop shim would not start under the SYSTEM account (“Cannot open shim file for read.”), which is a side effect of that unusual account rather than something you will see as a normal user.
On macOS the Homebrew command is the same one we ran on Linux, brew install derailed/k9s/k9s (or brew install k9s from core), because both platforms read the same formula and the release publishes Darwin builds for Intel and Apple Silicon. We had no Mac in this lab, so the macOS install itself was not run here. The keys and screens in the rest of this guide are the same everywhere, but the file paths are not: the paths below are the Linux ones, and macOS and Windows keep k9s config elsewhere, so run k9s info to see yours before you copy skins or plugins.
Step 3: Point k9s at a cluster
k9s reads the same kubeconfig as kubectl: $KUBECONFIG if it is set, otherwise ~/.kube/config. If kubectl get nodes works, k9s will work. Our lab cluster was single-node k3s on a throwaway VM, installed from the downloaded script with the kubeconfig made readable so we could copy it without sudo. Two cautions before you copy this block: the cp overwrites any ~/.kube/config you already have, so back yours up first, and --write-kubeconfig-mode 644 leaves cluster-admin credentials readable by every user on the box, which is fine on a lab VM and wrong on a shared host.
curl -sfL -o k3s-install.sh https://get.k3s.io
sudo INSTALL_K3S_EXEC="--write-kubeconfig-mode 644" sh k3s-install.sh
mkdir -p ~/.kube
cp /etc/rancher/k3s/k3s.yaml ~/.kube/config
chmod 600 ~/.kube/config
kubectl get nodes -o wide
The node came up Ready about 22 seconds after the installer returned. One node is plenty for learning k9s; if you want several boards behind it, our Raspberry Pi k3s cluster build uses the same installer.
NAME STATUS ROLES AGE VERSION INTERNAL-IP EXTERNAL-IP OS-IMAGE KERNEL-VERSION CONTAINER-RUNTIME
homelab-k3s Ready control-plane 22s v1.36.5+k3s1 10.0.1.50 <none> Ubuntu 26.04.1 LTS 7.0.0-15-generic (amd64) containerd://2.3.4-k3s1.36
An empty cluster makes a dull k9s screen, so we gave it something to look at: three nginx replicas behind a service, a redis cache, and a worker that exits with an error every few seconds. The worker is deliberate, because the interesting part of k9s is how fast it gets you from a red row to the reason:
kubectl create namespace shop
kubectl -n shop create deployment web --image=nginx:1.29-alpine --replicas=3 --port=80
kubectl -n shop expose deployment web --port=80
kubectl -n shop create deployment cache --image=redis:8-alpine
kubectl -n shop create deployment worker --image=busybox:1.37 -- sh -c "echo starting worker; sleep 5; echo FATAL: cannot reach queue at amqp://rabbitmq:5672; exit 1"
Before launching the UI, run k9s info. It prints every file k9s reads, which saves guessing between ~/.config/k9s and ~/.local/share/k9s later when you add skins, plugins and per-cluster settings:
k9s info
On a fresh install the last line is an ERROR saying the config file does not exist. That is harmless, because k9s writes config.yaml with its defaults the first time the UI starts:

Now start k9s in the namespace you care about. With several clusters in one kubeconfig, add --context, or switch later with :ctx inside the UI:
k9s -n shop
The header shows the context, the k9s and Kubernetes versions and live CPU and memory for the cluster, with the hotkeys for the current view next to it. The worker row turns red with CrashLoopBackOff or Error, which is the first thing your eye lands on:

The [RW] next to the context name means write mode, so every destructive key works. If the objects on that screen (ReplicaSets, owners, init containers) are still a blur, Nigel Poulton’s The Kubernetes Book (paid link) is the one we point new team members to; it was $49.99 and in stock when we checked on 1 November 2026. Our Kubernetes books guide compares it with ten others.
Step 4: Read logs, open a shell and port-forward
This is the loop you will run most often. Type /worker and Enter to narrow the list to the failing pod, then press l for its logs. k9s follows the stream and shows when the container exits:

The FATAL line names the real problem, a RabbitMQ service the worker cannot reach because it does not exist, and the “stream closed: EOF” line tells you the container died rather than went quiet. When a pod restarts fast, the current container may not have logged anything yet; p shows the previous run instead, which is the same as kubectl logs --previous. Press Esc to go back, then d for the describe output with the container state and events, the same text kubectl describe pod prints.
For a shell, select a healthy pod and press s. k9s opens a shell inside the container, and a banner tells you which pod and container you are in:
<<K9s-Shell>> Pod: shop/web-6946d795dc-mjlz5 | Container: nginx
/ # nginx -v; hostname
nginx version: nginx/1.29.8
web-6946d795dc-mjlz5
/ #
Typing exit drops you back into k9s on the same screen. Port-forwarding is Shift-F on a pod, and here is where the defaults bite: k9s pre-fills the local port with the container port, 80 for nginx, and a normal user cannot bind a port below 1024. Change the local port before you confirm:

- 1Local Port
- 2OK
- Tab to the Local Port field, delete 80 and type 8080.
- Tab to OK and press Enter. The forward now shows up under
:pf.
From a second terminal on the same machine, you can check that the tunnel answers.
curl -sI http://localhost:8080/ | head -2
nginx answered through the forward, which proves the tunnel and the pod are both fine.
HTTP/1.1 200 OK
Server: nginx/1.29.8
The forward binds to localhost by default (portForwardAddress: localhost in config.yaml), so it is not reachable from other machines on your network unless you change that address. That default is the right one for a laptop, and changing it on a shared jump host would expose the pod to everyone who can reach that host.
Step 5: Find things fast with filters, xray and pulses
Filters are what make k9s faster than kubectl on a busy cluster. A plain /web matches any column, so /Error also works as a status filter, and /!web inverts the match. The label form /-l app=web sends a real label selector to the API, which is the one to use on a namespace with hundreds of pods. The fuzzy form is looser than you might expect: /-f wrk matched our worker pod, but it also matched web-6946d795dc-rxj2k, because w, r and k appear in that name in order.
Xray answers the question “what does this deployment actually own?” without three separate get commands. :xray deploy shop draws deployments, their pods, service accounts and containers as one tree, with the broken branch flagged:

The counts in brackets read available, desired and unavailable replicas: web(1) [3/3/0] is healthy, while worker(1) [0/1/1] carries the TOAST marker that k9s uses for anything failing. Press Enter on a node in the tree to jump to that resource. :pulses is the opposite view, a namespace-wide dashboard of resource counters with CPU and memory graphs along the bottom:

Pulses needs a wide terminal to be useful. At 150 columns by 32 rows the counters were readable but the CPU and memory graphs were squeezed into a few lines, so give it a full-screen window or a large tmux pane. For a browser-based view of the same cluster, Headlamp and the other tools in our Kubernetes UI comparison are the alternatives we would look at.
Step 6: Run k9s in readonly mode on production
Readonly mode is the single most useful setting if you point k9s at anything that matters. Start it with --readonly and the context flag changes from [RW] to [R], and the menu loses every key that writes to the cluster:
k9s --readonly -n shop
Compare this menu with the one in Step 3. Attach, Delete, Edit, Kill, Shell, Set Image, Sanitize and Transfer are gone, and pressing Ctrl-D or e does nothing at all:
![k9s Cheat Sheet (PDF): Install, Hotkeys, Skins and Plugins 8 k9s readonly mode with the [R] flag and no delete, edit, shell or kill keys in the pods menu](https://computingforgeeks.com/wp-content/uploads/2026/11/k9s-readonly-mode-pods-menu-1024x453.png)
We checked what stays available, because readonly is not the same as harmless. Here is what a readonly session still lets you do and what it takes away:
- Still there: logs, previous logs, describe, YAML, jump owner
- Still there: Port-Forward, so you can still tunnel into a production pod
- Gone from pods: attach, delete, edit, kill (Ctrl-K, which never asks), shell, set image, transfer
- Gone from nodes: cordon, uncordon, drain
- Hidden: plugins marked
dangerous: true(checked with the PVC resize plugin in Step 8)
Treat readonly as a guard against fat fingers, not as access control. If a teammate must never delete pods in production, that rule belongs in RBAC on the cluster, because nothing stops them from starting k9s with --write or running kubectl directly.
Typing the flag every time is easy to forget, so set it per cluster instead. k9s keeps a config file for each context under ~/.local/share/k9s/clusters/<cluster>/<context>/ (our context was called default):
vim ~/.local/share/k9s/clusters/default/default/config.yaml
Add readOnly: true under the k9s: key. Your file will look much like ours did after the edit.
k9s:
cluster: default
readOnly: true
namespace:
active: shop
lockFavorites: false
favorites:
- shop
- default
view:
active: v1/persistentvolumeclaims shop
featureGates:
nodeShell: false
proxy: null
With that in place a plain k9s opened in [R] mode, and k9s --write overrode it back to [RW] for one session. The precedence runs global readOnly in config.yaml first, then the context file, then the command-line flags, so the flag always wins. A sensible pattern is a readonly context file for every production cluster and write mode only on dev and lab contexts.
While you are in config files, it helps to know what you would be changing. The global ~/.config/k9s/config.yaml that k9s generated on first start held these defaults:
refreshRate: 2(seconds between screen refreshes)maxConnRetry: 5before k9s gives up on the API serverapiServerTimeout: 2m0sper API calltail: 100log lines loaded,buffer: 5000keptportForwardAddress: localhost
Do you want faster refreshes than every two seconds? You cannot get them, because the k9s source caps the refresh rate at a 2-second minimum and quietly raises anything lower. The v0.51.0 release also raised the client’s API request rate from 5 to 50 queries per second (PR #3988), which is why large clusters feel snappier after the upgrade; there is no k9s setting for that value.
Step 7: Change the k9s skin
Skins are YAML colour files. k9s looks for them in the Skins path that k9s info printed, ~/.config/k9s/skins on our machine, and the repository ships 35 ready-made ones, including the One Light skin added in v0.51.0. Download the ones you want into that directory:
VER=$(curl -s https://api.github.com/repos/derailed/k9s/releases/latest | grep -Po '"tag_name": "\K[^"]+')
cd ~/.config/k9s/skins
curl -sLO https://raw.githubusercontent.com/derailed/k9s/${VER}/skins/one-light.yaml
curl -sLO https://raw.githubusercontent.com/derailed/k9s/${VER}/skins/dracula.yaml
To try a skin without editing anything, set K9S_SKIN for one run. The name you pass is the file name without .yaml.
K9S_SKIN=one-light k9s -n shop -c pods
One Light suits a light terminal theme, where the default skin’s pale text is hard to read. The red worker row still stands out:

Liked it enough to keep? Make a skin permanent in the global config.
vim ~/.config/k9s/config.yaml
Add a skin: line under ui:. These are the lines around our change, and the rest of the file stays as k9s wrote it.
portForwardAddress: localhost
ui:
skin: dracula
enableMouse: false
A skin: key in a context’s own config.yaml overrides the global one, which is a cheap safety trick: give production a loud red skin and you will always know which cluster you are on. The --invert flag flips a dark skin to light (or the reverse) without a second file, if you only need the contrast changed.
Step 8: Add k9s plugins with input fields, and custom hotkeys
Plugins bind a key to any command, with variables such as $NAME, $NAMESPACE and $CONTEXT filled from the selected row. Since v0.51.0 a plugin can also ask for values first through an inputs list, with up to five fields of type string, number, bool or dropdown. Each value reaches the command as $INPUT_ plus the field name in capitals. We wrote a logs plugin that asks for a time window, a line count and whether to read the previous container:
vim ~/.config/k9s/plugins.yaml
Paste the plugin definition. This is the exact file loaded in the lab.
plugins:
logs-window:
shortCut: Shift-L
description: Logs with options
scopes:
- pods
command: bash
background: false
args:
- -c
- >-
kubectl logs $NAME -n $NAMESPACE --context $CONTEXT
--since=$INPUT_SINCE --tail=$INPUT_TAIL --previous=$INPUT_PREVIOUS;
read -p "Press Enter to return to k9s..."
inputs:
- name: since
label: Time window
type: dropdown
required: true
default: 1h
options:
- 5m
- 1h
- 24h
- name: tail
label: Lines to show
type: number
required: true
default: "50"
- name: previous
label: Previous container
type: bool
required: true
default: "false"
Two details in the plugin schema caught our attention. A field with a default must also say required: true, which the JSON schema enforces even though one example in the k9s README pairs a default with required: false, and the default has to parse for its type, meaning a number for number fields, true or false for bool fields, one of the options for a dropdown. We quoted the number and bool defaults, though k9s accepts them unquoted too. k9s picked the file up on the next start, and Shift-L showed as “Logs with options” in the pod menu. Pressing it on the worker pod opened this form:

- 1Time window
- 2Lines to show
- 3Previous container
- 4OK
- Time window is the dropdown; leave it on 1h or pick another option.
- Lines to show takes a number, 50 by default.
- Previous container is the bool field. Tab to it and press Space, which marks it with an X.
- Tab to OK and press Enter.
Because the plugin has inputs, k9s shows the final command and asks for confirmation before running it (plugins with inputs default to confirm: true). The confirm dialog read kubectl logs worker-9bc599f88-zcb5k -n shop --context default --since=1h --tail=50 --previous=true, and after OK the terminal printed the crashed container’s two log lines (starting worker and the FATAL queue error) and waited for Enter. One earlier run with Previous container ticked printed unable to retrieve container logs for containerd://... instead, because the pod was mid-restart and the previous container’s log was not there yet; running it again a minute later worked. That preview is worth reading every time, since it is the only place you see how the inputs were substituted.
k9s also loads every file in a plugins/ directory next to plugins.yaml, so community plugins can be dropped in one per file. The PVC resize plugin from the k9s repository uses a string input for the new size. The -f flag makes curl fail on a 404 instead of saving an error page as your plugin file:
VER=$(curl -s https://api.github.com/repos/derailed/k9s/releases/latest | grep -Po '"tag_name": "\K[^"]+')
mkdir -p ~/.config/k9s/plugins
curl -fsSL -o ~/.config/k9s/plugins/pvc-resize.yaml https://raw.githubusercontent.com/derailed/k9s/${VER}/plugins/pvc-resize.yaml
You need a PVC to try it on; our lab had a 1Gi claim named data on the k3s local-path class, mounted by a busybox pod. In :pvc, the r key now reads “Resize PVC” and asks for a size such as 2Gi. The plugin is marked dangerous: true, and that is the flag readonly mode uses to hide it: in a --readonly session the same PVC view had no Resize entry at all. On k3s the resize itself failed, for a reason covered in the last section.
Hotkeys are simpler than plugins. They map a key to a k9s command, so you can jump straight to a view you use all day:
vim ~/.config/k9s/hotkeys.yaml
We bound Shift-0, Shift-1 and Shift-2 to pods, deployments and the deployment xray.
hotKeys:
shift-0:
shortCut: Shift-0
description: Viewing pods
command: pods
shift-1:
shortCut: Shift-1
description: View deployments
command: dp
shift-2:
shortCut: Shift-2
description: Xray deployments
command: xray deploy
Pressing Shift-2 from the pod view opened the Xray-Deployments tree for the shop namespace straight away. According to the k9s README the file reloads on its own, and a copy under a context’s directory in ~/.local/share/k9s/clusters/ applies to that cluster only. We ran every k9s session on this page inside a 150 by 32 tmux pane over SSH, and our tmux cheat sheet covers that setup if you want k9s to survive a dropped connection.
What broke in the lab, and the fixes
Every error below came up while we built this page. None of them are k9s bugs, but each one cost time the first time we saw it.
Error: “Homebrew’s x86_64 support on Linux requires a CPU with SSSE3 support!”
Homebrew died on its first brew update with this message. Our Proxmox VM was on the default kvm64 CPU model, which /proc/cpuinfo reported as “Common KVM processor” with zero SSSE3 flags. Setting the VM’s CPU type to host and rebooting the VM from Proxmox (so QEMU restarts with the new CPU model) exposed the real CPU, after which grep -c ssse3 /proc/cpuinfo returned 4 and the brew install went through. The .deb, tarball and webi routes do not care about the CPU model.
Error: “port 80 is not available on host”
This flashed at the bottom of the screen when we accepted the port-forward dialog with its pre-filled local port of 80. The k9s log (~/.local/state/k9s/k9s.log) had the detail, a warning that port 80 was not available on localhost, because unprivileged users cannot bind ports below 1024. Use 8080 or any free high port, as in Step 4.
Error: “only dynamically provisioned pvc can be resized and the storageclass that provisions the pvc must support resize”
The resize plugin only reported “command failed. Check k9s logs: exit status 1”. Running the same kubectl patch by hand printed the real message above, a Forbidden error from the API server. The k3s local-path storage class does not set allowVolumeExpansion, so no PVC on it can grow. The plugin works on storage classes that allow expansion, such as most cloud CSI drivers. When any plugin fails with that generic line, copy the command from its confirm dialog and run it yourself to see the error.
A filter typed without the slash opened the node drain dialog
This one is the reason readonly mode gets its own step. We meant to filter for the worker pod, but the slash never registered, so k9s read the letters of worker as hotkeys. The o jumped to the node view, and in the node view r opened the Drain dialog. What saved the node was luck: the l we pressed next, meaning to open logs, landed in the Timeout field and turned the default 5s into “5sl”, and k9s refused it with time: unknown unit "sl" in duration "5sl" (the line is still in ~/.local/state/k9s/k9s.log). Had the timeout parsed, a few more stray keystrokes could have reached the OK button and drained the only node in the cluster. Check that the prompt at the top shows the filter icon before you type, and run production clusters in readonly mode.
k9s opens on the wrong view, or an upgrade did not change the version
k9s remembers the last view per context, so after we looked at nodes, k9s -n shop opened straight into nodes again. Add -c pods (or any resource) to choose the first screen. For the version problem, run which -a k9s: with more than one install method on the machine, the first path listed is the binary you run, and it may not be the one your package manager just upgraded.