Skip to content

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

doable --version
# doable 0.3.0

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: standard tracing filter. Logs go to stderr at level warn by default. Set RUST_LOG=info,doable=debug for 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

doable install [OPTIONS]

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:

  1. Preflight checks (OS, sudo, network)
  2. System packages (apt, locales, tzdata)
  3. Node.js 22 + pnpm
  4. PostgreSQL 16 + extensions (pgcrypto, pgvector, pg_trgm)
  5. Caddy + cloudflared
  6. Puppeteer / Chrome dependencies
  7. Repo clone + workspace install
  8. Database creation + migrations
  9. Environment files + secret generation
  10. UFW firewall (deny all, allow SSH)
  11. fail2ban + sshd hardening
  12. Swap file (2 GB)
  13. Cloudflare Tunnel configuration
  14. systemd services (doable.service + cloudflared.service)
  15. tmux session (api / web / ws) + smoke test

Icons in the sidebar: pending, 🔄 running, done, failed.

Examples

Interactive on a fresh VPS:

doable install \
  --host 203.0.113.10 \
  --user root \
  --env-name myorg \
  --ssh-key ~/.ssh/id_ed25519

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:

doable install --host demo --user demo --env-name demo \
               --ssh-key /dev/null --demo

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 login and opens a browser link you paste back. Works on first install.
  • Pre-supplied: paste a tunnel UUID, a Cloudflare cert.pem, and the per-tunnel credentials.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_UUID
  • DOABLE_CF_CERT_PEM
  • DOABLE_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 green
  • 1: 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

doable admin [OPTIONS]

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):

  1. Reads DATABASE_URL from /opt/doable/.env, or falls back to the env var, or to postgres://doable:doable@localhost:5432/doable.
  2. Connects via tokio-postgres on the loopback interface.

Remote mode (doable admin --remote [email protected] --ssh-key ...):

  1. SSHes to the remote and runs cat /opt/doable/.env | grep DATABASE_URL (or sudo -n cat if needed) to fetch the real password.
  2. Spawns ssh -N -L 127.0.0.1:<random>:127.0.0.1:5432 user@host to forward Postgres.
  3. Rewrites the fetched URL's host/port to point at the local tunnel port.
  4. 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:

eval $(ssh-agent -s)
ssh-add ~/.ssh/id_ed25519

Then re-run doable admin --remote. The CLI detects the situation and prints exactly this hint before exiting.

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.
  • Enter toggles the is_platform_admin boolean.
  • r opens the platform-role picker. Press o/a/m/v to 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, current enabled state, min_plan, and min_role.
  • Enter toggles the global enable/disable.
  • Edits hit the feature_flags table; on first connect the admin TUI seeds 15 default flags if the table is empty.

Members & Roles

  • Lists every workspace_members row joined with users + workspaces.
  • r (or F2): change role
  • a (or F3): add a user to the focused workspace by email
  • d (or F4): remove a member

AI Settings

  • Per-workspace: enforce_ai, enforced_model, show_model_selector, default provider, default model.
  • w opens 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_balances row joined with users + workspaces.
  • d: edit daily credits
  • m: edit monthly credits
  • r: edit rollover credits
  • p: pick plan type (free / pro / enterprise)

API Keys

  • Lists every project_api_keys row with prefix, tier, allowed origins, allowed tools.
  • o: edit allowed origins (CORS allowlist)
  • t: edit allowed tools

Mode Tools

  • The mode_tools table: per AI mode, which tool names are allowed.
  • Enter / Space opens the tools editor.

Sandbox

  • Workspace-scoped sandbox rules from workspace_sandbox_rules.
  • a add, e edit, d delete, t toggle enabled, s open 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.
  • a add, e edit, d delete, t toggle. is_floor rules 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:

sudo install -m 0440 doable-cli/sudoers/90-doable-admin /etc/sudoers.d/
sudo visudo -c

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 (q or Ctrl-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 for DATABASE_URL, secrets, feature toggles like DOABLE_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.sh actually does.
  • Sandboxing: how the Sandbox and System Rules screens map to runtime enforcement.