Headless CLI
Everything the desktop app does, from a terminal. Same instances, same Docker, same SQLite - no GUI required.
The portside-cli binary ships with every Portside release (Windows, macOS Intel + ARM, Linux) next to the desktop installers, or builds from source with cargo build --manifest-path src-tauri/Cargo.toml --bin portside-cli. It talks to the same %LOCALAPPDATA%\portside\portside.db metadata store and the same Docker daemon as the desktop app, so instances created headlessly show up in the GUI and vice versa. This is the surface scripts, CI pipelines, and agents that prefer shell over MCP should use.
Command reference
| Command | What it does |
|---|---|
list [--json] | List instances (id, engine, port, status, container) |
catalog [--json] | Show the bundled engine catalog (engine, tag, image) |
create --engine E --tag T [--port 0] [--lan] [--password P] [--wait 60] [--json] | Create an instance; --port 0 means auto-assign; waits until healthy, then prints connect strings |
start <id> [--json] | Start a stopped instance |
stop <id> [--json] | Stop a running instance (data kept) |
rm <id> [--wipe] [--json] | Drop the container; --wipe also drops the data volume (irreversible) |
db list <id> [--json] | List databases inside an instance |
db create <id> <name> [--json] | CREATE DATABASE (SQL engines; names must be [A-Za-z0-9_]+) |
db drop <id> <name> [--json] | DROP DATABASE (SQL engines) |
conn <id> [--lan] [--strict] [--json] | Print connect strings without touching Docker |
logs <id> [--tail 200] [--json] | Tail container logs (not available for imported servers) |
health <id> [--wait 0] [--json] | Container/tcp/query health; --wait SEC polls until healthy |
mcp | Run the MCP server over stdio (see Agents and MCP) |
setup-agent --host H [--project-dir D] | Register the MCP server with a coding-agent harness (see Agents and MCP) |
setup-path [--remove] | Add (or remove) this binary's directory to the user PATH |
The agent contract: --json and exit codes
Every command prints human-readable text by default and a single JSON object with --json, which is what agents and scripts should always pass. Errors go to stderr as {"error": "...", "hint": "..."} and never include secrets beyond the connection object itself.
| Exit | Meaning |
|---|---|
| 0 | Success |
| 1 | Usage/validation error (bad engine, bad port, bad database name) |
| 2 | Runtime error (Docker down, unhealthy after --wait) - the hint usually says to start Docker Desktop |
portside-cli create --engine postgres --tag 17 --wait 60 --json
# {"id": "a1b2c3d4", "engine": "postgres", "tag": "17", "port": 5433,
# "status": "running", "localhost_url": "postgres://postgres:[email protected]:5433/postgres",
# "lan_url": null, "tls": false}
create --wait polls health once per second (container running, TCP open, live query OK) and only returns once the instance is actually ready, so one call replaces create-then-poll loops. Pass --wait 0 to skip waiting.
Recipes
CI ephemeral database
portside-cli create --engine postgres --tag 17 --wait 120 --json > db.json
export DATABASE_URL=$(python -c "import json; print(json.load(open('db.json'))['localhost_url'])")
# ... migrate + test against $DATABASE_URL ...
portside-cli rm $(python -c "import json; print(json.load(open('db.json'))['id'])") --wipe
The instance is fully isolated per run; --wipe guarantees no volume survives to the next job. The same shape works for MySQL/MariaDB (mysql://...) and Redis (redis://.../0).
Scratch-instance hygiene
portside-cli list --json
portside-cli rm <id> --wipe # per finished experiment
Inactivated instances still hold their volumes; list shows what is stopped versus running, and rm --wipe reclaims disk. See Data and Lifecycle for the exact remove-versus-wipe contract, which the CLI honors identically.
Troubleshooting
| Symptom | Cause and fix |
|---|---|
{"error": ..., "hint": "is Docker running? ..."} exit 2 | Docker Desktop is down - start it and retry; the CLI never starts the daemon itself |
instance <id> not healthy after Ns | The container started but the engine is not answering yet (large image first boot); check portside-cli logs <id> --tail 40, then retry with a longer --wait |
no LAN address detected from conn --lan | The machine is offline or loopback-only; retry without --lan |
| Live-Docker test flakes in CI | The repo's own E2E suite must run serially (--test-threads=1): parallel image pulls flake on Docker Desktop's named pipe |