Skip to content

Engine on Fly.io

Fly.io runs the engine image as a long-lived container, a Machine. Fly bills by usage, and new accounts get a short trial rather than a free plan. A Machine with auto_stop_machines stops when idle and starts on the next request.

  • A Fly.io account and the fly CLI signed in.
  • A Postgres, MySQL or SQL Server database the Machine can reach. On Supabase or Neon, use the connection string each page recommends.

Save this as fly.toml:

app = "your-lyeve-engine"
primary_region = "iad"
[build]
image = "ghcr.io/lyeve-labs/lyeve-core:latest"
# Content API: /api/v1 and /.well-known/jwks.json
[[services]]
internal_port = 3002
protocol = "tcp"
[[services.ports]]
port = 443
handlers = ["tls", "http"]
[[services.http_checks]]
interval = "15s"
timeout = "2s"
grace_period = "20s"
method = "get"
path = "/readyz"
# Admin API: /api/admin
[[services]]
internal_port = 3001
protocol = "tcp"
[[services.ports]]
port = 8443
handlers = ["tls", "http"]
[[services.http_checks]]
interval = "15s"
timeout = "2s"
grace_period = "20s"
method = "get"
path = "/readyz"

The engine listens on 3001 for the Admin API and 3002 for the Content API, and the image already binds both on all interfaces. Two services cannot share external port 443 in one app, so the Content API takes 443 and the Admin API takes 8443. If nothing outside Fly needs the Admin API, drop its [[services]] block and reach port 3001 over Fly's private network.

Run one Machine. A second Machine signs with a key of its own and refuses the first one's tokens. Pin a release tag instead of latest in production. See Docker images.

Set every setting with fly secrets set rather than in [env]:

Terminal window
fly secrets set \
DATABASE_URL="postgres://<user>:<password>@<host>:5432/<dbname>?sslmode=require" \
JWT_SECRET="$(openssl rand -hex 32)" \
ENCRYPTION_KEY="$(openssl rand -hex 32)" \
LYEVE_AUDIT_HMAC_KEY="$(openssl rand -hex 32)" \
SECURE_COOKIE=true \
RATE_LIMIT_RPS=100 \
CORS_ORIGINS="https://your-app.example.com"

Generate ENCRYPTION_KEY once and keep it. A new value makes stored secrets, such as MFA seeds, unreadable. Production refuses to start without these values. Configuration lists every check, and every other variable. Add LYEVE_LICENSE_KEY for paid features.

Point DATABASE_URL at your database. The engine reads the type from the scheme, as supported databases shows. Use sslmode=require for a PostgreSQL database outside Fly's private network, or the engine logs a warning on every start.

The checks in fly.toml use /readyz. Both listeners serve /healthz, /readyz and /startup without authentication, and over plain HTTP even with SECURE_COOKIE=true. Do not use /api/admin/health or /api/v1/health: they need authentication, so a check gets 401 and marks a healthy Machine down. Health endpoints says what each one checks.

Terminal window
fly deploy
fly status
curl https://your-lyeve-engine.fly.dev/.well-known/jwks.json
curl -6 https://your-lyeve-engine.fly.dev:8443/api/admin/setup

The first curl lists the engine's public signing key. The second goes over IPv6, because port 8443 does not answer on a shared IPv4 address, and a new install answers {"setup_required":true,"token_source":"log"}. The setup token is the one-time setup_token in fly logs, or LYEVE_SETUP_TOKEN when you set it. The quickstart continues from step 2.