Server Setup

Deploy and configure the Pharos server for Home Lab or Enterprise environments.

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-server

3. 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.key

If 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 pharos

Didn’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.

ModeDescriptionWrite Security
openFor local-only Home Labs.Reads are unauthenticated; writes always require the auto-generated or an authorized key.
protectedRecommended for remote access (not the default).Requires authorized SSH key.
scopedFor 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_percentage
    • pharos_memory_usage_bytes
    • pharos_total_records
  • Write-Path Counters: pharos_records_added_total, pharos_records_updated_total, and pharos_records_deleted_total, each labeled by source — which client actually performed the write (mdb, ph, pharos-scan, pharos-pulse, or web-console). Every record is stamped server-side with the source that 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.