Purple8 Hyper Graph — Cloud Deployment Guide
This guide covers deploying P8G on every major cloud platform. It is opinionated on purpose — for each platform it tells you exactly where things will break, so you don't have to learn it the hard way.
See also:
DOCKER_QUICKSTART.mdfor local first-run anddocs/SYSTEM_REQUIREMENTS.mdfor RAM/CPU sizing.
The one thing every platform must get right
P8G has one non-negotiable infrastructure requirement: P8G_DATA_PATH must point to a persistent, durable volume.
P8G_DATA_PATH is where BrickCoreStorage writes everything — the graph, the WAL, auth data, the HNSW index snapshots. If it points to a container's ephemeral filesystem, all data is wiped on every redeploy or restart.
Every section below tells you exactly how to satisfy this requirement on each platform — and what will silently go wrong if you don't.
Platform comparison
| Platform | Persistent volume | Env var behaviour | RAM sizing | Cost | Recommended for |
|---|---|---|---|---|---|
| Fly.io | ✅ Reliable — Fly Volumes | ✅ Immediate (fly secrets set, no redeploy) | ✅ Exact (choose machine RAM) | ~$5–20/mo | Production cloud |
| Hetzner VPS | ✅ Block storage or local SSD | ✅ systemd EnvironmentFile | ✅ Full control | €4–16/mo | Self-hosted / on-prem |
| DigitalOcean Droplet | ✅ Block storage volumes | ✅ Standard env vars | ✅ Full control | $6–24/mo | Self-hosted / on-prem |
| Render | ⚠️ Available but auto-deleted after 90 days on free tier | ✅ Instant env var changes | ⚠️ Shared CPU on free tier | $7/mo (starter) | Dev/staging |
| Railway | ⚠️ Available but requires explicit volume + mount | ⚠️ Env var changes don't always trigger redeploy | ⚠️ No RAM guarantee on shared plans | $5/mo (hobby) | Stateless services only |
| AWS ECS (Fargate) | ✅ EFS mount or EBS | ✅ Parameter Store / Secrets Manager | ✅ Exact | $15–50/mo | Enterprise / existing AWS |
| Red Hat OpenShift | ✅ PVCs (CSI block/file) | ✅ Secrets / ConfigMaps | ✅ Exact (Dockerfile.rhel UBI9) | Cluster cost | Enterprise OpenShift / RHEL |
| GCP Cloud Run | ⚠️ Cloud Run is stateless by design — requires Cloud Filestore NFS mount | ✅ Instant | ⚠️ Requires NFS setup | $10–30/mo | Not recommended for P8G |
| Self-hosted Docker | ✅ Local disk / mounted volume — most durable | ✅ docker run -e / compose env | ✅ Full hardware | Hardware cost only | Customer on-prem |
Fly.io — Recommended for cloud deployments
Fly.io is the best fit for P8G in the cloud. Its Machines API gives you deterministic RAM sizing, fly secrets set takes effect without a redeploy, and Fly Volumes are persistent across deployments.
Setup
1. Install flyctl and create your app:
fly launch --no-deploy --name purple8-graph2. Create a persistent volume (1 GB is enough to start — Fly can resize online):
fly volumes create p8g_data --region lhr --size 13. Configure fly.toml:
app = "purple8-graph"
primary_region = "lhr"
[build]
[mounts]
source = "p8g_data"
destination = "/data"
[env]
P8G_DATA_PATH = "/data"
PORT = "8080"
[[services]]
internal_port = 8080
protocol = "tcp"
[[services.ports]]
port = 443
handlers = ["tls", "http"]
[[services.ports]]
port = 80
handlers = ["http"]
[services.concurrency]
type = "connections"
hard_limit = 25
[vm]
memory = "2gb"
cpu_kind = "shared"
cpus = 24. Set secrets (takes effect immediately — no redeploy needed):
fly secrets set \
JWT_SECRET=$(python3 -c "import secrets; print(secrets.token_hex(32))") \
BOOTSTRAP_TOKEN=$(python3 -c "import secrets; print(secrets.token_hex(32))")5. Deploy:
fly deploy6. Confirm the volume is mounted:
fly ssh console -C "ls /data"You should see auth/, storage/, wal/ directories appear after the first request.
What works well
fly secrets set KEY=valuetakes effect in ~5 seconds. No redeploy.- Fly Volumes survive restarts, deploys, and machine migrations.
fly scale memory 4096resizes RAM live — P8G restarts in ~10 seconds.fly logsgives you structlog JSON output in real time.- Rollback with
fly deploy --image <previous-image>restores previous state from the same volume.
What to watch
- A single Fly Volume is attached to a single region. For HA, use Fly's volume replication (
fly volumes fork). fly.toml[mounts]is notfly.toml[volumes]— use[mounts].- Free tier Fly machines are suspended after inactivity. Use
min_machines_running = 1in[http_service]if you need always-on.
Hetzner VPS — Best value for self-hosted
A Hetzner CX22 (2 vCPU, 4 GB RAM, €4/mo) runs P8G comfortably for corpora up to ~500k nodes. A CX32 (4 vCPU, 8 GB RAM, €8/mo) handles multi-million node graphs.
Setup
1. Create a server (Ubuntu 24.04 LTS): From the Hetzner Cloud Console or:
hcloud server create --name p8g --type cx22 --image ubuntu-24.04 --location nbg12. Attach a block volume for data (optional but recommended for larger corpora):
hcloud volume create --name p8g-data --size 20 --server p8g3. Mount the volume:
# On the server:
mkfs.ext4 /dev/disk/by-id/scsi-0HC_Volume_<id>
mkdir -p /data
echo '/dev/disk/by-id/scsi-0HC_Volume_<id> /data ext4 defaults,nofail 0 2' >> /etc/fstab
mount -a4. Install Docker:
apt-get update && apt-get install -y docker.io
systemctl enable --now docker5. Create /etc/purple8/env:
mkdir -p /etc/purple8
cat > /etc/purple8/env <<EOF
P8G_DATA_PATH=/data
JWT_SECRET=$(python3 -c "import secrets; print(secrets.token_hex(32))")
BOOTSTRAP_TOKEN=$(python3 -c "import secrets; print(secrets.token_hex(32))")
PORT=8080
EOF
chmod 600 /etc/purple8/env6. Create a systemd service:
# /etc/systemd/system/purple8-graph.service
[Unit]
Description=Purple8 Hyper Graph
After=network.target docker.service
Requires=docker.service
[Service]
EnvironmentFile=/etc/purple8/env
ExecStartPre=-/usr/bin/docker stop p8g
ExecStartPre=-/usr/bin/docker rm p8g
ExecStart=/usr/bin/docker run --rm \
--name p8g \
-p 8080:8080 \
-v /data:/data \
--env-file /etc/purple8/env \
ghcr.io/purple8-technologies/purple8-graph:latest
ExecStop=/usr/bin/docker stop p8g
Restart=always
RestartSec=10
[Install]
WantedBy=multi-user.targetsystemctl daemon-reload
systemctl enable --now purple8-graph7. Update env vars without rebuilding:
# Edit the file, then restart:
nano /etc/purple8/env
systemctl restart purple8-graphWhat works well
- Cheapest option for persistent workloads.
- Full RAM and CPU control — no noisy neighbours.
- Block volumes can be resized online via
hcloud volume resize. - systemd restart policy handles P8G crashes correctly.
journalctl -u purple8-graph -fgives you live structlog JSON output.
What to watch
- No built-in TLS. Use
caddyornginxin front of P8G (apt install caddy). - Hetzner's public network is unfiltered — add
ufw allow 22 && ufw allow 443 && ufw enableimmediately. - Block volumes are tied to one datacenter location. Snapshots are your backup strategy.
Railway — Proceed with caution for stateful services
Railway is excellent for stateless FastAPI services (like Purple8 Command Center). It is not ideal for P8G because of two compounding problems:
Problem 1: Ephemeral filesystem by default
Railway containers use an ephemeral filesystem. Without a Volume explicitly attached, P8G_DATA_PATH=./data writes to the container FS — wiped on every redeploy, every restart, every crash.
Fix: Railway → your P8G service → Volumes → add a volume → mount path /data → set P8G_DATA_PATH=/data in Variables.
Problem 2: Variable changes don't always trigger a redeploy
Railway sometimes requires a manual redeploy after adding or changing env vars. The new value isn't loaded until the container restarts.
Fix: After any variable change, go to Deployments tab → click Redeploy on the latest deployment.
Problem 3: No RAM guarantee on shared plans
Railway's Hobby plan allocates shared CPU and RAM. P8G's CortexAutoConfig probes available RAM at startup to size the processing envelope. On a shared container, psutil may report RAM from the host — leading to over-allocation that Railway then OOM-kills.
Fix: Either use Railway's Pro plan (dedicated RAM) or set HNSW_M and BATCH_SIZE explicitly to stay under 512 MB:
# Conservative settings for Railway shared plan
HNSW_M=8
HNSW_EF_CONSTRUCTION=64
BATCH_SIZE=500What works well on Railway
- Stateless services with no
P8G_DATA_PATHdependency (Command Center, DocIntel). - Fast prototypes where data loss on redeploy is acceptable.
- Services that read from P8G but don't write (query-only replicas).
Recommended Railway configuration (if you must use it for P8G)
# railway.toml
[build]
builder = "DOCKERFILE"
[deploy]
startCommand = "sh -c 'PYTHONPATH=src uvicorn purple8_graph.secure_server:app --host 0.0.0.0 --port ${PORT:-8080} --workers 1'"
healthcheckPath = "/health"
healthcheckTimeout = 30
restartPolicyType = "ON_FAILURE"
restartPolicyMaxRetries = 3Required Railway Variables:
P8G_DATA_PATH = /data ← must match your Volume mount path
JWT_SECRET = <token_hex_32>
BOOTSTRAP_TOKEN = <token_hex_32>
WORKERS = 1 ← multi-worker needs P8G_DATA_PATH on shared storageRender — Good for staging
Render's managed disks are reliable and env var changes apply on the next deploy (triggered automatically). The free tier uses shared CPU with sleeping instances — use the Starter plan ($7/mo) for always-on.
Setup
1. Create a Web Service from your Docker image or repo.
2. Add a Disk: Render Dashboard → your service → Disks → add disk → mount path /data → size 1 GB.
3. Add env vars:
P8G_DATA_PATH /data
JWT_SECRET <token_hex_32>
BOOTSTRAP_TOKEN <token_hex_32>4. Important: Render free-tier disks are deleted after 90 days of inactivity. Use the paid tier for any persistent data.
What works well
- Env var changes trigger an automatic redeploy.
- Disks are persistent across restarts and deploys on paid plans.
- Auto-TLS with custom domain in one click.
What to watch
- Render's free plan CPU-throttles sleeping containers — cold start after inactivity can take 30–60 seconds.
- Disk IOPS on Starter is lower than Fly.io or Hetzner. For write-heavy ingestion, use a larger paid tier.
AWS ECS (Fargate) — Enterprise / existing AWS
For teams already in AWS, Fargate + EFS (Elastic File System) is the correct architecture. EBS is faster but cannot be shared across tasks.
Minimal task definition (excerpt)
{
"containerDefinitions": [{
"name": "purple8-graph",
"image": "ghcr.io/purple8-technologies/purple8-graph:latest",
"environment": [
{"name": "P8G_DATA_PATH", "value": "/data"},
{"name": "PORT", "value": "8080"}
],
"secrets": [
{"name": "JWT_SECRET", "valueFrom": "arn:aws:ssm:..."},
{"name": "BOOTSTRAP_TOKEN", "valueFrom": "arn:aws:ssm:..."}
],
"mountPoints": [{
"sourceVolume": "p8g-data",
"containerPath": "/data",
"readOnly": false
}]
}],
"volumes": [{
"name": "p8g-data",
"efsVolumeConfiguration": {
"fileSystemId": "fs-xxxxxxxx",
"transitEncryptionEnabled": true
}
}]
}Secrets via Parameter Store:
aws ssm put-parameter \
--name /p8g/JWT_SECRET \
--value "$(python3 -c "import secrets; print(secrets.token_hex(32))")" \
--type SecureStringWhat works well
- EFS survives task replacement, scaling, and AZ failover.
- IAM task roles eliminate long-lived credentials from the container.
- CloudWatch Logs integrates with P8G's structlog JSON output natively.
- ECS Service Connect handles service discovery if running multiple services.
What to watch
- EFS IOPS are ~3 IOPS/GB provisioned. For write-heavy workloads, provision throughput explicitly or use
burstingmode. - Fargate has no persistent local disk — everything must go to EFS.
- Cold start on Fargate is ~10–30 seconds. Use Application Auto Scaling with a minimum of 1 task.
GCP Cloud Run — Not recommended for P8G
Cloud Run is a stateless platform by design. Every instance starts with a clean filesystem. Persistent storage requires mounting a Cloud Filestore NFS volume — which adds ~10 seconds to cold start, costs extra, and introduces a network FS failure mode that affects P8G's WAL.
Recommendation: Use GKE Autopilot with a PersistentVolumeClaim instead. Cloud Run is the wrong substrate for a stateful engine.
If you are committed to Cloud Run:
- Provision a Cloud Filestore instance (Basic HDD, 1 TB minimum — smaller tiers are not available)
- Mount via VPC connector
- Set
P8G_DATA_PATH=/mnt/p8g-data - Accept the cold-start and IOPS overhead
Self-hosted Docker & Red Hat OpenShift — The right answer for most customers
The commercial Docker image (Dockerfile.commercial) and the Red Hat OpenShift-certified UBI9 image (Dockerfile.rhel, based on registry.access.redhat.com/ubi9/python-312:9.5) are the recommended deployment paths for any customer who:
- Wants to run Purple8 Hyper Graph (
v0.72.0) on their own hardware or Kubernetes/OpenShift cluster - Cannot (or will not) manage a public cloud service
- Runs air-gapped or on a regulated network
Purple8 is an approved Red Hat Technology Partner and NVIDIA Inception program member. Dockerfile.rhel is engineered specifically for Red Hat OpenShift's default restricted Security Context Constraint (arbitrary-UID assignment, group 0 write permissions across /data, /backups, /app/logs, /opt/venv, and /app, /licenses/LICENSE, and non-root UID 1001 default).
A single docker run (or oc new-app on OpenShift) with a mounted data volume brings up the engine, REST API, 125 MCP tools across 16 namespaces, and LCNC admin UI:
| Deployment | Default P8G_DATA_PATH |
|---|---|
| Docker / Podman / OpenShift (volume mount) | /data inside the container → host path or PVC of your choice |
pip wheel (pip install purple8-hyper-graph) | ~/.local/share/purple8/graph/data |
For Docker, set -v /host/path:/data and P8G_DATA_PATH=/data. No further configuration needed.
Environment variable cheatsheet
These variables must always be set in production, regardless of platform:
| Variable | Required | Description | Generate with |
|---|---|---|---|
P8G_DATA_PATH | Critical | Persistent storage root. Must point to a mounted volume. | Set to /data (or your mount path) |
JWT_SECRET | Critical | Signs all auth tokens. Changing this invalidates all active sessions. | python3 -c "import secrets; print(secrets.token_hex(32))" |
BOOTSTRAP_TOKEN | Required once | Gates first-owner creation at /auth/setup. Rotate after first use. | python3 -c "import secrets; print(secrets.token_hex(32))" |
PORT | Platform-set | Injected by Fly/Railway/Render automatically. Default 8080. | — |
WORKERS | Optional | Number of uvicorn workers. Keep at 1 unless your storage is on shared FS. | 1 |
Variables you should not need to set (P8G handles them via CortexAutoConfig):
| Variable | Why you don't need it |
|---|---|
HNSW_M, HNSW_EF_CONSTRUCTION | Auto-sized to fit available RAM at startup |
BATCH_SIZE | Auto-sized by CortexAutoConfig based on available RAM |
MAX_MEMORY_GB | Not a P8G variable — RSS is bounded by the Cortex model, not a config cap |
Diagnosing P8G_DATA_PATH problems
If you see any of these symptoms after a redeploy, P8G_DATA_PATH is almost certainly pointing to an ephemeral path:
setup_required: trueon/healthafter a previously completed setup- Users created yesterday are gone
- API keys no longer work
- The graph appears empty
Diagnosis:
# Check what P8G actually resolved at startup:
# Look for this line in your platform's log output:
# event="data_path_resolved" data_path="/data" ← correct
# event="data_path_ephemeral" data_path="./data" ← problemFix sequence:
- Attach a persistent volume and mount it at
/dataon your platform (see per-platform sections above) - Set
P8G_DATA_PATH=/datain your env vars - Redeploy
- Confirm
data_path_resolved data_path=/datain startup logs - Re-run
/auth/setup(or/portal/bootstrapfor Command Center) — data was lost on the ephemeral path
Multi-worker deployments
P8G's AuthManager handles multiple workers correctly via mtime-based file reload (TTL=2s). However, WORKERS > 1 requires all workers to share the same P8G_DATA_PATH. This means:
| Setup | Workers | Note |
|---|---|---|
| Single container, local volume | 1 or 2 | Correct — all workers share the same mounted path |
| Multiple containers / replicas | 1 per container | Do not run replicas pointing at separate volumes — graph state will diverge |
| Multiple containers, shared NFS (EFS/Filestore) | 1 per container | Supported — mount the same NFS path on all replicas |
For most deployments: WORKERS=1 is correct. P8G is I/O-bound on ingest and CPU-bound on vector search — horizontal scaling is done by adding more P8G instances with separate data paths (tenant isolation), not by adding workers to one instance.