Architecture
One registry, three interfaces, a self-healing core
Portoser is a Bash CLI, a FastAPI backend, and a React UI — all reading the same registry.yml.
The intelligence layer (observe, diagnose, solve, standardize) runs on every deploy.
What's below maps to actual files in the repo.
Cluster status, from the CLI
A snapshot of the same data the web UI shows, served by portoser status.
The registry is the source of truth; everything else hangs off it.
┌──────────────────────────────────────────┐
│ CLI Web UI MCP │
│ (./portoser) (React) (FastMCP) │
└──────────────────────────────────────────┘
│
▼
┌──────────────────────────────────────────┐
│ FastAPI backend — 16 routers /api/* │
│ WebSocket streaming │
└──────────────────────────────────────────┘
│
▼
┌──────────────────────────────────────────┐
│ Intelligence: observe → diagnose → │
│ solve → standardize │
│ Knowledge base (~/.portoser/knowledge/) │
└──────────────────────────────────────────┘
│
▼
┌──────────────────────────────────────────┐
│ Orchestration: docker / local / native │
│ cluster (buildx, build, deploy, sync) │
└──────────────────────────────────────────┘
│
▼
┌──────────────────────────────────────────┐
│ SSH to N machines (no agents) │
│ mTLS via built-in CA · Vault · Caddy │
└──────────────────────────────────────────┘Layers
Interfaces
CLI
~1,100-line Bash entry point at ./portoser. Sources every lib/ module and dispatches to subcommands.
Web UI
React + Vite + react-dnd. Cluster view, dependency graph (ReactFlow), monitoring with custom SVG charts, Vault management, deployment history.
MCP server
FastMCP on the backend. Tool registry stored in Postgres. Lets Claude / Cursor / other AI assistants register tools that act on the cluster.
Backend
FastAPI
16 routers under /api/* — services, machines, deployment, diagnostics, health, knowledge, vault, dependencies, history, mcp, certificates, devices, config, cluster, metrics, uptime, plus WebSocket endpoints.
WebSocket streaming
Deployment logs and metrics stream live to the UI on /api/ws/metrics and /ws.
Knowledge reader
lib/knowledge/reader.sh exposes ~/.portoser/knowledge/ to the UI as JSON.
Self-healing intelligence
Observe
lib/observe/observer.sh writes per-deploy observations to ~/.portoser/observations/.
Diagnose
lib/diagnose/analyzer.sh fingerprints failures (port_conflict, stale_process, docker_daemon_down, disk_exhausted, ssh_unreachable, permission_denied).
Solve
lib/solve/solver.sh applies pattern-matched remediation from lib/solve/patterns/.
Standardize
lib/standardize/learning.sh writes successful playbooks to ~/.portoser/knowledge/playbooks/ for next time.
Orchestration
docker
lib/docker.sh — Docker Compose services. SSH the repo, docker compose up -d, watch health.
local
lib/local.sh — Python services via uv. PID under ~/.portoser/run/, logs under ~/.portoser/logs/.
native
lib/native.sh + lib/platform/detector.sh — launchctl on macOS, systemctl on Linux.
cluster
lib/cluster/{buildx,build,deploy,sync,health}.sh — multi-host build + deploy, especially Pi-aware.
Security & secrets
mTLS
Built-in CA at ~/.portoser/ca/. lib/certificates.sh issues client + server certs. install_ca_on_hosts.sh distributes trust.
Vault
lib/vault.sh integrates with HashiCorp Vault. Web UI does CRUD on KV v2 secrets and AppRole auth.
Keycloak (backend)
OIDC middleware wired in web/backend/keycloak_client.py. Frontend login page is in progress.
SSH key auth
No agents installed on workers. Deploys are SSH + scp.
Networking & state
Caddy
lib/caddy_integration.sh + lib/caddyfile_generator.sh. Registry → Caddyfile → live admin-API reload.
dnsmasq
lib/dns.sh manages a cluster-internal DNS resolver. *.internal hostnames Just Work.
Postgres
Backend persistence: tool registry, audit logs, MCP database. Production state lives in registry.yml; the DB is read-mostly.
Knowledge base
~/.portoser/knowledge/{playbooks,patterns_history}/ — file-based, no daemon needed.
What happens on a deploy
- 1
You edit registry.yml — add a service, change a port, move it to another host.
- 2
You run ./portoser deploy <machine> <service> (or drag-and-drop in the UI and click Deploy).
- 3
CLI sources every lib/* module, validates the registry, and runs the appropriate deployment type.
- 4
For docker: SSH to target, sync repo, docker compose up -d. For local: uv sync, restart process. For native: regenerate unit / plist, reload service manager.
- 5
Observe runs. Health checks fire. If something fails, Diagnose fingerprints it.
- 6
Solve applies a matching pattern. If healthy, Standardize records what worked. If not, surface the diagnostic.
- 7
Caddy gets re-synced if the route changed. The history record is written. The UI updates over WebSocket.