DocHub
Browser-based tmux access via ttyd and nginx, authenticated by Cloudflare Access

Web Terminals

Browser-based access to each LXC container’s tmux session, primarily for phone or remote machine access when SSH is not available.

Access

URL Container Restriction
https://laptop.ipnoelp.com Laptop Claude Code session chas@omelasai.com only
https://term.ipnoelp.io/ Landing page All @omelasai.com
https://term.ipnoelp.io/chasclaude/ chasclaude chas@omelasai.com only
https://term.ipnoelp.io/infoclaude/ infoclaude chas@omelasai.com only
https://term.ipnoelp.io/seanclaude/ seanclaude All @omelasai.com
https://term.ipnoelp.io/jazclaude/ jazclaude All @omelasai.com
https://term.ipnoelp.io/managerclaude/ managerclaude All @omelasai.com

Architecture

Browser
  |
  +-- Cloudflare Access (Google OAuth, @omelasai.com)
       |
       +-- nginx (:443, term.ipnoelp.io)
            |
            +-- Per-user restriction (Cf-Access-Authenticated-User-Email header)
            |
            +-- /chasclaude/  --> ttyd (127.0.0.1:7681) --> lxc-attach chasclaude
            +-- /infoclaude/  --> ttyd (127.0.0.1:7682) --> lxc-attach infoclaude
            +-- /seanclaude/  --> ttyd (127.0.0.1:7683) --> lxc-attach seanclaude
            +-- /jazclaude/   --> ttyd (127.0.0.1:7684) --> lxc-attach jazclaude
            +-- /managerclaude/ -> ttyd (127.0.0.1:7685) --> lxc-attach managerclaude

Components

ttyd

ttyd provides a web-based terminal. One ttyd instance per container, each running as a systemd service.

Each ttyd instance runs:

ttyd --port PORT --base-path /CONTAINER/ --writable \
  lxc-attach -n CONTAINER -- su -l dev

This attaches directly to the container as the dev user with a full login shell, which triggers the .login-gateway.sh menu.

Systemd Services

Service Port Container
ttyd-chasclaude.service 7681 chasclaude
ttyd-infoclaude.service 7682 infoclaude
ttyd-seanclaude.service 7683 seanclaude
ttyd-jazclaude.service 7684 jazclaude
ttyd-managerclaude.service 7685 managerclaude

All services bind to 127.0.0.1 only (not externally accessible). nginx handles external traffic.

nginx Configuration

File: /etc/nginx/sites-available/web-terminals

Key features:

  • SSL with self-signed cert (Cloudflare terminates public SSL)
  • WebSocket support (Upgrade and Connection headers proxied)
  • Landing page at / listing all containers
  • Per-container location blocks with proxy pass to ttyd
  • chasclaude and infoclaude restricted to chas@omelasai.com via $http_cf_access_authenticated_user_email header

Cloudflare Access

A Cloudflare Access application protects term.ipnoelp.io:

  • Type: Self-hosted
  • Auth: Google OAuth
  • Policy: Allow @omelasai.com email domain
  • Session: 30-day cache

The Cf-Access-Authenticated-User-Email header is set by Cloudflare and checked by nginx for per-container access control.

Authentication Flow

  1. User opens https://term.ipnoelp.io/chasclaude/
  2. Cloudflare Access intercepts (if no valid session)
  3. User authenticates via Google OAuth (@omelasai.com)
  4. Cloudflare sets session cookie + Cf-Access-Authenticated-User-Email header
  5. nginx checks email header for restricted containers
  6. If allowed, nginx proxies to the appropriate ttyd instance
  7. ttyd serves a web terminal attached to the container

Managing Web Terminals

# Check service status
ssh ovh5 "systemctl status ttyd-chasclaude ttyd-infoclaude ttyd-seanclaude ttyd-jazclaude ttyd-managerclaude"

# Restart a specific terminal
ssh ovh5 "sudo systemctl restart ttyd-chasclaude"

# View logs
ssh ovh5 "journalctl -u ttyd-chasclaude -n 20"

# Check nginx config
ssh ovh5 "sudo nginx -t"
ssh ovh5 "sudo systemctl reload nginx"

Files on Server

File Purpose
/etc/systemd/system/ttyd-*.service Per-container ttyd services
/etc/nginx/sites-available/web-terminals nginx reverse proxy config
/etc/ssl/dashboard/cert.pem Self-signed SSL cert (shared with dashboard)
/etc/ssl/dashboard/key.pem SSL private key