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.

Terminal capture: 03-cluster-status
         ┌──────────────────────────────────────────┐
         │       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. 1

    You edit registry.yml — add a service, change a port, move it to another host.

  2. 2

    You run ./portoser deploy <machine> <service> (or drag-and-drop in the UI and click Deploy).

  3. 3

    CLI sources every lib/* module, validates the registry, and runs the appropriate deployment type.

  4. 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. 5

    Observe runs. Health checks fire. If something fails, Diagnose fingerprints it.

  6. 6

    Solve applies a matching pattern. If healthy, Standardize records what worked. If not, surface the diagnostic.

  7. 7

    Caddy gets re-synced if the route changed. The history record is written. The UI updates over WebSocket.