- Dockerfile 60.6%
- Shell 39.4%
| skel | ||
| .env.example | ||
| .gitignore | ||
| debian-ttyd.container | ||
| debian-ttyd.service | ||
| Dockerfile | ||
| entrypoint.sh | ||
| Readme.md | ||
| sshd-wetty-localhost.conf | ||
| wetty.container | ||
| wetty.service | ||
Docker-Debian: Web Terminals
Two browser terminals for a small host, both rootless Podman managed by systemd user units:
- Sandbox (
debian-ttyd, port 7681): a throwaway Debian shell powered by ttyd. Every session runs in a fresh container — when the last client disconnects, the container is destroyed and a new one starts automatically. - Admin terminal (
wetty, port 3000): a persistent WeTTY that logs into the real host over SSH.
Sandbox: fresh Debian per session
ttydruns with--exit-no-conn: it exits as soon as the last browser client disconnects.- The container runs with
--rm, so it is removed when ttyd exits. - systemd (
Restart=always) immediately starts a brand-new container.
Only data in the data volume (mounted at DATA_DIR, default /data)
survives between sessions.
A message on every login reminds users of that path.
Quick start (local test)
cp .env.example .env # then edit at least PASSWORD
podman build -t debian-ttyd .
podman run --rm -p 7681:7681 --env-file=.env -v data:/data debian-ttyd
Open http://localhost:7681 and log in with USERNAME / PASSWORD.
Without systemd the container is not recreated automatically — that part
is handled by the unit files below.
Configuration (.env)
| Variable | Default | Description |
|---|---|---|
USERNAME |
user |
Login user created on container start |
PASSWORD |
Passw0rd |
Password for that user — change it! |
SUDO_OK |
false |
true grants passwordless sudo |
AUTOLOGIN |
false |
true skips the login prompt (login -f) |
DATA_DIR |
/data |
Persistent mount point (must match the unit's volume) |
TZ |
Europe/Berlin |
Container timezone |
Deployment with systemd (rootless)
Clone the repo to ~/Docker-Debian on the host (or adjust the paths in the
unit files); for the sandbox also create .env and build the image as shown
above. Each service ships in two variants — install one:
Quadlet (Podman >= 4.4, e.g. Debian 13) — *.container files. Quadlet
generates and enables the service on daemon-reload:
mkdir -p ~/.config/containers/systemd
cp debian-ttyd.container ~/.config/containers/systemd/ # and/or wetty.container
systemctl --user daemon-reload
systemctl --user start debian-ttyd.service
Classic unit (Podman < 4.4, e.g. Debian 12) — *.service files:
mkdir -p ~/.config/systemd/user
cp debian-ttyd.service ~/.config/systemd/user/ # and/or wetty.service
systemctl --user daemon-reload
systemctl --user enable --now debian-ttyd.service
In both cases, keep the services running after logout:
loginctl enable-linger "$USER"
Useful commands
systemctl --user status debian-ttyd.service
journalctl _SYSTEMD_USER_UNIT=debian-ttyd.service -e # works without persistent journal
podman volume rm data # wipe the sandbox's persistent data
Troubleshooting
journalctl --user -u ...prints "No entries": the host has no persistent journal (common on Raspberry Pi OS). Use the_SYSTEMD_USER_UNIT=form above, or enable it once:sudo mkdir -p /var/log/journal, then restartsystemd-journald(considerSystemMaxUse=50Minjournald.confon SD-card systems).- Sandbox dead after a few quick sessions (
systemctl --user statusshowsstart-limit-hit): the unit is missingStartLimitIntervalSec=0. Update it, then rundaemon-reload,reset-failed debian-ttyd.service,start debian-ttyd.service. - Quadlet unit not found after
daemon-reload: Quadlet needs Podman >= 4.4 (podman --version); on older hosts use the classic unit. Debug generation with/usr/lib/systemd/user-generators/podman-user-generator --user --dryrun.
WeTTY: admin access to the real host
Unlike the sandbox, WeTTY gives you a persistent terminal on the host
itself, logging in as a real host account. The official wettyoss/wetty
image connects out to the host's own sshd over a real SSH connection
(--force-ssh is required: the image runs as root, and without it WeTTY
would spawn a login shell inside the container instead of using SSH).
The container runs with --network host, so its ssh connection always
originates from 127.0.0.1, regardless of Podman version or network
backend. That lets sshd allow password auth only for this source address,
while every real network connection stays key-based.
Setup, in order:
-
Configure sshd — requires
PasswordAuthentication noin the mainsshd_config(Debian's default); the drop-in only adds the loopback exception:sudo cp sshd-wetty-localhost.conf /etc/ssh/sshd_config.d/ sudo sshd -t # validate config sudo systemctl reload ssh -
Set the login account: replace
--ssh-user=CHANGE_MEin wetty.container / wetty.service with the host account to log into. WeTTY prompts for that account's Unix password in the browser on every connection — nothing is stored. -
Install the unit as described under Deployment with systemd.
Open http://<host>:3000 and log in with the account's Unix credentials.
Why direct ssh stays key-based: the drop-in allows password auth only
for source address 127.0.0.1/::1. A real ssh user@host from anywhere
else still hits PasswordAuthentication no and needs a key. Verify with
sudo sshd -T | grep -i passwordauthentication (must print no).
Security notes
- ttyd/WeTTY serve plain HTTP/WS — credentials cross the wire unencrypted. Put a TLS reverse proxy (Caddy, nginx, Traefik) in front of port 7681 (sandbox) / 3000 (WeTTY) and make sure it forwards WebSocket upgrades.
SUDO_OK=truegives the user root inside the sandbox container. Fine for a throwaway sandbox, but treat the container as untrusted in that case.- The WeTTY container gets full host network access (
--network host) to make the loopback-source trick work. It cannot reach anything it couldn't already reach as a process on the host, but it is not network-isolated the way the sandbox container is.
Credits
Sandbox based on https://github.com/colinmurphy1/docker-ttyd