Skip to content
letsrevelPublic

About

Self-host Revel, the open-source event management, ticketing and membership platform: a Docker Compose stack and a one-command setup wizard.

Topics

Resources

Stars

12 stars

Watchers

1 watching

Forks

Repository files navigation

Revel infrastructure

Revel is an open-source event management, ticketing and membership platform for communities, clubs, independent venues and independent artists.

License Discord

This repository is the Docker Compose deployment for Revel: the stack, three Caddyfile variants, the setup.sh wizard for self-hosters and the scripts we use to run letsrevel.io. For what Revel does and who it is for, read the main README. The full self-hosting guide is at docs.letsrevel.io/self-hosting.

Install

You need:

  • A Linux x86-64 server (the images are built for linux/amd64 only).
  • A root shell, or a user in the docker group.
  • git, openssl and curl.
  • DNS A records for two hostnames: one for the web app and one for the API, which defaults to api.<your domain>. Add a third for Grafana if you enable observability.
  • Ports 80 and 443 open.

Then:

git clone https://github.com/letsrevel/infra && cd infra && ./setup.sh

The wizard is interactive and has no unattended mode. It:

  1. Offers to install Docker (via get.docker.com) if it is missing, and checks that ports 80 and 443 are free.
  2. Suggests a tier from the detected CPU and RAM.
  3. Asks for the frontend and API domains, SMTP (or dry-run email; with real SMTP it also offers an optional dedicated sending domain for organization mail and an optional bounce-webhook secret, both left out of .env when blank and kept on a re-run) and which optional services to enable: observability (plus its Grafana domain), ClamAV, the Telegram bot, LLM questionnaire evaluation, Stripe, Google login, the login canary and the OAuth / OpenID Connect provider. It also asks whether you are behind Cloudflare.
  4. Asks whether this is a single-organization instance (the default), which turns off public organization creation.
  5. Backs up any existing .env, writes a new one with generated secrets (plus, if the OAuth provider is on, a signing key in certs/oidc.pem that is created once and never replaced) and picks the matching Caddyfile.
  6. Downloads the city list (and, optionally, the 182 MB IP2Location LITE database), hands media/, geo-data/ and sentinel/ to the containers' user (see Data directory ownership), pulls the images from ghcr.io/letsrevel and runs docker compose up -d.
  7. Once the API is healthy, registers Stripe webhooks (if Stripe is enabled and jq is installed) and creates the admin user and first organization.

Caddy obtains Let's Encrypt certificates on first start. Behind Cloudflare, keep the DNS records DNS-only (gray cloud) until the certificates are issued; the wizard pauses for this.

Tiers

A tier is a preset of defaults for the questions above and for resource limits. Every optional service can still be switched on or off on its own.

Tier Hardware Runs
Slim 2 vCPU, 4 GB RAM Web app, API, Celery worker and beat, PostgreSQL/PostGIS, PgBouncer, Redis, Caddy
Full 8 vCPU, 32 GB RAM Slim plus any of: Grafana/Loki/Tempo/Prometheus observability, ClamAV, the Telegram bot, a login canary. The wizard defaults observability and ClamAV to on; Telegram and the canary stay opt-in

The slim tier costs about €20/month (Hetzner CPX22, September 2026). letsrevel.io runs the full tier on a Hetzner CCX33 (8 dedicated vCPU, 32 GB RAM, 240 GB disk). The wizard suggests full only on hosts with at least 8 vCPU and 24 GB RAM, and warns below 2 vCPU and 3.5 GB.

Slim Full
Gunicorn workers x threads 2 x 2 6 x 4
Celery concurrency 2 4
Postgres shared_buffers / max_connections 256MB / 50 4GB / 100
Web memory limit 1500m 12g

Services, profiles and feature flags

Always running: caddy, web (Gunicorn, gthread workers), celery_default, beat, frontend, revel_postgres (PostGIS 17), pgbouncer and redis.

Optional services are Compose profiles, listed in COMPOSE_PROFILES in .env. Most profiles have a matching feature flag that the backend reads, and the wizard sets both together. GET /api/version reports some of the flags (organization creation, Telegram, LLM evaluation) so the web app can hide features that are off.

Profile Services Flag
observability grafana, prometheus, alertmanager, loki, tempo, pyroscope, alloy, postgres-exporter, redis-exporter, node_exporter, blackbox-exporter FEATURE_OBSERVABILITY
antivirus clamav FEATURE_MALWARE_SCAN
telegram telegram FEATURE_TELEGRAM
canary canary (synthetic login check) none

Flags with no service behind them: FEATURE_LLM_EVALUATION (with LLM_*), FEATURE_ORGANIZATION_CREATION (off for single-organization instances), Stripe keys, OIDC_PROVIDERS for user login through Google or any OpenID Connect provider, GOOGLE_SSO_* for the Django admin login, APPLE_WALLET_*, GOOGLE_WALLET_*, INTEGRATIONS_EVENTBRITE_* and OIDC_SIGNING_KEY_PATH with OAUTH_ISSUER for the OAuth provider. All are optional. SMTP is optional too, but without it nobody receives verification or ticket emails. See .env.example for every variable; its values document our own production deployment.

