Self-hosted
Self-host Odly
1 · Prerequisites & infrastructure
Odly ships as a small set of containers. The reference docker-compose.prod.yml wires them together for you.
- PostgreSQL 16 with pgvector — the reference image is pgvector/pgvector:pg16. If you bring your own Postgres, it must be v16+ and you must enable the extension once: CREATE EXTENSION vector;
- Valkey 8 — a Redis-compatible store used for the BullMQ job queues (async work, scheduled checks).
- Backend container — image ghcr.io/orqesto/odly-backend:main. Budget roughly 3.5 GB of memory for it.
- nginx — sits in front and terminates TLS. The backend, database and Redis all bind to loopback; only nginx is exposed to the network.
2 · Environment
Odly validates its configuration at startup and refuses to boot if anything required is missing or invalid. Set these before the first run — the authoritative template is .env.client.example.
- LICENSE_KEY — required in production. On a missing, invalid or expired key the app calls exit(1) and stops.
- DATABASE_URL — in the reference compose this is built for you from DB_USER / DB_PASSWORD / DB_NAME, so don't set DATABASE_URL directly there.
- JWT_SECRET — min 32 characters. Generate with openssl rand -base64 32.
- ENCRYPTION_KEY — min 32 characters. Encrypts integration credentials at rest, so treat it like a root secret and never rotate it casually.
- FRONTEND_URL — comma-separated list of allowed origins.
- REDIS_HOST / REDIS_PORT / REDIS_PASSWORD — how the app reaches Valkey.
- BACKEND_URL — recommended, so email-verification and password-reset links point back to the right host.
A minimal .env to boot:
# --- required in production --- LICENSE_KEY=xxxxxxxx-your-license-key DEPLOYMENT_MODE=self_hosted SUBSCRIPTION_ENFORCEMENT_ENABLED=false # database — compose builds DATABASE_URL from these DB_USER=odly DB_PASSWORD=change-me DB_NAME=odly # secrets — min 32 chars each (openssl rand -base64 32) JWT_SECRET=REPLACE_WITH_32+_CHARS ENCRYPTION_KEY=REPLACE_WITH_32+_CHARS # origins & links FRONTEND_URL=https://help.acme.com BACKEND_URL=https://api.acme.com # valkey / redis REDIS_HOST=redis REDIS_PORT=6379 REDIS_PASSWORD=change-me # first-run admin (see step 4) BOOTSTRAP_ADMIN_EMAIL=admin@odly.ai BOOTSTRAP_ADMIN_PASSWORD=change-me-on-first-login
Full, commented reference: .env.client.example.
3 · Self-hosted mode & knobs
Setting DEPLOYMENT_MODE=self_hosted flips Odly out of its SaaS defaults so it behaves like software you own.
With DEPLOYMENT_MODE=self_hosted set, new workspaces default to the unlimited admin plan and land active with no trial — there is no 14-day expiry — and the billing / plan-catalog UI is hidden. Managed AI works using your own platform key, with no per-org entitlement to manage. BILLING_PROVIDER defaults to none, so there is no paywall.
Optional knobs
- BEDROCK_ALLOW_INSTANCE_PROFILE — use EC2 instance-profile credentials for Amazon Bedrock instead of static keys.
- INTEGRATIONS_ALLOW_PRIVATE_ENDPOINT / STORAGE_ALLOW_PRIVATE_ENDPOINT — relax the SSRF guard so Odly can reach private / loopback services on your own network. Self-host only.
4 · First-run bootstrap
Once the containers are up and the environment validates, seed the instance a single time.
- Run npm run init:prod (a.k.a. seed:prod:bootstrap). It creates the plan catalog, one admin organization, one admin user, and the default departments.
- It uses BOOTSTRAP_ADMIN_EMAIL (default admin@odly.ai) and BOOTSTRAP_ADMIN_PASSWORD (default password123) for that first admin.
- Sign in and change the admin password immediately. The command is idempotent, so it's safe to re-run.
5 · Storage
Attachments and uploads land on local disk unless you point Odly at object storage.
By default files are written to local disk at /app/uploads. For S3-compatible object storage, set STORAGE_DRIVER=s3 together with the S3_* variables.
6 · Updates, migrations & rollback
Database migrations are automatic and forward-only; the update script health-gates every new release and can roll the image back on its own.
- Migrations run on container start — automatically and forward-only. For schema changes use db:migrate; never db:push.
- Auto-update — the selfhosted-autoupdate.sh script (suggested as a cron job every ~15 min) checks GHCR for a newer image, pulls it, redeploys, and health-gates the new version for up to 180s. If the new version doesn't come up healthy, it auto-rolls-back the image to the previous one.
- Health & version — GET /api/health/version reports the running version and the deployment mode.