Browser-based TUI host monitors — top, htop, btop, atop, and bottom — wrapped in ttyd for one-image, runtime-controlled privilege.
The default image cannot kill host processes. If you want it to, you opt in at runtime — it's never required to run the container.
| Tool | Tag | What you get |
|---|---|---|
top |
:top (also :latest) |
busybox top — minimal, always present, every arch |
htop |
:htop |
Interactive process viewer with a richer UI than top |
btop |
:btop |
Modern resource monitor with mouse support and pretty graphs |
atop |
:atop |
Advanced system & process monitor (interactive, 2s refresh) |
bottom |
:bottom |
Rust-based graphical process/system monitor (btm) |
Pin to an immutable version like :1.0.0-btop for production. Floating tags (:btop, :latest, etc.) follow the latest release.
docker run -d --name ttyd-tops \
--pid=host \
-p 127.0.0.1:7681:7681 \
--restart unless-stopped \
btleffler/ttyd-tops:btopOpen http://localhost:7681. Swap :btop for any tag in the table above.
services:
ttyd-tops:
image: btleffler/ttyd-tops:btop # change tag to switch tools
pid: host
ports:
- "127.0.0.1:7681:7681"
restart: unless-stoppedttydserves a web-based terminal on port 7681 (xterm.js in the browser).- The chosen tool runs in
while true; do <tool>; done, so quitting it (e.g.qin btop) just relaunches it. There's no shell to escape to. --pid=hostlets the container see host processes. Without it, you'd only see the container's own process tree.- The container runs as a non-root
monitoruser by default. Standard Unix permissions stop it from signaling host processes — none of them are owned bymonitor.
The default — no extra flags — already prevents the web terminal from killing host processes. Lock it down further with seccomp, or opt in to kill capability when you actually want it. Three tiers, pick one:
docker run -d --pid=host -p 127.0.0.1:7681:7681 btleffler/ttyd-tops:btopContainer runs as monitor (non-root). Cannot kill host processes — the kernel returns EPERM because monitor doesn't own them.
services:
ttyd-tops:
image: btleffler/ttyd-tops:btop
pid: host
ports:
- "127.0.0.1:7681:7681"
security_opt:
- seccomp:./nokill.json
restart: unless-stoppedAdds the nokill.json profile (in this repo) to block kill, tkill, and tgkill syscalls at the kernel level. Belt and suspenders on top of Tier 1. Recommended if the terminal is exposed beyond localhost.
Drop nokill.json next to your compose.yaml:
curl -O https://raw.githubusercontent.com/btleffler/ttyd-tops/main/nokill.jsondocker run -d --pid=host --user root \
-p 127.0.0.1:7681:7681 \
btleffler/ttyd-tops:btopOr with Compose:
services:
ttyd-tops:
image: btleffler/ttyd-tops:btop
pid: host
user: "0:0" # run as root inside the container
ports:
- "127.0.0.1:7681:7681"
restart: unless-stoppedRun as root inside the container. With --pid=host, container root is host root for signal-permission purposes — the web terminal can now kill host processes. Trusted private networks only. Add a reverse proxy with auth if it's exposed at all.
For the rare cases that need full device/capability access (e.g. raw block I/O, kernel module visibility), add privileged: true to the Compose service (or --privileged --user root on docker run). Most monitoring use cases don't.
services:
ttyd-tops:
image: btleffler/ttyd-tops:btop
pid: host
user: "0:0"
privileged: true # full device/capability access — only when actually needed
ports:
- "127.0.0.1:7681:7681"
restart: unless-stopped- Bind to localhost.
127.0.0.1:7681:7681is the default. Don't bind to0.0.0.0directly on machines with public IPs. - Reverse proxy with auth. Caddy, Nginx, or Traefik in front, with HTTP basic auth or OIDC + TLS. Required if exposed beyond a private LAN, mandatory if exposed to the internet.
- ttyd built-in basic auth. Pass
-c user:passto ttyd. Weaker than a real reverse proxy, but trivial. - Read-only filesystem.
--read-only --tmpfs /tmpblocks writes inside the container.
"Doesn't ttyd -W mean read-only?" No — it's the opposite. -W is --writable, which lets the browser send keystrokes to the TTY. Without -W, you couldn't navigate the TUI at all (no q to quit, no arrow keys, no filters). All five tools need -W to be usable. Read-only protection in this image comes from the OS user and seccomp, not from ttyd.
"Why --pid=host instead of --privileged?" --pid=host is the minimum needed for the tool to see host processes. --privileged grants full kernel/device access — far more than you want for monitoring.
"--user root vs --privileged?" Different things. --user root only changes the UID inside the container. --privileged lifts capability bounds, grants device access, and unfences cgroups. For Tier 3, --user root is enough; reach for --privileged only if you specifically need it.
:1.0.0-btop,:1.0-btop,:1-btop— version-pinned, immutable.:btop,:latest— floating, follow new releases. Use these on a homelab; pin in production or CI.
Built for linux/amd64, linux/arm64, linux/arm/v7, linux/arm/v6, linux/386, linux/ppc64le, linux/s390x. Suitable for Raspberry Pi 3B and other ARM/x86 Linux hosts.
This project replaces:
btleffler/ttyd-btop— replaced bybtleffler/ttyd-tops:btop(drop-in).btleffler/ttyd-btop-privileged— replaced bybtleffler/ttyd-tops:btopplus--user root(Tier 3).
The old images stay pullable but get no further updates.
MIT — see LICENSE.