The wizard asks for one Google OAuth client and uses it for two separate toggles. User-facing login writes OIDC_PROVIDERS=google with OIDC_GOOGLE_ISSUER, OIDC_GOOGLE_CLIENT_ID and OIDC_GOOGLE_CLIENT_SECRET; add https://<API_DOMAIN>/api/auth/oidc/google/callback as a redirect URI on that client. Admin login writes GOOGLE_SSO_*. To add another OpenID Connect provider, see tiers and configuration.

Email

Any SMTP provider works; letsrevel.io uses the Brevo SMTP relay (smtp-relay.brevo.com, port 587, TLS). A few optional variables sit next to the SMTP settings:

  • ORG_EMAIL_DOMAIN: a dedicated sending domain (for example mail.<your domain>) for mail that organizations send to people: announcements, invitations, event updates and reminders. This keeps its reputation apart from account mail such as verification, password reset and tickets. Unset, that mail uses the domain of DEFAULT_FROM_EMAIL. Authenticate the domain (DKIM and DMARC) at your SMTP provider before you set it. It needs no MX record, but do not publish a null MX: RFC 7505 says a null-MX domain should not be used as a From domain.
  • EMAIL_WEBHOOK_SECRET: turns on the bounce and complaint webhook at POST https://<API_DOMAIN>/api/email-events/brevo. The endpoint accepts Authorization: Bearer <secret> or basic auth with the secret as password, so in Brevo's webhook settings you can use https://revel:<secret>@<API_DOMAIN>/api/email-events/brevo. Unset, the endpoint returns 404.
  • ORG_NUDGE_REPLY_TO and ORG_NUDGE_SIGNATURE: Reply-To and check-in signature for the org setup nudges, which are short reminders to owners of stalled organizations (private profile, forgotten draft, no events, gone quiet). Without a Reply-To the emails still go out, but the personal "what's in the way?" check-in never does. The daily task ships disabled: preview with docker compose exec web python manage.py org_nudges, then enable "Send org setup nudges" under Periodic tasks in the Django admin.

Caddyfiles

Domains come from .env (FRONTEND_DOMAIN, API_DOMAIN and GRAFANA_DOMAIN, plus DOCS_DOMAIN in the production Caddyfile only). Pick a variant with CADDYFILE_PATH; the wizard sets it.

  • Caddyfile: Cloudflare-aware plus legacy redirects. The production default; its fallbacks resolve to the letsrevel.io domains.
  • Caddyfile.cloudflare: self-hosting behind Cloudflare's proxy.
  • Caddyfile.generic: self-hosting with Caddy as the edge.

Never buffer frontend responses. The frontend uses SvelteKit streaming SSR. A flush_interval -1 on the frontend reverse_proxy makes Caddy hold the whole response, and pages hang on a loading state until SSR finishes. None of the shipped Caddyfiles set it. To check a deployment, compare time to first byte with total time; they should differ:

curl -w "\nTTFB: %{time_starttransfer}s\nTotal: %{time_total}s\n" -o /dev/null "https://<your-frontend-domain>/events"

Organization custom domains

