Skip to content

Search is only available in production builds. Try building and previewing the site to test it out locally.

Self-hosting

Reactor is designed for easy self-hosting. A single reactor-server binary runs every enabled capability against your PostgreSQL database and local or object storage. This guide covers O2 (single node) deployment — the recommended self-hosted production topology.

The fastest path to a running server:

Terminal window
docker run -d \
--name reactor \
-p 8000:8000 \
-v reactor-data:/data \
-e REACTOR_DATABASE__URL="postgres://user:pass@host:5432/reactor" \
-e REACTOR_ADMIN__TOKEN="$(openssl rand -hex 32)" \
-e REACTOR_AUTH__DATA_KEY="$(openssl rand -base64 32)" \
ghcr.io/reactor-cloud/reactor-server:latest

Verify health:

Terminal window
curl http://localhost:8000/health
# {"status":"ok","capabilities":["auth","data","storage","functions","jobs","sites"]}

A complete local or small-production stack with Postgres:

services:
postgres:
image: postgres:16
environment:
POSTGRES_USER: reactor
POSTGRES_PASSWORD: reactor
POSTGRES_DB: reactor
volumes:
- postgres-data:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U reactor"]
interval: 5s
timeout: 5s
retries: 5
reactor:
image: ghcr.io/reactor-cloud/reactor-server:latest
ports:
- "8000:8000"
environment:
REACTOR_DATABASE__URL: postgres://reactor:reactor@postgres:5432/reactor
REACTOR_ADMIN__TOKEN: ${REACTOR_ADMIN_TOKEN}
REACTOR_AUTH__DATA_KEY: ${REACTOR_AUTH_DATA_KEY}
REACTOR_STORAGE__FS_BASE_PATH: /data/blobs
REACTOR_FUNCTIONS__WORKDIR: /data/functions
REACTOR_SITES__WORKDIR: /data/sites
volumes:
- reactor-data:/data
depends_on:
postgres:
condition: service_healthy
restart: unless-stopped
volumes:
postgres-data:
reactor-data:

Create a .env file (never commit it):

Terminal window
REACTOR_ADMIN_TOKEN=your-secure-admin-token-min-32-chars
REACTOR_AUTH_DATA_KEY=your-base64-32-byte-key

Start the stack:

Terminal window
docker compose up -d
docker compose logs -f reactor

Fly.io is a natural fit for O2: one machine, one volume, automatic HTTPS. The reactor.cloud marketing site runs this way.

Terminal window
flyctl apps create my-reactor
flyctl volumes create reactor_data --region iad --size 10
app = "my-reactor"
primary_region = "iad"
[build]
image = "ghcr.io/reactor-cloud/reactor-server:latest"
[env]
REACTOR_SERVER__BIND = "0.0.0.0:8000"
[mounts]
source = "reactor_data"
destination = "/data"
[[services]]
internal_port = 8000
protocol = "tcp"
auto_stop_machines = false
auto_start_machines = true
min_machines_running = 1
[[services.ports]]
port = 443
handlers = ["tls", "http"]
[[services.ports]]
port = 80
handlers = ["http"]
[services.concurrency]
type = "requests"
hard_limit = 250
soft_limit = 200
[[services.http_checks]]
interval = "15s"
timeout = "5s"
path = "/health"
Terminal window
flyctl secrets set \
REACTOR_DATABASE__URL="postgres://..." \
REACTOR_ADMIN__TOKEN="$(openssl rand -hex 32)" \
REACTOR_AUTH__DATA_KEY="$(openssl rand -base64 32)" \
REACTOR_STORAGE__FS_BASE_PATH="/data/blobs" \
REACTOR_FUNCTIONS__WORKDIR="/data/functions" \
REACTOR_SITES__WORKDIR="/data/sites" \
REACTOR_AUTH__PUBLIC_URL="https://my-reactor.fly.dev"
Terminal window
flyctl deploy
flyctl logs
Terminal window
flyctl certs add api.myapp.com

Update REACTOR_AUTH__PUBLIC_URL to match your custom domain and redeploy.

reactor-server is HTTP-only. Terminate TLS at the edge:

api.myapp.com {
reverse_proxy localhost:8000
}

Place Reactor.toml in your project root or mount it into the container:

[project]
name = "my-app"
id = "proj_01HZ..."
[server]
bind = "0.0.0.0:8000"
[database]
url = "postgres://reactor:reactor@postgres/reactor"
pool_max = 20
[admin]
token = "{{ env REACTOR_ADMIN_TOKEN }}"
allow_remote = false
[auth]
data_key = "{{ env REACTOR_AUTH_DATA_KEY }}"
public_url = "https://api.myapp.com"
[storage]
backend = "fs"
fs_base_path = "/data/blobs"
signing_secret = "{{ env REACTOR_STORAGE_SIGNING_SECRET }}"
[functions]
workdir = "/data/functions"
data_key = "{{ env REACTOR_FUNCTIONS_DATA_KEY }}"
runtimes = ["wasm", "bun"]
[jobs]
webhook_secret = "{{ env REACTOR_JOBS_WEBHOOK_SECRET }}"
[sites]
workdir = "/data/sites"

See the full reference in Configuration.

Terminal window
reactor-server migrate

Migrations run in topological order: auth → data → storage → functions → jobs → sites.

Push a bundle from your project directory:

Terminal window
reactor deploy --endpoint https://api.myapp.com

Under the hood, the CLI POSTs to /_admin/deploy with a tarball containing migrations, function bundles, job manifests, and site assets.

Terminal window
pg_dump -h localhost -U reactor reactor | gzip > backup-$(date +%Y%m%d).sql.gz

Schedule daily backups with cron or your provider’s snapshot feature.

Terminal window
tar -czf reactor-data-$(date +%Y%m%d).tar.gz \
/data/blobs /data/functions /data/sites
[Unit]
Description=Reactor Server
After=network.target postgresql.service
Requires=postgresql.service
[Service]
Type=simple
User=reactor
WorkingDirectory=/opt/reactor
EnvironmentFile=/etc/reactor/env
ExecStartPre=/usr/local/bin/reactor-server migrate
ExecStart=/usr/local/bin/reactor-server
Restart=on-failure
RestartSec=5
[Install]
WantedBy=multi-user.target
EndpointPurpose
GET /healthLoad balancer readiness — 503 if any capability fails
GET /_admin/doctorDeep probes (DB, storage backend, runtimes)
GET /metricsPrometheus scrape target
Terminal window
curl -H "Authorization: Bearer $ADMIN_TOKEN" \
https://api.myapp.com/_admin/doctor | jq