๐Ÿช yoink

TUI

Keybind table for every TUI pane, log piping, and how drift is shown in the cell view.

yoink tui

Heavily inspired by k9s and lazydocker, but pointed at remote hosts you deploy to instead of just the local docker daemon. Keyboard-driven, real-time, and aware of yoink-specific concepts (services, drift, sealed secrets, deploy history) on top of the everyday docker introspection an operator wants when something is on fire.

yoink TUI dashboard

yoink TUI container detail

What it shows

panewhat it's for
Dashboardone row per yoink-managed container across every host, with drift, CPU/mem, and exit-code colour. The "is anything broken right now" view.
Hostsper-host preflight (ssh, daemon version, kernel/OS) + aggregate CPU/mem across the host. Drill in for a per-host table.
HostDetailevery running container on a host (yoink-managed or not) with live stats, plus a rolling per-host docker-events panel at the bottom.
Servicesone row per configured service with replica count + image. Drill in for ServiceDetail (every replica across every host) โ†’ ServiceHistory (every past deploy โ†’ roll back from here).
ContainerDetailk9s-style "describe": image, command, env (secrets redacted), ports, mounts, networks, security profile (cap_drop / read_only / pids_limit), restart count, three 5-minute history charts (CPU%, Mem, mirrored net tx/rx), and a NET โ†“rx โ†‘tx cumulative line in the card. p opens a docker top modal listing in-container processes.
Logsa multiplexed live tail of every yoink-managed container (auto-piped through hl when present). / filters substring, g/G jump to top/bottom, y yanks the visible buffer to the system clipboard via OSC-52.
ContainerLogssame shape, scoped to one container.
Resourcesthree sub-tabs (Images, Volumes, Networks) covering everything lazydocker exposes. Per-row remove with d; per-tab P prune (A for the aggressive image prune that goes beyond <none>:<none>).
Secretsview / add / edit / remove individual sealed secrets without leaving the TUI; reuses the same on-disk format as yoink secrets edit.

Top-level navigation

keymode
dDashboard
hHosts
sServices
lLogs
RResources (Images / Volumes / Networks)
eEncrypted-secrets
aAudit log (merged operator + on-host JSONL)
Tab / Shift-Tabcycle modes forward / backward
?toggle help overlay (per-view keybinds)
q / Ctrl-Cquit

Operator gestures from the dashboard

