Skip to content

btleffler/ttyd-tops

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

4 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

ttyd-tops

Browser-based TUI host monitors — top, htop, btop, atop, and bottom — wrapped in ttyd for one-image, runtime-controlled privilege.

Source on GitHub · Docker Hub

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.

Pick your tool

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.

Quick start

docker run -d --name ttyd-tops \
  --pid=host \
  -p 127.0.0.1:7681:7681 \
  --restart unless-stopped \
  btleffler/ttyd-tops:btop

Open http://localhost:7681. Swap :btop for any tag in the table above.

Docker Compose

services:
  ttyd-tops:
    image: btleffler/ttyd-tops:btop  # change tag to switch tools
    pid: host
    ports:
      - "127.0.0.1:7681:7681"
    restart: unless-stopped

How it works

  • ttyd serves 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. q in btop) just relaunches it. There's no shell to escape to.
  • --pid=host lets the container see host processes. Without it, you'd only see the container's own process tree.
  • The container runs as a non-root monitor user by default. Standard Unix permissions stop it from signaling host processes — none of them are owned by monitor.

Security

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:

Tier 1 — Default (recommended for trusted home networks)

docker run -d --pid=host -p 127.0.0.1:7681:7681 btleffler/ttyd-tops:btop

Container runs as monitor (non-root). Cannot kill host processes — the kernel returns EPERM because monitor doesn't own them.

Tier 2 — Default + seccomp (defense in depth)

services:
  ttyd-tops:
    image: btleffler/ttyd-tops:btop
    pid: host
    ports:
      - "127.0.0.1:7681:7681"
    security_opt:
      - seccomp:./nokill.json
    restart: unless-stopped

Adds 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.json

Tier 3 — Opt in to kill capability

docker run -d --pid=host --user root \
  -p 127.0.0.1:7681:7681 \
  btleffler/ttyd-tops:btop

Or 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-stopped

Run 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

Hardening recipes (orthogonal to all tiers)

  • Bind to localhost. 127.0.0.1:7681:7681 is the default. Don't bind to 0.0.0.0 directly 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:pass to ttyd. Weaker than a real reverse proxy, but trivial.
  • Read-only filesystem. --read-only --tmpfs /tmp blocks writes inside the container.

Common confusion

"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.

Tag immutability

  • :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.

Multi-arch

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.

Project history

This project replaces:

The old images stay pullable but get no further updates.

License

MIT — see LICENSE.