doable-cli Reference¶
doable is a single Rust binary that combines a server installer and a
runtime admin TUI. Source at doable-cli/,
package version 0.3.0.
Two subcommands plus a no-subcommand picker:
doable # interactive picker (recommended first time)
doable install # form-driven server provisioning
doable admin # admin TUI for THIS server (local Postgres)
doable admin --remote ... # admin TUI for a REMOTE server (over SSH)
If you're new, just run doable
The bare command opens an arrow-key picker with three options: install, manage local, or manage remote. You don't have to memorise the subcommands until you start scripting.
Install¶
Pre-built binary¶
Download from GitHub Releases
and put it on your PATH:
curl -L https://github.com/doable-me/doable/releases/latest/download/doable-linux-x86_64 \
-o ~/.local/bin/doable
chmod +x ~/.local/bin/doable
Replace linux-x86_64 with the suffix matching your laptop (darwin-arm64,
darwin-x86_64, windows-x86_64.exe).
Build from source¶
You need Rust 1.80 or newer and a working ssh on
PATH. macOS and Linux ship with ssh; Windows users can install Git for
Windows (provides OpenSSH) or use WSL.
git clone https://github.com/doable-me/doable.git
cd doable/doable-cli
cargo build --release
# binary at target/release/doable (or doable.exe on Windows)
Verify install¶
Global flags¶
doable itself takes no flags; they belong to the subcommands. The two
that the top-level dispatcher inspects via clap's env support are:
RUST_LOG: standardtracingfilter. Logs go to stderr at levelwarnby default. SetRUST_LOG=info,doable=debugfor verbose output.
Terminal state is restored on panic via a term::install_panic_hook(), so
even an unexpected crash will not leave your terminal in raw mode.
doable install¶
Provisions a fresh Ubuntu 22.04 or 24.04 host. The installer uploads
deployment/server-setup.sh (or setup-server-v3.sh when configured), streams the
script over SSH, and parses Phase N/15 markers from the output to drive
the colour-coded sidebar.
Usage¶
With no arguments and a TTY attached, you get the interactive welcome form.
With --host + --env-name + --ssh-key plus --non-interactive, the
form is skipped and provisioning starts immediately.
Flags¶
| Flag | Env var | Default | Description |
|---|---|---|---|
--host <ADDR> |
DOABLE_HOST |
(none) | Target IPv4, IPv6, or DNS name |
--user <NAME> |
DOABLE_USER |
ubuntu |
SSH user (needs sudo or root) |
--env-name <STR> |
DOABLE_ENV_NAME |
(none) | Environment label (e.g. myorg, staging) |
--ssh-key <PATH> |
DOABLE_SSH_KEY |
(none) | Path to SSH private key |
--ssh-port <N> |
DOABLE_SSH_PORT |
22 |
Non-standard SSH port |
--non-interactive |
DOABLE_NON_INTERACTIVE |
false |
Skip prompts (CI mode) |
--with-tmux |
DOABLE_WITH_TMUX |
true |
Start the on-server tmux session after setup |
--skip-admin-user |
DOABLE_SKIP_ADMIN_USER |
false |
Skip creating the platform-admin user |
--demo |
(none) | false |
Replay a canned phase stream (no SSH) |
--setup-script <PATH> |
DOABLE_SETUP_SCRIPT |
setup-server-v3.sh |
Custom setup script to upload |
The 15 phases¶
The phase list mirrors deployment/server-setup.sh. The TUI shows their live state
in the left sidebar:
- Preflight checks (OS, sudo, network)
- System packages (apt, locales, tzdata)
- Node.js 22 + pnpm
- PostgreSQL 16 + extensions (pgcrypto, pgvector, pg_trgm)
- Caddy + cloudflared
- Puppeteer / Chrome dependencies
- Repo clone + workspace install
- Database creation + migrations
- Environment files + secret generation
- UFW firewall (deny all, allow SSH)
- fail2ban + sshd hardening
- Swap file (2 GB)
- Cloudflare Tunnel configuration
- systemd services (
doable.service+cloudflared.service) - tmux session (api / web / ws) + smoke test
Icons in the sidebar: ⏳ pending, 🔄 running, ✅ done, ❌ failed.
Examples¶
Interactive on a fresh VPS:
Unattended via env vars (for CI / IaC):
DOABLE_HOST=203.0.113.10 \
DOABLE_USER=root \
DOABLE_ENV_NAME=myorg \
DOABLE_SSH_KEY=$HOME/.ssh/id_ed25519 \
DOABLE_NON_INTERACTIVE=1 \
doable install
Preview the TUI without a real server:
Local-mode install¶
The interactive welcome form has a Target: Local toggle. When selected,
doable install runs sudo -E bash setup-server-v3.sh on the current
host instead of dialling SSH. Use this when you have already SSH'd into
the target and want the TUI without bouncing through another connection.
Pre-supplied tunnel credentials¶
In the form, the Tunnel section has two modes:
- Interactive: the setup script runs
cloudflared tunnel loginand opens a browser link you paste back. Works on first install. - Pre-supplied: paste a tunnel UUID, a Cloudflare
cert.pem, and the per-tunnelcredentials.json. This is what you use when reprovisioning with an existing tunnel, or when scripting an unattended install.
Translated env vars (consumed by setup-server-v3.sh):
DOABLE_CF_TUNNEL_UUIDDOABLE_CF_CERT_PEMDOABLE_CF_CREDENTIALS_JSON
The cert.pem is per-Cloudflare-account (not per-tunnel), so any prior account-level cert.pem works.
Key bindings during install¶
| Key | Action |
|---|---|
q / Esc / Ctrl-C |
Quit |
l |
Toggle log filter (errors only) |
r |
Flag the current phase for retry |
p |
Pause auto-scroll |
The retry key just leaves a hint in the log; to actually retry, fix the
underlying issue and re-run doable install. The setup script is
idempotent.
Exit codes¶
0: provisioning finished, all 15 phases green1: any phase failed, or SSH error, or panic (terminal is restored regardless)
Common errors¶
| Symptom | Cause | Fix |
|---|---|---|
Permission denied (publickey) in Phase 1 |
Key not authorized on target | Add ~/.ssh/id_ed25519.pub to target's ~/.ssh/authorized_keys |
Tunnel credentials file not found in Phase 13 on first run |
Known grep -oP UUID-duplication bug in setup script |
Re-run; the second pass takes the existing-tunnel branch and succeeds |
SSH password auth is not yet supported |
Form's auth set to Password | Re-run with a key, or use Local mode |
| Stalls mid-Phase 7 | Slow or rate-limited git clone | Wait it out, or pre-clone manually and re-run |
doable admin¶
Opens a full-screen runtime management TUI against an existing Doable server's Postgres. It can run locally on the server, or from your laptop over an SSH tunnel.
Usage¶
Flags¶
| Flag | Env var | Default | Description |
|---|---|---|---|
--remote <USER@HOST[:PORT]> |
(none) | (none) | Manage a remote server over SSH |
--ssh-key <PATH> |
(none) | (none) | Private key (required with --remote) |
--database-url <URL> |
DATABASE_URL |
(none) | Override; skip .env lookup |
--print-db-url |
(none) | false |
Print full URL to stdout and exit (no TUI) |
--print-db-pass |
(none) | false |
Print just the password and exit (no TUI) |
--print-db-url and --print-db-pass are mutually exclusive.
Local vs remote¶
Local mode (doable admin on the server itself):
- Reads
DATABASE_URLfrom/opt/doable/.env, or falls back to the env var, or topostgres://doable:doable@localhost:5432/doable. - Connects via tokio-postgres on the loopback interface.
Remote mode (doable admin --remote [email protected] --ssh-key ...):
- SSHes to the remote and runs
cat /opt/doable/.env | grep DATABASE_URL(orsudo -n catif needed) to fetch the real password. - Spawns
ssh -N -L 127.0.0.1:<random>:127.0.0.1:5432 user@hostto forward Postgres. - Rewrites the fetched URL's host/port to point at the local tunnel port.
- Connects via tokio-postgres as if Postgres were local.
The tunnel child is killed automatically when doable exits (including on
panic) thanks to the Drop impl on Tunnel.
Encrypted SSH keys
If your private key has a passphrase, doable admin --remote requires
that the key be loaded into ssh-agent first (the binary cannot prompt
for passphrases interactively). Run:
Then re-run doable admin --remote. The CLI detects the situation
and prints exactly this hint before exiting.
Print-mode (no TUI)¶
Useful for piping creds into psql, DBeaver, or a backup script:
# Full URL
doable admin --remote [email protected] --ssh-key ~/.ssh/key --print-db-url
# Just the password (e.g. for PGPASSWORD)
PGPASSWORD=$(doable admin --remote [email protected] --ssh-key ~/.ssh/key --print-db-pass) \
psql -h myorg.doable.me -U doable doable
Both modes tear down the alt-screen before printing so stdout lands cleanly.
Examples¶
# Locally on a server
doable admin
# From your laptop
doable admin --remote [email protected] --ssh-key ~/.ssh/id_ed25519
# Bypass .env lookup; connect with an explicit URL
doable admin --database-url postgres://doable:[email protected]:5432/doable
Admin TUI screens¶
The left sidebar has 10 screens. Tab/BackTab cycles, arrow keys move
within a screen, Enter activates the selected row.
Users & Admins¶
- Lists every user, ordered by platform-admin first, then by
platform_role(owner / admin / member / viewer), then email. Entertoggles theis_platform_adminboolean.ropens the platform-role picker. Presso/a/m/vto set owner / admin / member / viewer directly.- Read-only columns: id, display name, created date.
Feature Flags¶
- Lists every feature flag (e.g.
ai_chat,code_editor,custom_domains) with its label, currentenabledstate,min_plan, andmin_role. Entertoggles the global enable/disable.- Edits hit the
feature_flagstable; on first connect the admin TUI seeds 15 default flags if the table is empty.
Members & Roles¶
- Lists every
workspace_membersrow joined with users + workspaces. r(orF2): change rolea(orF3): add a user to the focused workspace by emaild(orF4): remove a member
AI Settings¶
- Per-workspace:
enforce_ai,enforced_model,show_model_selector, default provider, default model. wopens the workspace selector so you can switch between workspaces.- Provider rows come from
ai_providers(workspace-scoped,is_valid = true).
Credits & Plan¶
- Lists every
credit_balancesrow joined with users + workspaces. d: edit daily creditsm: edit monthly creditsr: edit rollover creditsp: pick plan type (free/pro/enterprise)
API Keys¶
- Lists every
project_api_keysrow with prefix, tier, allowed origins, allowed tools. o: edit allowed origins (CORS allowlist)t: edit allowed tools
Mode Tools¶
- The
mode_toolstable: per AI mode, which tool names are allowed. Enter/Spaceopens the tools editor.
Sandbox¶
- Workspace-scoped sandbox rules from
workspace_sandbox_rules. aadd,eedit,ddelete,ttoggle enabled,sopen settings (backend, default tool/network actions).- The table-not-found case (no sandbox migration applied) renders an empty screen rather than erroring.
System Rules¶
- System-wide sandbox rules from
sandbox_system_rules(migration 080), ordered by scope/type/priority. aadd,eedit,ddelete,ttoggle.is_floorrules are global floor (cannot be loosened per workspace).
Server Config¶
Linux-only screen that lets you read and (with a sudoers fragment) edit
the on-disk service config. Number keys switch the sub-tab:
| Key | Sub-view | File / source |
|---|---|---|
1 |
Squid Allowlist | /etc/squid/squid.conf |
2 |
Cloudflared Ingress | /etc/cloudflared/config.yml |
3 |
API .env | /opt/doable/.env |
4 |
systemd Hardening | /etc/systemd/system/doable*.service (read-only) |
5 |
nft Egress Jail | nft list table inet doable_egress (read-only) |
6 |
Caddy Routing | /etc/caddy/Caddyfile or /opt/doable/Caddyfile (read-only) |
7 |
DB Credentials | DATABASE_URL parsed from /opt/doable/.env |
Inside each editable sub-view:
| Key | Action |
|---|---|
Enter |
Edit the focused row |
a |
Add a new entry |
d |
Delete focused entry |
A |
Apply pending changes (validates, then writes) |
R |
Reload from disk (drop unsaved edits) |
The DB Credentials sub-view (7) supports s/r/c for show/rotate/copy.
Rotation generates a fresh 64-hex-char password (via openssl rand -hex 32
or a nanos-seeded fallback), updates Postgres, rewrites /opt/doable/.env,
and restarts doable.service.
sudoers fragment required for writes
The TUI itself never runs as root. It writes to /tmp/<name>.new as
the invoking user, then shells out to sudo -n mv and sudo -n
systemctl reload <unit>. Install the bundled NOPASSWD fragment once
per server:
Without it, edits stage but A (apply) fails with "sudo: a password
is required". Reads work without the fragment.
Key bindings (global, admin TUI)¶
| Key | Action |
|---|---|
q |
Quit |
Tab / BackTab |
Cycle focus between sidebar and content |
↑ ↓ / k j |
Move within list / sidebar |
Enter / Space |
Activate the focused row |
Home / End |
First / last row |
PageUp / PageDown |
Jump 10 rows |
← |
Return focus to sidebar |
Esc |
Close dialog / back out |
Ctrl-C |
Quit (panic-safe terminal restore) |
The admin TUI also enables mouse capture, so you can click sidebar items and table rows directly.
Exit codes¶
0: clean exit (qorCtrl-C)1: DB connect failed, SSH tunnel failed, or panic. Errors include the full source chain (e.g.connect db at postgres://doable:***@... : io error: Connection refused).
Common errors¶
| Symptom | Cause | Fix |
|---|---|---|
connect db ... wrong password |
.env rotated outside the TUI |
Re-rotate via DB Credentials sub-view, or pass --database-url |
| Server Config sub-tab shows "not found" | Host is not a Doable server, or file moved | Verify with ls /etc/cloudflared/config.yml; the loader checks paths and shows a friendly placeholder |
sudo: a password is required after pressing A |
Sudoers fragment not installed | See above |
ssh exited ... before tunnel was ready |
Key wrong, host key not in known_hosts, network unreachable, or encrypted key not in agent |
Error message lists all four possibilities; check stderr in the message body |
Could not read /opt/doable/.env over remote |
SSH user lacks read access | Either chmod the file, give the user passwordless sudo, or use --database-url |
Configuration files¶
doable itself reads no config file. All inputs come from CLI flags or
environment variables.
The runtime on the server depends on:
/opt/doable/.env: source of truth forDATABASE_URL, secrets, feature toggles likeDOABLE_HARDENING,DOABLE_DEV_UID_DISABLED, proxy vars (BUILD_HTTP_PROXY,HTTP_PROXY,HTTPS_PROXY),PUBLISH_SUBDOMAIN_PREFIX,CLOUDFLARED_TUNNEL_ID,CORS_ORIGINS./etc/cloudflared/config.yml: tunnel ingress map/etc/squid/squid.conf: egress allowlist for sandboxed UIDs/etc/caddy/Caddyfile: reverse-proxy routing/etc/systemd/system/doable*.service: service definitions
The admin TUI's Server Config screen reads and edits these via parsers
that preserve unrelated lines and comments. A typo in the YAML/conf is
caught by cloudflared tunnel ingress validate and squid -k reconfigure
before the change goes live; failed validation rolls back automatically.
Output formats¶
The TUI is the primary interface. The two non-TUI outputs are:
--print-db-url: stdout, one line:postgres://user:pass@host:port/db--print-db-pass: stdout, one line: the password only
Both write nothing to stderr on success. Logs (when RUST_LOG is set)
go to stderr.
There is no JSON or table output mode today. If you need structured data
from the underlying Postgres, hit Postgres directly with the URL from
--print-db-url.
Comparison: TUI vs /admin web panel¶
doable admin TUI |
/admin web panel |
|
|---|---|---|
| Audience | Operators (SSH + DB access) | Platform admins (browser + is_platform_admin) |
| Transport | Local socket or SSH tunnel | HTTPS through Cloudflare Tunnel |
| Edits server files | Yes | No |
| Manages users / roles / flags | Yes | Yes |
| Works when app is down | Yes | No |
| Requires sudoers fragment | For writes only | Never |
Use the TUI for low-level work (rotate DB password, edit .env, add a
tunnel ingress route, work around a broken web app). Use /admin for
day-to-day user and workspace management when the app is healthy.
Where to go next¶
- Operations Runbook: concrete recipes for backups, migrations, scaling, and incidents.
- Bare-metal deployment: the underlying
network diagram and what
deployment/server-setup.shactually does. - Sandboxing: how the Sandbox and System Rules screens map to runtime enforcement.