keyaction
โ†‘ โ†“ / j knavigate
enterdrill into selected row (container detail)
icontainer inspect (security & limits, env, mounts, networks); also from HostDetail / ServiceDetail
KSIGKILL container (with confirmation)
S / X / Rstart / stop / restart container; from HostDetail or ContainerDetail
Ureconcile this service (with confirmation); drift-only services no-op
Areconcile all services (with confirmation)
Pprune stale + orphan containers (with confirmation)
!shell into container (bash then fallback to sh)
Bdebug sidecar (alpine in target's pid+net ns, for distroless / shell-less images)
Hservice deploy history; on a stopped row press r to roll back
~show drift detail for the focused service (image / tag / spec_hash / env keys / label keys)
fport-forward the focused service. Auto-mode: published path when the service has a matching publish: entry, else spawns an ephemeral alpine/socat sidecar that joins the service's docker network. Footer band stays visible across panes until closed.
o / Oopen the active port-forward URL in the system browser. Works in any view; falls back to the most-recently-opened tunnel when the focused row has no forward of its own.
Fclose every active port-forward (sidecars are force-removed; ssh children killed)
xtoggle eXited containers visible in the table
rrefresh
/filter substring (Esc clears)

Container detail (i from any list view)

The detail pane renders four kinds of information for a single container:

  1. Header card: image, state (colored), command, restart count, started/finished/exit-code timestamps. The right column has live CPU / MEM gauges and a NET โ†“rx โ†‘tx cumulative-bytes line ("how much has this thing transferred since start").
  2. 5-minute history charts: three side-by-side panels, each with the latest sampled value baked into its title so it's readable without squinting at the rightmost edge:
    • CPU % (cyan): own y-axis scaled to peak CPU
    • Mem (magenta): own y-axis. % when memory is capped, MB when uncapped
    • net tx / rx (yellow / green): btop-style mirrored: tx (outgoing) plots above the zero line, rx (incoming) mirrored below. The two series can never overlap. Title shows current rates: โ†‘1.2MB/s โ†“340KB/s. Y-axis labels carry the direction arrow on each side.
  3. Runtime + security blocks: published ports, mounts, attached networks, then cap_drop / cap_add / security_opt / read_only / pids_limit / effective user so you can see at a glance whether this container is hardened.
  4. Env + labels: sorted KEY=value with secret-ish keys (*TOKEN*, *SECRET*, *PASSWORD*, *API_KEY*, *PRIVATE_KEY*, *DSN*) auto-redacted; full label table including the yoink.* set.

Plus, at the bottom of the pane, a rolling tail of the container's logs.

History is collected for every container, all the time. A background poller samples docker stats for every running container across every configured host every 2 seconds, regardless of which view you're currently on. When you drill into a container's detail pane, the chart is already populated with up to 5 minutes of context instead of starting from zero. Stale entries (containers that stopped and aged out) are GC'd automatically.

keyaction
enter / lopen dedicated logs view
!exec a shell inside (bash โ†’ sh)
Bdebug sidecar (alpine sharing pid+net ns)
S / X / Rstart / stop / restart
KSIGKILL (with confirmation)
Ureconcile this service
pdocker top, shows in-container processes in a modal (Esc to close)
rrefresh
escback to host detail

If your image is distroless or otherwise has no shell, ! will fail. Fall back to B: the debug sidecar attaches an alpine container sharing the target's PID and network namespaces, so you can run ps, ss, cat /proc/<pid>/... against the target without modifying the production image. The sidecar auto-removes when you exit / Ctrl-D.

Drift detail (~)

When the dashboard / host detail / service detail / container detail view shows โš  on a row, press ~ to open a modal that explains what drifted, the same per-field diff yoink up --plan produces on the CLI side:

 drift: api on host-a (esc to close)
   image  ghcr.io/me/api  (unchanged)
   spec   a1b2c3d โ†’ e5f6a7b
    tag   v1.2.4
env:
  + DATABASE_POOL_SIZE
  ~ LOG_LEVEL
labels:
  - yoink.caddy.tls

Color-coded so the markers read at a glance: + green (added in desired), - red (removed from running), ~ yellow (changed). The hash and image columns highlight the running โ†’ desired transition in cyan when they differ; the line dims to (unchanged) when they match.

The fetch reuses diff::compute under the hood, so the modal's content is identical to yoink up --plan --service <name> against the same host. For services without a tag: pinned in config (typical for image: ghcr.io/you/api where CI provides the tag), the modal falls back to the running replica's tag so the diff isolates the env/label change instead of erroring on a missing tag, useful for "what changed since deploy?" without leaving the TUI.

Esc closes the modal. The underlying view stays put.

Hosts pane (h)

Per-host summary table:

colmeaning
hostuser@address from yoink.yaml
statusssh probe + Docker API handshake โ€” ok / unreachable (with classified hint for Tailscale auth, ssh-add reminders, etc.)
daemondocker server version, OS / kernel
cpu, memaggregate across all running containers (sum of docker stats) โ€” gauge-coloured

Enter drills into HostDetail, which shows every running container on the host (whether yoink manages it or not), with the same live CPU/mem cells, drift indicator, and per-container actions:

keyaction
โ†‘โ†“ / j kselect container
enterlive logs
icontainer detail (env, mounts, security, history charts, โ€ฆ)
!shell ยท B debug sidecar
S / X / Rstart ยท stop ยท restart
KSIGKILL (with confirmation)
Ureconcile this service
/filter substring ยท esc clears
rrefresh
escback to Hosts (when no active filter)

The events panel at the bottom of HostDetail collects live docker events for that host (start / stop / die / health-status / kill / oom / restart). Each row is a one-line summary timestamped with relative time. The ring keeps the last 200 events per host, long enough that an operator returning to the pane after a reconcile sees the full sequence of swaps, not just the final state.

Services pane (s)

Services lists every service in yoink.yaml with its current replica count and configured image. Enter opens ServiceDetail: one row per running replica across every host, with the same per-row container actions. H opens ServiceHistory: every yoink-managed container with yoink.service=<name> (running and exited), sorted newest-first by yoink.deployed-at. Pressing r on a row triggers a rollback confirmation pinned to that row's tag (same flow as yoink rollback --tag <value>).

Resources pane (R)

Three sub-tabs covering the introspection lazydocker users expect, fanned out across every configured host:

tabcolumnsactions
Images (i inside Resources)host ยท 12-char id ยท size ยท age ยท dangling ยท tagsd remove ยท P prune dangling ยท A prune all unused
Volumes (v)host ยท name ยท driver ยท mountpointd remove ยท P prune unused
Networks (n)host ยท name ยท driver ยท scope ยท internald remove ยท P prune unused

Tab / Shift-Tab cycles between the three sub-tabs (only inside Resources, not the top-level modes). / filters across host/name/tag substrings; partial fetch errors per host appear as a red footer ribbon rather than blanking the whole table. Dangling images sort to the top so they're trivial to prune.

There's intentionally no volume file browsing. Drop into the container with ! (or, for distroless containers, B for the debug sidecar) and use the shell. That's strictly more capable than the half-baked file UI lazydocker has, and it's what most operators reach for anyway.

Secrets pane (e)

View / add / edit / remove individual sealed secrets without leaving the TUI. Reuses the same on-disk format as yoink secrets edit and respects per-environment secrets.file: paths; the title bar shows which file is active.

keyaction
โ†‘โ†“ / j kselect key
rreveal/mask values
/filter substring
aadd a new secret (age provider only)
e / enteredit selected value
ddelete selected (with confirmation)
Esc / qback

When provider: command is configured the pane is read-only; rotation happens in whichever external tool the configured CLI talks to. When no age identity is available, the pane shows the failed-load reason + a remediation pointer. For bulk multi-line edits, drop to the CLI: yoink secrets edit.

Logs pane (l)

The multiplexed Logs view aggregates a live tail from every yoink-managed container across every host into one scrollable buffer. Each line is prefixed with the container name; lines from different hosts and services interleave in real time. Useful for "something just happened on prod, what was it?" when you don't yet know which service.

keyaction
โ†‘โ†“ / PgUp PgDnscroll line / page (auto-follow disengages while scrolling away from bottom)
g / G / Endjump to top / bottom (resumes auto-follow)
/begin filter input โ€” Enter applies, Esc cancels (live-typed, case-insensitive substring)
cclear the buffer (next ticks repopulate)
rrestart streams (re-opens log pipes if any died)
yyank the visible buffer to the system clipboard via OSC-52

The buffer is bounded at 5,000 lines; older lines fall off as new ones arrive. Filter doesn't shrink the buffer, only the rendered view.

ContainerLogs (single container)

Reached via Enter from any list view. Same shape as the multiplexed Logs pane, scoped to one container. Same keybindings; Esc returns to the parent. ! and B are also bound here for quick "tail logs โ†’ drop into shell" pivots.

Audit pane (a)

Merged operator + per-host JSONL audit log: every state-changing run yoink performed (deploy, rollback, prune, secrets rotate). Same data path as yoink audit log, with stable selection on event_id so a refresh keeps you on the row you were looking at.

keyaction
โ†‘โ†“ / j kselect row
Entertoggle detail panel (full event_id, deploy_id, actor, git SHA, log tail for DeployFailed)
/begin substring filter โ€” matches host, event name, summary, deploy_id, actor
rrefresh (parallel SSH per host + local operator log read)
Escclears active filter on first press, returns to Dashboard on second

The first column tags each row with operator (purple) or host (cyan) so you can tell at a glance which side of the merge a line came from.

Shell / debug sidecar (!, B)

Both gestures put you on a PTY inside the host's docker daemon with no SSH on top; yoink uses the Docker exec API and the TUI streams bytes both ways through a terminal-emulator parser.

! runs bash (falls back to sh) inside the existing container, equivalent to yoink shell <service> but staying in the TUI. Useful when the image has a shell and you want quick access to the running process's filesystem, env, etc.

B spins up an ephemeral alpine debug sidecar sharing the target container's PID and network namespaces. The fallback for distroless / scratch / shell-less images: you get ps, ss, cat /proc/<pid>/..., tcpdump, apk add whatever you need, without modifying the production image. The sidecar is --rm and force-removed when you exit / Ctrl-D, even if the TUI crashes.

key inside the shell viewaction
anythingforwarded into the in-shell process (Ctrl-C, Ctrl-D, arrow keys, โ€ฆ)
Ctrl-Qexit shell, back to the parent pane (yoink-side gesture)
?toggle help overlay (one yoink-side gesture even inside the shell)
exit / Ctrl-Dend the in-container shell normally

Window resizing flows through automatically; the panel size is sent to the daemon on every render so top / vim / etc. re-flow.

Progress modals

Long-running operations (reconcile-one, reconcile-all, prune) render a centred modal that streams the deploy event log live. The border colour reflects state: cyan while running, green on success, red on failure. The modal eats every key while the operation is in flight (so a stray j can't drive the underlying view); y yanks the modal's text to the clipboard at any time. Once finished, Esc / Enter dismisses.

reconcile-all (A from Dashboard) gets a richer status table at the top of the modal: one row per service, colour-coded by current state (waiting โ†’ pulling โ†’ healthcheck โ†’ swapping โ†’ done / failed), so when the wave-parallel deploy is mid-flight you can see all six services' progress at a glance instead of hunting through interleaved log lines.

Filter conventions

Every list/table pane has a consistent filter:

  • / enters input mode; type freeform, Backspace deletes, Enter applies, Esc cancels (drops back to whatever filter was already active)
  • Active filter is shown in cyan in the footer (filter: foo); editable filter buffer is yellow
  • Esc with no input mode and an active filter clears it

Filter is case-insensitive substring across multiple fields per pane (host + service + container name + state + version + networks for the dashboard; analogous sets elsewhere). Empty filter shows everything.

Help overlay (?)

Toggles a centred per-view modal listing every key binding active in the current view. Press ? again or Esc to close. The contents are scoped; Dashboard's overlay shows only Dashboard keys, Resources' shows only Resources keys, etc., so you don't have to scan irrelevant bindings.

The overlay is available in every view including inside the embedded shell (which otherwise forwards every key to the PTY).

CLI launch flags

yoink tui                       # Dashboard view by default
yoink tui --mode hosts          # start on a specific top-level pane
yoink tui --mouse               # enable mouse capture (scroll wheel + selection)

--mode accepts dashboard / hosts / services / logs / resources / secrets / audit. --mouse is opt-in because mouse capture disables your terminal's native text-selection; if you don't actively use mouse scroll inside the TUI, leave it off.

YOINK_NO_HL=1 in the environment skips the hl auto-detection (the logs pane will use raw output even if hl is on PATH). Useful when troubleshooting hl itself.

Pretty logs

Structured log lines (JSON, logfmt, etc.) are hard to scan as raw text. The TUI's logs pane auto-detects hl (brew install pamburus/tap/hl) on the operator's PATH and transparently pipes every container's log stream through it before rendering; JSON keys are colored, timestamps are dim, levels are highlighted, and stack traces stay readable. Falls back to raw output when hl isn't installed; no config knob to toggle.

The yoink logs <svc> -f CLI doesn't auto-pipe (the operator decides their own shell pipeline), but yoink logs api -f | hl works the same way.

Keybinding cheat sheet

A consistent set of letters has the same meaning everywhere they appear:

keymeaning
j k / โ†‘ โ†“navigate up / down
enterdrill in (logs from a list, modal-confirm from a dialog)
escback / dismiss / clear filter
/begin filter input
rrefresh the current pane
!shell into selection
Bdebug sidecar
iinspect (container detail)
Kkill (SIGKILL with confirmation)
S / X / Rstart ยท stop ยท restart container
Ureconcile current service
Areconcile all services
Pprune (containers from Dashboard; resources from Resources)
Hhistory (only on ServiceDetail)
pdocker top (only on ContainerDetail)
yyank visible buffer to clipboard (logs / progress modals)
?help overlay scoped to the current view
q / Ctrl-Cquit

Letters are case-sensitive; capital letters generally mean "destructive or expensive" (kill, restart, reconcile-all, prune-all-images, โ€ฆ) and require a y/Enter confirmation when state-changing, while lowercase letters are read-only navigation / refreshes.

See also

On this page