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.
Quick start
Section titled “Quick start”The fastest path to a running server:
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:latestVerify health:
curl http://localhost:8000/health# {"status":"ok","capabilities":["auth","data","storage","functions","jobs","sites"]}Download the release binary for your platform, create Reactor.toml, and run:
# Generate secretsexport REACTOR_ADMIN__TOKEN=$(openssl rand -hex 32)export REACTOR_AUTH__DATA_KEY=$(openssl rand -base64 32)
# Run migrations then servereactor-server migratereactor-serverThe server reads Reactor.toml from the working directory and merges REACTOR_* environment overrides.
Docker Compose
Section titled “Docker Compose”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):
REACTOR_ADMIN_TOKEN=your-secure-admin-token-min-32-charsREACTOR_AUTH_DATA_KEY=your-base64-32-byte-keyStart the stack:
docker compose up -ddocker compose logs -f reactorFly.io
Section titled “Fly.io”Fly.io is a natural fit for O2: one machine, one volume, automatic HTTPS. The reactor.cloud marketing site runs this way.
1. Create app and volume
Section titled “1. Create app and volume”flyctl apps create my-reactorflyctl volumes create reactor_data --region iad --size 102. Configure fly.toml
Section titled “2. Configure fly.toml”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"3. Set secrets
Section titled “3. Set secrets”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"4. Deploy
Section titled “4. Deploy”flyctl deployflyctl logs5. Custom domain
Section titled “5. Custom domain”flyctl certs add api.myapp.comUpdate REACTOR_AUTH__PUBLIC_URL to match your custom domain and redeploy.
TLS and reverse proxy
Section titled “TLS and reverse proxy”reactor-server is HTTP-only. Terminate TLS at the edge:
api.myapp.com { reverse_proxy localhost:8000}server { listen 443 ssl; server_name api.myapp.com;
ssl_certificate /etc/letsencrypt/live/api.myapp.com/fullchain.pem; ssl_certificate_key /etc/letsencrypt/live/api.myapp.com/privkey.pem;
location / { proxy_pass http://127.0.0.1:8000; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; }}Configuration file
Section titled “Configuration file”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.
Migrations
Section titled “Migrations”reactor-server migratecurl -X POST \ -H "Authorization: Bearer $REACTOR_ADMIN_TOKEN" \ https://api.myapp.com/_admin/migrate[data]run_migrations = truemigrations_dir = "./migrations"Migrations run in topological order: auth → data → storage → functions → jobs → sites.
Deploying application code
Section titled “Deploying application code”Push a bundle from your project directory:
reactor deploy --endpoint https://api.myapp.comUnder the hood, the CLI POSTs to /_admin/deploy with a tarball containing migrations, function bundles, job manifests, and site assets.
Backups
Section titled “Backups”Database
Section titled “Database”pg_dump -h localhost -U reactor reactor | gzip > backup-$(date +%Y%m%d).sql.gzSchedule daily backups with cron or your provider’s snapshot feature.
tar -czf reactor-data-$(date +%Y%m%d).tar.gz \ /data/blobs /data/functions /data/sitesSystemd (bare metal)
Section titled “Systemd (bare metal)”[Unit]Description=Reactor ServerAfter=network.target postgresql.serviceRequires=postgresql.service
[Service]Type=simpleUser=reactorWorkingDirectory=/opt/reactorEnvironmentFile=/etc/reactor/envExecStartPre=/usr/local/bin/reactor-server migrateExecStart=/usr/local/bin/reactor-serverRestart=on-failureRestartSec=5
[Install]WantedBy=multi-user.targetHealth checks
Section titled “Health checks”| Endpoint | Purpose |
|---|---|
GET /health | Load balancer readiness — 503 if any capability fails |
GET /_admin/doctor | Deep probes (DB, storage backend, runtimes) |
GET /metrics | Prometheus scrape target |
curl -H "Authorization: Bearer $ADMIN_TOKEN" \ https://api.myapp.com/_admin/doctor | jqNext steps
Section titled “Next steps”- Deployment grades — when to move beyond G2
- Security — secrets, admin endpoints, checklist
- Observability — metrics, logs, tracing