All three Caddyfiles can serve organizations on their own hostnames (#67; the backend side is the white-label operator guide). The pieces:

  • CUSTOM_DOMAIN_CNAME_TARGET (for example customers.letsrevel.io) is the name organizers CNAME to. Caddy serves it (a redirect to the frontend) so it has its own certificate; behind Cloudflare it is also the Cloudflare for SaaS fallback origin, set up grey first, orange once Caddy has its certificate. Unset, the block resolves to an inert .localhost name.
  • An https:// catch-all at the end of each file serves the frontend for any other hostname, with on-demand TLS: Caddy gets a Let's Encrypt certificate for a customer hostname only after the backend's ask endpoint (GET /api/tls/ask?domain=<host>, called straight over the docker network, unthrottled) answers 200 (an ACTIVE domain of an organization with custom_domain_enabled). The backend must be deployed with that endpoint before Caddy is reloaded, or every ask is refused. Behind Cloudflare this is what satisfies Full (strict): Cloudflare connects with the customer hostname as SNI and Host. Only HTTP-01 is used there, since TLS-ALPN cannot cross Cloudflare's proxy.
  • Every frontend proxy overwrites X-Forwarded-Host and X-Forwarded-Proto, and the frontend runs with HOST_HEADER / PROTOCOL_HEADER and PLATFORM_HOSTS instead of ORIGIN. Keep ORIGIN out of .env.
  • The catch-all sends HSTS without includeSubDomains or preload: those domains are not ours.
  • Protected media echoes Access-Control-Allow-Origin from Django's CORS decision, so it works on the platform and on active custom domains alike. CORS_ALLOWED_ORIGINS must list the frontend origin (the wizard sets it).
  • Changing CUSTOM_DOMAIN_CNAME_TARGET needs docker compose up -d caddy (a recreate): Caddy reads .env at container start, not on caddy reload.

The Cloudflare token and zone id, the URL template and the Turnstile hostname list are described in .env.example.

Data directory ownership

The backend containers (web, celery_default, beat, telegram) run as a non-root user, appuser (uid/gid 997). They write to bind-mounted directories from the checkout: media/ (uploads, logos, generated PDFs and wallet passes; all four), plus geo-data/ (the periodic IP2Location refresh) and sentinel/ (the LLM sentinel model) for web and celery_default only. A fresh clone belongs to whoever cloned it, so without a chown those writes fail with PermissionError: [Errno 13]. setup.sh reads the uid/gid from the image and runs the chown for you (with sudo when not root). On an install that did not go through the wizard:

docker compose run --rm --no-deps --entrypoint id web     # expect uid=997(appuser) gid=997(appuser)
sudo chown -R 997:997 media geo-data sentinel

Apple Wallet certificates

Apple Wallet passes need a Pass Type ID certificate, its private key and Apple's WWDR certificate. Put them in certs/, which is mounted into the containers at /app/certs, and point APPLE_WALLET_CERT_PATH, APPLE_WALLET_KEY_PATH and APPLE_WALLET_WWDR_CERT_PATH at them (for example /app/certs/pass-certificate.pem). Also set APPLE_WALLET_PASS_TYPE_ID, APPLE_WALLET_TEAM_ID and, if the key has one, APPLE_WALLET_KEY_PASSWORD.

The containers run as a non-root user whose UID differs from the host's, so every file, including the key, must be mode 644. With 600, pass generation fails with PermissionError: [Errno 13] Permission denied: '/app/certs/pass-certificate.pem'.

chmod 644 certs/*.pem

Google Wallet setup is in the self-hosting docs.

OAuth / OpenID Connect provider

Revel can act as an OAuth 2.1 / OpenID Connect provider, so third-party apps and MCP hosts can act on a user's behalf and other sites can offer "Sign in with" your instance. It is off by default and turns on only when both OIDC_SIGNING_KEY_PATH points at a readable RSA private key and OAUTH_ISSUER is set; otherwise every /o/* and discovery route returns 404. The wizard asks about it (default: no) and, if you say yes, generates the key, sets both variables and applies the permissions. By hand:

openssl genpkey -algorithm RSA -pkeyopt rsa_keygen_bits:2048 -out certs/oidc.pem
chmod 644 certs/oidc.pem
OIDC_SIGNING_KEY_PATH=/app/certs/oidc.pem
OAUTH_ISSUER=https://<API_DOMAIN>

OAUTH_ISSUER is exactly the public API origin, with no trailing slash. The key needs mode 644 for the same reason as the wallet certificates. If the containers cannot read it, the provider stays off while the rest of the API keeps serving, and every management command warns oauth.W002 (visible in docker compose logs web). Set OAUTH_ISSUER together with the key: a readable key without an issuer fails the backend's oauth.E001 check, and web (which migrates on start) does not start. No Caddy changes are needed: the API site block already proxies /o/* and /.well-known/* to Django. To check it works, https://<API_DOMAIN>/api/version should report features.oauth_provider: true, and the issuer in https://<API_DOMAIN>/.well-known/openid-configuration should equal OAUTH_ISSUER.

Never regenerate the key in place. A new key invalidates every ID token already issued; access and refresh tokens live in the database and are not affected. The wizard reuses an existing certs/oidc.pem on every re-run. To rotate, generate a new key, point OIDC_SIGNING_KEY_PATH at it and list the old path in OIDC_SIGNING_KEYS_INACTIVE_PATHS (comma-separated). Tuning variables are in .env.example.

Operations

./deploy.sh update      # pull the latest images and redeploy
./deploy.sh backup      # pg_dump to backup_<timestamp>.sql
./deploy.sh logs web    # also: up, down, restart, ps, pull
docker compose up -d --scale celery_default=4

deploy-rollout.sh replaces containers with docker-rollout instead of stopping them first, safe-reboot.sh drains Celery tasks before rebooting the host and ALERTING_SETUP.md covers Pushover alerts and Grafana dashboards. Persistent data lives in named volumes (revel_postgres_data, redis_data, caddy_data, caddy_config and clamav_data, plus one per observability service). docker compose down -v deletes all of it.

./deploy.sh backup covers the database only. Back up .env and certs/ (wallet certificates and the OAuth signing key certs/oidc.pem) separately. Both are gitignored, so they are not in your clone's history either.

Before exposing an instance: change every default password in .env, keep .env out of version control and keep the images updated.

License

MIT. See LICENSE.

Related repositories

About

Self-host Revel, the open-source event management, ticketing and membership platform: a Docker Compose stack and a one-command setup wizard.

Topics

Resources

Stars

12 stars

Watchers

1 watching

Forks

Releases

Packages

Contributors

Languages