Solo Laptop or Mac mini
A single-host setup. The most common starting point: you want to learn the tool, run side projects, or stage what you'll later split across machines — all on the laptop you already use.
TL;DR
| Hardware | 1 machine — laptop, Mac mini, NUC, or any always-on workstation |
| OS | macOS 12+ or any Linux with bash 4.0+, yq, jq, ssh, curl |
| RAM | 8 GB minimum for the demo + a handful of services. 16–32 GB if you want to host real workloads. |
| Time to first deploy | ~5 minutes (demo via root docker-compose.yml) |
| Biggest gotcha (macOS) | Apple ships Bash 3.2. Install Bash 5 with brew install bash before running the CLI. |
Why pick this shape
- Zero networking surprises.
localhostis your cluster. - All three deployment types work side by side —
dockercontainers,localPython viauv, andnativeservices underlaunchdorsystemd. - The same
registry.ymlyou write here keeps working when you grow into a 3+ machine cluster — you only add new entries tohosts:.
What ships out of the box
The repo's root docker-compose.yml is a "Path 1" demo: Caddy on :8080, a tiny FastAPI dashboard, and one nginx-backed dummy service. It does not start Postgres, Redis, Vault, or Keycloak.
git clone https://github.com/nonagenticai/portoser.git
cd portoser
cp .env.example .env
docker compose up
# Open http://localhost:8080
The demo registry is at demo/registry.demo.yml — useful as a reading sample, not as a base for your own cluster.
Wiring up your own services
Once the demo is up, point CADDY_REGISTRY_PATH at a real registry. Minimal example for a single host:
domain: internal
hosts:
laptop:
ip: 127.0.0.1
arch: arm64-apple # or amd64-linux on x86 Linux
ssh_user: <your-user>
path: <repo-base-path>
roles:
- everything
services:
notes-api:
hostname: notes.internal
current_host: laptop
deployment_type: docker
docker_compose: /notes-api/docker-compose.yml
port: 8400
healthcheck_url: http://notes.internal/health
notes-worker:
hostname: notes-worker.internal
current_host: laptop
deployment_type: local
service_file: /notes_worker/service.yml
port: 9001
caddy:
host: laptop
ingress_host: laptop
config_path: /caddy/Caddyfile
admin_api: http://127.0.0.1:2019
use_admin_api: true
version: 3.0
schema: minimal
Two services on one host, one Docker-based and one Python-uv-based. SSH still happens (Portoser uses SSH even when the target is localhost), so make sure passwordless SSH to your own machine works:
ssh-copy-id $USER@127.0.0.1
ssh $USER@127.0.0.1 "echo ok"
Bringing it up
# Validate the registry
./portoser registry validate
# Deploy a single service
./portoser deploy laptop notes-api
# Deploy multiple at once
./portoser deploy laptop notes-api notes-worker
# Health check
./portoser health check-all
The web UI is its own service, brought up via web/docker-compose.yml:
cd web
docker compose up -d
# Open http://localhost:8989 (frontend) — backend on 8988
Health and observability on a single host
./portoser health check-allprints a JSON-shaped per-service health summary../portoser metricstaps the metrics collector for CPU / memory / disk per service../portoser uptimeshows uptime windows tracked by the uptime collector.- The web UI's monitoring dashboard renders the same data live over WebSocket.
Native services on a single host
native deployments work even without a multi-machine setup. On macOS, this means launchctl; on Linux, systemctl. Useful for things you want managed by the OS rather than Docker — Postgres, dnsmasq, your own daemons.
services:
postgres-local:
hostname: postgres.internal
current_host: laptop
deployment_type: native
service_file: /postgres/service.yml
port: 5432
The service_file points at a YAML descriptor inside your service's repo telling Portoser how to start, stop, and health-check it. lib/native.sh reads that file and dispatches to the right launchctl / systemctl invocation.
Common gotchas
bash: bad substitutionon macOS. You're on Apple's Bash 3.2. Runbrew install bashand put Homebrew's Bash on your PATH.- Port already in use. The self-healing loop's
port_conflictpattern catches these ondeploy. If you want to inspect manually:lsof -nP -iTCP:<port> -sTCP:LISTEN. yqflavor mismatch. Portoser uses Mike Farah's Go-basedyq(v4), not the Pythonyq.brew install yqgives you the right one.- Caddy can't reload. If
caddy.use_admin_api: true, Caddy must be running. For a demo, the docker-compose Caddy is on:8080. For real deployment of services on the host, install Caddy viabrew install caddyand let./portoser caddy syncwrite the Caddyfile for you.
Where this shape falls down
- A single host means no failover. If your laptop sleeps, your cluster sleeps.
- mTLS still works, but the threat model is mostly about staging the configuration that will matter later when you add a second host.
- You're paying SSH and
docker execoverhead even though everything is local. It's fine for tens of services. Past that, consider splitting roles across at least two hosts.
Next
- First Deployment — the hands-on walkthrough that uses this shape.
- Deployment Types — when to pick
dockervslocalvsnative. - When you want to add a second machine: Mac mini Lab or Raspberry Pi Cluster.