Self-Hosting Revel¶
Revel is designed to run as a managed SaaS, but the entire stack is open and self-hostable. You can run a complete instance — frontend, API, background workers, database, and (optionally) the full observability suite — on a single box with Docker Compose. This section walks you through standing one up, sizing it, and keeping it healthy.
Self-hosting is a good fit if you want full data ownership, you run a single community or organization, or you simply want to learn how the platform fits together. It is not a good fit if you need the operational guarantees, backups, and uptime of a managed service — for that, use the hosted instance.
Two tiers¶
Revel scales down a long way. There are two reference sizings:
- Slim — roughly 2 vCPU / 4 GB RAM, about €20/month (Hetzner CPX22, September 2026). Runs the core services only, with conservative resource limits. ClamAV, Telegram, and the observability stack are switched off. This is the recommended starting point for a single-org instance.
- Full — 8 vCPU / 32 GB RAM. Can run every optional profile: antivirus scanning, the LGTM observability stack (Grafana/Loki/Tempo/etc.), the Telegram bot, and the canary. This mirrors a production deployment.
The difference between the two is almost entirely a matter of which Compose profiles you enable and a handful of resource-limit environment variables — see Tiers & Configuration.
Architecture recap¶
A minimal (Slim) instance runs these core services:
- caddy — TLS termination, reverse proxy, and HMAC-protected media serving.
- frontend — the SvelteKit web app (SSR + CSR).
- web — the Django/Ninja API (served by Gunicorn).
- celery — background task worker.
- beat — the Celery scheduler for periodic jobs.
- postgres — PostgreSQL with PostGIS.
- pgbouncer — connection pooler in front of Postgres.
- redis — Celery broker, result backend, and cache.
Everything else (ClamAV, the LGTM observability stack, the Telegram bot) is optional and gated behind Compose profiles and feature flags.
Optional dependencies¶
Revel degrades gracefully when external integrations are absent. The table below summarizes what each dependency does, whether you can omit it, and the lever you use to do so:
| Dependency | Safe to omit? | Behaviour when absent | Lever |
|---|---|---|---|
| ClamAV | Yes | Uploads marked clean, no scan | FEATURE_MALWARE_SCAN=False |
| Stripe | Yes (unless selling) | Only paid checkout hits it | leave keys as placeholders |
| LLM / OpenAI | Yes | Manual questionnaire eval still works | FEATURE_LLM_EVALUATION=False |
| Geo / IP2Location | Yes | Lookups return None | BIN optional; mini cities fallback |
| Telegram | Yes | Per-user delivery skipped | FEATURE_TELEGRAM=False; profile off |
| OIDC login (Google, Keycloak, …) | Yes (opt-in) | Login buttons hidden; password auth only | OIDC_PROVIDERS=google + per-provider vars |
| OAuth / OIDC provider (third-party apps, MCP hosts, "Sign in with") | Yes (opt-in) | Every /o/*, /.well-known/* and /api/oauth/* route is 404 | OIDC_SIGNING_KEY_PATH + OAUTH_ISSUER |
| Eventbrite listing sync | Yes (opt-in) | Integrations panel hides the provider | INTEGRATIONS_EVENTBRITE_CLIENT_ID + _CLIENT_SECRET |
| Custom domains (white-label) | Yes (opt-in) | Organizations can still set brand colors; no domain can be requested or activated | custom_domain_enabled per org; CLOUDFLARE_* optional (manual activation without) |
| Apple Wallet | Yes | /wallet/apple → 503; PDF tickets fine | leave certs unset |
| Google Wallet | Yes | /wallet/google → 503; PDF tickets fine | leave issuer ID / key unset |
| Email / SMTP | Required* | Needed for verification | real SMTP or EMAIL_DRY_RUN=True |
Email is effectively required
Email is the one "soft requirement": account verification, password resets, and ticket delivery all flow through it. For a real instance, configure a working SMTP provider. For a throwaway test instance you can set EMAIL_DRY_RUN=True, which logs messages instead of sending them.
Google Wallet setup (one-time)¶
- Create an issuer account in the Google Pay & Wallet Console (requires business verification — can take days) and accept the Google Wallet API ToS to get your Issuer ID.
- Create a GCP service account, enable the Google Wallet API in its project, and add the service account's email under Users in the Wallet Console.
- Provision the service-account JSON key to the backend host and set:
GOOGLE_WALLET_ISSUER_ID,GOOGLE_WALLET_SERVICE_ACCOUNT_KEY_PATH(and optionallyGOOGLE_WALLET_CLASS_PREFIX, defaultrevel). - Passes show [TEST ONLY] until you request publishing access in the Wallet Console.
If your GCP org enforces iam.disableServiceAccountKeyCreation, creating the key requires a temporary project-scoped org-policy override (allow ~2 minutes of propagation before retrying).
OAuth / OpenID Connect provider setup (one-time)¶
Revel can act as an OAuth 2.1 authorization server and OpenID Provider, so third-party apps, scripts and MCP hosts can call the API on a user's behalf with scoped, revocable tokens. It is off by default and switched on by credential presence (ADR-0018): the provider exists only when a readable signing key and an issuer are configured. The infra setup wizard can do steps 1–2 for you.
-
Generate the signing key (RSA 2048, unencrypted PKCS8 PEM) in the infra directory:
openssl genpkey -algorithm RSA -pkeyopt rsa_keygen_bits:2048 -out certs/oidc.pem chmod 644 certs/oidc.pemInside a running stack,
docker compose exec web python manage.py generate_oidc_signing_key --out /app/certs/oidc.pemproduces the same format. Thechmod 644is not optional: the command writes the file0600for your host user, but the containers run as uid 997. An unreadable key leaves the provider off while the rest of the API keeps serving, with anoauth.W002warning in the logs (see Troubleshooting). -
Set in
.env(./certsis mounted read-only at/app/certsin every service that runs management commands:web,celery_defaultandtelegram):OIDC_SIGNING_KEY_PATH=/app/certs/oidc.pem OAUTH_ISSUER=https://api.example.org # your public API origin, no trailing slashOAUTH_ISSUERmust be exactly the origin that serves/.well-known/openid-configuration; clients reject tokens whoseissdoes not match it. Set both together: a readable key with an empty issuer is the one misconfiguration that stopswebfrom starting (oauth.E001), because it would publish broken discovery. -
Restart and verify:
-
Back up
certs/oidc.pemoff the box: it is not in the database dump (see Maintenance).
No Caddy change is needed: the API host already proxies /o/* and /.well-known/* to Django. The consent page and the Developer apps / Connected apps screens ship with the frontend, so deploy a frontend that includes them. Turning the provider off again is config-only: unset OIDC_SIGNING_KEY_PATH and restart; every provider route returns to 404 and no data is lost.
Optional tuning (defaults shown): OAUTH_MAX_APPS_PER_USER=10 (developer-portal apps per user), OAUTH_DCR_DAILY_CAP=500 (instance-wide dynamic client registrations per day), OAUTH_DCR_UNUSED_TTL_HOURS=24 (how long a dynamically registered app that never completed an authorization is kept). The token endpoints are rate-limited to 60 requests/min per IP and dynamic registration to 10/hour per IP; a busy "Sign in with" integration or a hosted MCP host calls from a single server IP, so watch for 429 slow_down as usage grows. For app developers, see the OAuth developer guide.
Where to next¶
-
Quickstart
Clone the infra repo, run the setup wizard, and be live in minutes.
-
DNS & Cloudflare
The records you need, and the Cloudflare cert-issuance caveat.
-
Tiers & Configuration
Slim vs full, Compose profiles, and every environment knob.
-
Observability
Enable the LGTM stack when you want dashboards and traces.
-
Maintenance
Backups, upgrades, geo refresh, and dataset attribution.
-
Troubleshooting
The known failure modes and their fixes.