Self-hosted SSH gateway that drops users into isolated Docker-based dev containers. Connect via SSH, pick your tools, and land in a persistent environment with zellij, your editor, and your runtimes.
- SSH-native —
ssh hop@serveris all you need. No VPN, no browser. - Per-user isolation — each user gets their own Docker container with a persistent home directory.
- Tool selection wizard — choose your multiplexer, editor, shell, runtimes, and CLI tools on first connect.
- Multiple devboxes —
ssh hop+project@serverfor separate environments.ssh hop+?@serverto pick. - Shared boxes across keys —
hopbox linklets a second device join an existing box. - TOFU registration — new SSH keys are auto-registered with an interactive username prompt.
- Idle timeout — containers auto-stop after configurable hours of inactivity.
- Resource limits — CPU, memory, and PID limits per container.
- Admin web UI — dashboard for users, boxes, and settings with HTTP basic auth.
- Observability — structured logs,
/healthz, Prometheus metrics, ready-made Grafana dashboards.
- Linux server (Ubuntu 22.04+ recommended)
- Docker Engine 24+
- Go 1.24+ (for building from source)
On a brand-new Ubuntu/Debian VPS with nothing installed:
curl -fsSL https://raw.githubusercontent.com/hopboxdev/hopbox/main/scripts/provision-vps.sh | sudo bashThis installs Docker, configures UFW (allows 22/tcp and 2222/tcp), then runs the hopbox installer. It intentionally does not touch /etc/ssh/sshd_config or create admin users to avoid lockout — harden OpenSSH yourself afterwards. Supports the same flags as the install script (v0.1.0, --with-monitoring).
curl -sSL https://raw.githubusercontent.com/hopboxdev/hopbox/main/scripts/install.sh | sudo bashThis downloads the latest release, installs hopboxd to /usr/local/bin, drops a config at /etc/hopbox/config.toml, and starts the systemd service on port 2222. Re-run the same command to upgrade.
Optional flags:
# Pin to a specific version
curl -sSL https://raw.githubusercontent.com/hopboxdev/hopbox/main/scripts/install.sh | sudo bash -s -- v0.1.0
# Also start Prometheus + Grafana with pre-provisioned dashboards
curl -sSL https://raw.githubusercontent.com/hopboxdev/hopbox/main/scripts/install.sh | sudo bash -s -- --with-monitoringThen connect:
ssh -p 2222 hop@your-server # default port is 2222; see FAQ to use port 22git clone https://github.com/hopboxdev/hopbox.git
cd hopbox
make run # builds hopboxd + in-container CLI and runs itListens on :2222 by default. First run builds the base Docker image (~2 min).
Copy the example config and modify as needed:
cp config.example.toml config.tomlKey options:
| Option | Default | Description |
|---|---|---|
port |
2222 |
SSH server port |
data_dir |
./data |
User data, profiles, home directories |
host_key_path |
(auto-generate) | Path to SSH host key |
open_registration |
true |
Allow new users to self-register |
idle_timeout_hours |
24 |
Hours before idle containers stop (0 = disabled) |
resources.cpu_cores |
2 |
CPU cores per container |
resources.memory_gb |
4 |
RAM per container (GB) |
resources.pids_limit |
512 |
Max processes per container |
See config.example.toml for the full annotated config.
The hop CLI wraps SSH/SCP so you don't have to remember flags and ports.
# Homebrew (macOS / Linux)
brew tap hopboxdev/tap
brew install hop
# Or with Go
go install github.com/hopboxdev/hopbox/cmd/hop@latest
# Or download from GitHub releases
curl -L https://github.com/hopboxdev/hopbox/releases/latest/download/hop-darwin-arm64 -o hop
chmod +x hop && sudo mv hop /usr/local/bin/hop init
# Server hostname: [hopbox.dev]:
# SSH port [22]:
# Default box: [default]:hop # SSH into your default box
hop -b work # SSH into a specific box
hop expose 3000 # forward box:3000 to localhost:3000
hop transfer file.txt # upload to ~/
hop transfer file.txt :~/dir/ # upload to specific path
hop transfer :~/file.txt . # download to current dir
hop config # show resolved configurationIf hopboxd is on port 22 (see FAQ):
ssh hop@server # default box
ssh hop+myproject@server # named box
ssh hop+?@server # box pickerIf hopboxd is on the default port 2222:
ssh -p 2222 hop@server
ssh -p 2222 hop+myproject@serverAdd this to ~/.ssh/config to avoid typing the port and hostname every time:
Host hop
HostName hopbox.dev
User hop
Port 22
Host hop-*
HostName hopbox.dev
Port 22
Then connect with:
ssh hop # default box
ssh -o User=hop+work hop # named boxNew SSH keys trigger registration:
- Choose a username
- Select your tools (multiplexer, editor, shell, runtimes, CLI tools)
- Wait for your environment to build
- Land in your dev container at a login shell
Subsequent connections skip the wizard and go straight to your container shell.
The gateway installs your chosen multiplexer but won't start it for you — drop you in a login shell and leave the rest to your rc file. Opt in with a snippet in your own ~/.zshrc / ~/.bashrc / ~/.config/fish/config.fish:
# zsh / bash
if [[ -z "$ZELLIJ" && $- == *i* ]]; then
exec zellij attach --create main
fi# fish
if status is-interactive; and not set -q ZELLIJ
exec zellij attach --create main
endReplace zellij attach --create main with tmux new-session -As main for tmux. Because it's in your rc (not the gateway), detaching drops you back to the shell instead of disconnecting SSH — handy for paste-hostile TUIs or anything that doesn't play well inside the multiplexer. Run zellij attach (or tmux attach) from the shell to jump back in.
Forward ports from your container to your local machine:
ssh -L 3000:localhost:3000 hop@server # add -p 2222 if not on port 22Each user's tunnels are isolated — no port collisions.
Inside your container, the hop command is available:
hop status # show box info (user, box, container, resources)
hop expose 3000 # print the SSH tunnel command for a local port
hop link # generate a one-time code to add another SSH key to this box
hop destroy # destroy this box (with confirmation)From inside a box on your first device:
hop link
# → code: ABCD-1234 (valid 5 min)From your second device:
ssh hop@server # add -p 2222 if not on port 22
# the wizard offers "Link to existing box" — paste the codeThe second key is linked to the same user and boxes via a filesystem symlink, so both devices share the same container and home directory.
Set [admin].enabled = true in the config (disabled by default) and hopboxd serves a small htmx/Tailwind admin UI on port 8080:
- Dashboard — user, box, and container counts
- Users — registered users and their keys
- Boxes — running boxes with live resource usage
- Settings — current config (read-only)
Protected by HTTP basic auth (admin.username / admin.password in config).
Security note: the admin server binds
:8080on all interfaces./healthzand/metricson the same port are unauthenticated. Block 8080 at the firewall (the provisioning script already does) and front it with a reverse proxy that handles TLS — seedeploy/caddy/Caddyfile.examplefor a ready-to-use setup.
Unauthenticated endpoints on the same listener:
GET /healthz— liveness probe; pings DockerGET /metrics— Prometheus metrics
A ready-to-run Prometheus + Grafana stack lives under deploy/monitoring/. It's bundled into release tarballs, and the install script sets it up for you with --with-monitoring.
Both services bind to 127.0.0.1 only — reach them over an SSH tunnel or front them with a reverse proxy.
For local development:
make monitoring-up # starts Prometheus on :9090 and Grafana on :3000
# Grafana default login: admin / admin
make monitoring-downGrafana comes pre-provisioned with two dashboards:
- Hopbox — Server Overview — users, boxes, build durations, running containers with drill-down
- Hopbox — Box Details — per-box CPU, memory, network, and disk IO
Release tarballs ship a ready-to-use Caddy config at /opt/hopbox/current/deploy/caddy/Caddyfile.example and a static landing page at /opt/hopbox/current/web/landing/. To wire up hopbox.dev, admin.hopbox.dev, and grafana.hopbox.dev:
# Install Caddy (see https://caddyserver.com/docs/install)
sudo mkdir -p /var/www/hopbox
sudo cp -R /opt/hopbox/current/web/landing/. /var/www/hopbox/
sudo cp /opt/hopbox/current/deploy/caddy/Caddyfile.example /etc/caddy/Caddyfile
# Edit /etc/caddy/Caddyfile: replace hopbox.dev with your domain and paste a bcrypt hash
# from: caddy hash-password --plaintext 'your-grafana-password'
sudo systemctl reload caddyOpen 80/tcp and 443/tcp in UFW (sudo ufw allow 80/tcp && sudo ufw allow 443/tcp). Caddy handles Let's Encrypt automatically.
Most users should use the install script. The steps below document what it does in case you want to install manually.
- Create a hopbox user:
sudo useradd -r -s /usr/sbin/nologin -d /opt/hopbox hopbox
sudo usermod -aG docker hopbox- Install the binary and templates:
sudo mkdir -p /opt/hopbox /etc/hopbox /var/lib/hopbox
sudo cp hopboxd /usr/local/bin/
sudo cp -r templates /opt/hopbox/
sudo cp config.example.toml /etc/hopbox/config.toml
sudo chown -R hopbox:hopbox /opt/hopbox /var/lib/hopbox- Edit the config:
sudo vim /etc/hopbox/config.toml
# Set data_dir = "/var/lib/hopbox"
# Adjust resources and timeout as needed- Install and start the service:
sudo cp deploy/hopboxd.service /etc/systemd/system/
sudo systemctl daemon-reload
sudo systemctl enable --now hopboxd- Check status:
sudo systemctl status hopboxd
sudo journalctl -u hopboxd -fOpen the SSH port (default 2222):
sudo ufw allow 2222/tcpConsider also listening on port 443 for corporate firewall bypass.
By default, hopboxd auto-generates an ED25519 host key on first run. For production, pre-generate one:
ssh-keygen -t ed25519 -f /etc/hopbox/host_key -N ""Then set host_key_path = "/etc/hopbox/host_key" in your config.
By default hopboxd listens on port 2222 so it doesn't conflict with your server's OpenSSH. If you want users to connect with ssh hop@server (no -p flag), you can move hopboxd to port 22.
What's involved:
- Move OpenSSH to a different port (e.g., 2222) so port 22 is free
- Grant hopboxd permission to bind a privileged port (below 1024)
- Update firewall rules
Step by step:
# 1. Move OpenSSH to port 2222
sudo sed -i 's/^#Port 22$/Port 2222/' /etc/ssh/sshd_config
# Or if Port 22 is already uncommented:
sudo sed -i 's/^Port 22$/Port 2222/' /etc/ssh/sshd_config
# 2. Allow the new port in UFW
sudo ufw allow 2222/tcp comment 'admin SSH'
# 3. Restart OpenSSH
sudo systemctl restart sshd
# 4. TEST admin SSH on the new port (from a NEW terminal, keep current session open!)
ssh -p 2222 user@server
# ⚠️ Only proceed if this works. If it doesn't, fix it using your existing session.
# 5. Stop hopboxd, change its port to 22
sudo systemctl stop hopboxd
sudo sed -i 's/^port = 2222$/port = 22/' /etc/hopbox/config.toml
# 6. Grant hopboxd the capability to bind privileged ports
sudo systemctl edit hopboxd
# Add these two lines in the editor:
# [Service]
# AmbientCapabilities=CAP_NET_BIND_SERVICE
# 7. Reload and start
sudo systemctl daemon-reload
sudo systemctl start hopboxd
# 8. Verify
sudo ss -tlnp | grep ':22 '
# Should show hopboxd listening on :22Update your SSH config for admin access:
# ~/.ssh/config
Host myserver-admin
HostName your-server-ip
User debian
Port 2222
Update hop client config:
hop init
# SSH port [2222]: 22Hopbox is a single Go binary (hopboxd) that runs an SSH server using charmbracelet/ssh. Users authenticate by SSH public key. Each user gets Docker containers created from a shared base image (Ubuntu 24.04 + mise) with per-user tool layers built from their profile.
SSH Client → hopboxd (auth, wizard, container lifecycle) → Docker containers
├── hopbox-gandalf-default
├── hopbox-gandalf-project1
└── hopbox-aragorn-default
Data is stored on the filesystem under data_dir:
users/<fingerprint>/user.toml— username and key infousers/<fingerprint>/profile.toml— default tool profileusers/<fingerprint>/boxes/<boxname>/profile.toml— per-box profile overrideusers/<fingerprint>/boxes/<boxname>/home/— bind-mounted as/home/dev
Elastic License 2.0 (ELv2) — free to use, modify, and self-host. You may not offer Hopbox as a hosted or managed service.