One-Liner Installation
Prefer a frictionless automated setup? Use our Automated Installation Guide to deploy the Pharos Server in under 60 seconds.
The pharos-server is an Invisible High-Performance Engine optimized for Linux (Ubuntu LTS). It acts as the central RFC 2378 backplane for the entire ecosystem, providing the lightning-fast data layer for your Agent-Native Control Plane.
🚀 3-Minute Quick Start: The “YOLO” Lab
Reach a success state in under 180 seconds. No installation required if you have Podman/Docker.
# TLS is mandatory — generate a throwaway self-signed cert for the demo
mkdir -p ./pharos-demo-certs
openssl req -x509 -newkey rsa:2048 -nodes -days 365 \
-keyout ./pharos-demo-certs/server.key -out ./pharos-demo-certs/server.crt \
-subj "/CN=pharos-demo" -addext "subjectAltName=DNS:localhost,IP:127.0.0.1"
chmod 600 ./pharos-demo-certs/server.key
# Start the Pharos Server in the background
podman run -d --name pharos-demo \
-p 2378:2378 -p 9090:9090 \
-v ./pharos-demo-certs:/certs:Z \
-e PHAROS_TLS_CERT=/certs/server.crt \
-e PHAROS_TLS_KEY=/certs/server.key \
ghcr.io/iamrichardd/pharos-server:latest
Didn’t come up? Check podman logs pharos-demo — pharos-server refuses to start without a readable cert/key pair.
🛡️ Production Deployment
For production use, we recommend a native installation in a Proxmox LXC container or a dedicated Ubuntu VM.
🏠 Home Lab: Persistent JSON Storage
Perfect for Proxmox LXC containers. Pharos uses a simple, restart-survivable JSON file for your data. The server binary is statically compiled and has no dependencies on Ubuntu-specific APIs, making Debian 13 (“Trixie”) or Ubuntu 26.04 LTS (“Resolute Raccoon”) equally excellent targets.
1. Prepare the LXC Container
Configure the container with a minimum of 1 CPU (vCPU), 512MB RAM, and 2GB Disk.
2. Install the Pharos Server
# Ensure curl and openssl are installed
apt-get update && apt-get install -y curl openssl
# Download the Pharos Server binary (always resolves to the latest release)
curl -sSL -O https://github.com/iamrichardd/pharos/releases/latest/download/pharos-server-linux-x86_64
chmod +x pharos-server-linux-x86_64
mv pharos-server-linux-x86_64 /usr/local/bin/pharos-server3. Generate a TLS Certificate
PHAROS_TLS_CERT/PHAROS_TLS_KEY are mandatory — the server refuses to start without them. A self-signed pair is enough for a Home Lab. Include a SAN entry (not just CN) — modern TLS clients ignore CN for hostname verification:
mkdir -p /etc/pharos/certs
LAN_IP="$(hostname -I | awk '{print $1}')"
openssl req -x509 -newkey rsa:2048 -nodes -days 365 \
-keyout /etc/pharos/certs/pharos-server.key \
-out /etc/pharos/certs/pharos-server.crt \
-subj "/CN=pharos-server" \
-addext "subjectAltName=DNS:pharos-server,DNS:localhost,IP:127.0.0.1${LAN_IP:+,IP:$LAN_IP}"
chmod 600 /etc/pharos/certs/pharos-server.keyIf any Pulse agents will connect to this server by its LAN IP rather than from localhost, that IP must be in the SAN above (the $(hostname -I ...) substitution adds the primary one; append more IP:x.x.x.x entries for additional interfaces) — otherwise remote nodes pass CA trust but fail hostname verification with certificate not valid for name. The Automated Installation Guide handles this for every detected interface automatically; or, for a custom domain instead of just an IP, install.sh -- server your.domain.com adds it to the certificate automatically.
4. Configure as a Systemd Service
cat <<'EOF' > /etc/systemd/system/pharos.service
[Unit]
Description=Pharos Infrastructure Server
After=network.target
[Service]
ExecStart=/usr/local/bin/pharos-server
ExecReload=/bin/kill -HUP $MAINPID
Environment=PHAROS_TLS_CERT=/etc/pharos/certs/pharos-server.crt
Environment=PHAROS_TLS_KEY=/etc/pharos/certs/pharos-server.key
Environment=PHAROS_STORAGE_PATH=/var/lib/pharos/data.json
Environment=PHAROS_SECURITY_TIER=open
Restart=always
User=root
[Install]
WantedBy=multi-user.target
EOF
systemctl daemon-reload
systemctl enable --now pharosDidn’t come up? Check journalctl -u pharos -f.
Tip
This example runs as root for simplicity. Prefer a dedicated unprivileged service user with proper certificate permissions handled for you? Use the Automated Installation Guide instead — curl -sSL .../install.sh | bash -s -- server.
🔐 Security Configuration
Pharos implements a tiered security model to balance accessibility and safety.
| Mode | Description | Write Security |
|---|---|---|
open | For local-only Home Labs. | Reads are unauthenticated; writes always require the auto-generated or an authorized key. |
protected | Recommended for remote access (not the default). | Requires authorized SSH key. |
scoped | For multi-tenant Enterprise teams. | Requires SSH key + LDAP group matching. |
The installer’s default is open
Both the Automated Installation Guide’s server/hub targets and this page’s manual systemd example ship PHAROS_SECURITY_TIER=open — that means anyone who can reach the port can read your data unauthenticated; writes still require a key, always, in every tier. open is intended for a self-contained Home Lab with no untrusted network access, not as a general-purpose default — if you’re exposing the server beyond a trusted local network, switch to protected (see below for provisioning a key first, since protected refuses to self-generate one).
Next Steps output from the installer tells you which tier is active; systemctl cat pharos-server shows it directly (Environment=PHAROS_SECURITY_TIER=...) on any install.
Provision a key before switching tiers
protected and scoped refuse to self-generate an admin credential — that convenience shortcut only exists for open. If you set PHAROS_SECURITY_TIER to protected or scoped while PHAROS_KEYS_DIR is empty, the server starts successfully but rejects every authenticated command (queries, adds, deletes — everything except status/login). It won’t crash or refuse to boot, so there’s no obvious signal beyond one log line: SECURITY: ... tier requires an operator-provisioned key.
Enroll a key before switching tiers, or switch and enroll immediately after — either way, no restart is needed (see below).
Enrolling SSH Keys
To authorize a user for write access, place their public key in the keys directory:
mkdir -p /etc/pharos/keys
cp ~/.ssh/id_ed25519.pub /etc/pharos/keys/admin.pub
export PHAROS_KEYS_DIR="/etc/pharos/keys"
This takes effect immediately — no restart required. pharos-server re-scans PHAROS_KEYS_DIR on SIGHUP and atomically swaps in the new key set (if the directory is unreadable or transiently empty mid-copy, the previous key set is kept — reload never leaves you with zero authorized keys):
systemctl reload pharos # manual systemd setup above (unit name: pharos)
systemctl reload pharos-server # Automated Installation Guide (unit name: pharos-server)
kill -HUP $(pgrep pharos-server) # or send it directly, regardless of unit name
The same reload also picks up a renewed PHAROS_TLS_CERT/PHAROS_TLS_KEY pair, if you’re using an externally renewed certificate — no restart needed for that either.
🔗 Multi-Server Synchronization
For a multi-node home lab, a node can replicate its writes to other Pharos servers:
export PHAROS_SYNC_ADDR="pharos-node-a.lan:2378" # this node's own advertised peer address
export PHAROS_BOOTSTRAP_PEER="pharos-node-b.lan:2378" # optional: pull an existing peer's records on startup
PHAROS_SYNC_ADDR both advertises this node as a replication target and turns on relaying its own
add/change/delete commands to every peer it currently knows about. PHAROS_BOOTSTRAP_PEER is
a one-time pull from that peer at startup — it is not a persistent, bidirectional membership
protocol. In practice this means peer awareness flows in the direction you bootstrap: if node B
bootstraps from node A, B learns about A, but A does not automatically learn about B in return.
For a fully mutual mesh today, bootstrap every node from every other node, or add each peer’s
role="pharos-server" record manually with mdb add.
Peer Authorization Required for Replication
For a replicated write to be correctly recognized as trusted (and therefore not re-replicated in a ping-pong loop or double-notified), each node’s own SSH key must be registered in every other node’s PHAROS_KEYS_DIR with peer somewhere in the filename (e.g., nodeA-peer_id_ed25519.pub).
This uses the same hot-reloadable keys directory already used for regular client authorization (see Enrolling SSH Keys above for reload guidance). Without this setup, replicated writes will still succeed, but multi-server synchronization silently loses the anti-ping-pong and duplicate-notification protection.
🖥️ Live Terminal Dashboard
For interactive debugging on a Home Lab box you’re sitting in front of, run the server with --tui instead of letting it log to stdout:
pharos-server --tui
This swaps the normal log stream for a live terminal dashboard in the same window: current CPU/memory usage, total record count, and a scrolling event feed of every add/update as it happens, refreshed 4 times a second. Press q to quit — this also stops the server, since it shares the same shutdown path as Ctrl+C.
It needs a real terminal to draw into and keyboard input to quit, so it’s for foreground, interactive use — not something you’d run under systemd (no terminal to attach to, and normal journalctl logging is suppressed while it’s active). Reach for it when you want to watch what’s happening on a server in real time, e.g. confirming a pharos-scan --auto cycle or a bulk mdb add script is actually landing.
📊 Monitoring & Observability
Pharos exposes Prometheus metrics on port 9090 to track performance and inventory health.
-
Endpoint:
http://localhost:9090/metrics -
Health Gauges:
pharos_cpu_usage_percentagepharos_memory_usage_bytespharos_total_records
-
Write-Path Counters:
pharos_records_added_total,pharos_records_updated_total, andpharos_records_deleted_total, each labeled bysource— which client actually performed the write (mdb,ph,pharos-scan,pharos-pulse, orweb-console). Every record is stamped server-side with thesourcethat created it (immutable once set, never client-supplied), which is what makes these counters attributable per-tool instead of just one global write counter.These are standard Prometheus counters — always graph them with
rate()/increase(), not the raw value, since they reset to zero on every server restart. A starting query for a per-source write-rate panel in Grafana:sum by (source) (rate(pharos_records_added_total[5m]))Worth alerting on: any nonzero rate where
source="unknown". Every write is supposed to come from a recognized Pharos client, so that label appearing at all means something is writing to your inventory that Pharos doesn’t recognize as one of its own tools.