Self-Hosting

Your infrastructure, your reviews.

Run Crit Web on your own server with Docker. Share reviews within your team without sending data to a third party.

tomasz-tomczyk/crit-web

Source code, Dockerfile, example configs, and issue tracker.

View on GitHub →

What you'll need

Docker with the Compose plugin and a Postgres 17 database. Compose ships with Docker Desktop and most server installs, and the example config bundles its own Postgres if you don't already have one.

Docker

With Compose plugin (included in Docker Desktop and most server installs).

PostgreSQL 17

Bundled in the docker-compose file, or bring your own.

Download and configure

Grab the example Docker Compose file and a starter .env, then generate a secret key for signing cookies.

Terminal
# Download config files
$ curl -o docker-compose.yml \
https://raw.githubusercontent.com/tomasz-tomczyk/crit-web/main/contrib/docker-compose.example.yml
$ curl -o .env \
https://raw.githubusercontent.com/tomasz-tomczyk/crit-web/main/.env.example
# Generate a secret key
$ openssl rand -base64 64

Set SECRET_KEY_BASE and PHX_HOST in your .env file.

Start the services

One command brings the stack up. Database migrations run automatically on boot, so you don't have to wire anything else in.

Terminal
$ docker compose up -d
Container crit-db Started
Container crit-web Started
$ # Ready at https://crit.example.com

Environment variables

Every knob lives in .env — grouped below by what they control.

Database

Set DATABASE_URL or all four of DB_HOST, DB_USER, DB_PASSWORD, DB_NAME.

DATABASE_URL

PostgreSQL connection URL, e.g. ecto://user:pass@host/db. Use this or the individual DB_* vars.

required*
DB_HOST

Database host — alternative to DATABASE_URL

required*
DB_USER

Database user

required*
DB_PASSWORD

Database password

required*
DB_NAME

Database name

required*
DB_PORT 5432

Database port

optional
DB_SSL false

Set to true to enable SSL. Without DB_SSL_CA_CERT, connects encrypted without certificate verification (recommended for AWS RDS)

optional
DB_SSL_CA_CERT

Path to a CA certificate file. When set alongside DB_SSL=true, enables full verify_peer verification. In Docker, mount the file as a volume and pass the container path.

optional
POOL_SIZE 10

Database connection pool size

optional
Application
SECRET_KEY_BASE

64+ byte secret for signing cookies. Generate with openssl rand -base64 64

required
PHX_HOST

Hostname where crit-web is served, e.g. crit.example.com

required
PHX_SERVER

Set to true to start the web server

required
SELFHOSTED

Set to true to enable self-hosted mode (dashboard, no marketing pages)

required

Authentication

Pick one of three ways for your team to sign in:

  • GitHub OAuth — set GITHUB_CLIENT_ID and GITHUB_CLIENT_SECRET.
  • Bring your own OAuth / OIDC — set OAUTH_CLIENT_ID, OAUTH_CLIENT_SECRET, and OAUTH_BASE_URL for Google, GitLab, Keycloak, Okta, or any other OIDC provider.
  • Basic accounts — email + password managed by crit-web. No OAuth setup. Set LOCAL_REGISTRATION_ENABLED=true, then sign up at /users/register and flip it back to false once your team is in.

The two OAuth options are mutually exclusive. Basic accounts are off by default and can run on their own or alongside either OAuth provider — useful for service accounts or users without an SSO identity.

Authentication
GITHUB_CLIENT_ID

GitHub OAuth App client ID. Set alongside GITHUB_CLIENT_SECRET to enable GitHub login.

optional
GITHUB_CLIENT_SECRET

GitHub OAuth App client secret

optional
OAUTH_CLIENT_ID

Generic OIDC/OAuth2 client ID. Use with OAUTH_CLIENT_SECRET and OAUTH_BASE_URL. Mutually exclusive with GITHUB_CLIENT_ID.

optional
OAUTH_CLIENT_SECRET

Generic OAuth2 client secret

optional
OAUTH_BASE_URL

OIDC discovery base URL, e.g. https://accounts.google.com

optional
LOCAL_REGISTRATION_ENABLED false

Set to true to open /users/register for email + password sign-ups. Flip back to false once your team is in.

optional
ADMIN_EMAILS

Comma-separated list of admin email addresses. Users with these emails are auto-promoted to admin on every login and on app boot, gaining access to /admin/users and /admin/settings plus moderation powers (delete any review or comment). Removing an email and restarting demotes that user back to a regular user.

optional
Network
PORT 4000

HTTP port the server listens on

optional
PHX_SCHEME https

URL scheme used in generated links

optional
FORCE_SSL false

Set to true to force HTTPS redirects. Not needed when running behind a reverse proxy.

optional
Error monitoring

All Sentry vars are optional. Leave them unset and no error data is sent anywhere — the browser SDK isn't even loaded.

SENTRY_DSN

Backend Sentry DSN. Captures Elixir/Phoenix exceptions. Request bodies, cookies, and comment text are scrubbed before events leave the server.

optional
SENTRY_FRONTEND_DSN

Browser SDK DSN. The @sentry/browser chunk is only fetched when this is set, so unset means zero third-party requests from your users' browsers.

optional
SENTRY_ENV prod

Environment tag attached to events (e.g. staging, prod).

optional
SENTRY_RELEASE app version

Release tag for grouping events (e.g. a commit SHA or version string). Falls back to the app version from mix.exs.

optional

Using your own Postgres

Already have a managed Postgres (RDS, Supabase, Neon, anything)? Skip the bundled database and run crit-web as a single container pointed at your existing instance.

Terminal
$ docker run -d \
--name crit-web \
--restart unless-stopped \
-e DATABASE_URL="postgres://user:pass@your-host:5432/crit" \
-e SECRET_KEY_BASE="your-generated-secret" \
-e PHX_HOST="crit.example.com" \
-e SELFHOSTED=true \
-e PHX_SERVER=true \
-e PORT=4000 \
-p 4000:4000 \
ghcr.io/tomasz-tomczyk/crit-web:latest \
sh -c "/app/bin/migrate && /app/bin/server"

Staying up to date

Updates are a pull and a restart. Migrations run on boot, so you don't have to coordinate anything.

Terminal
$ docker compose pull
$ docker compose up -d
Updated to latest

Available image tags.

Images are published to the GitHub Container Registry. Pick a tag based on how aggressive you want updates to be.

latest

Latest stable release. Recommended for production.

main

Bleeding edge. Built from the main branch on every push.

1.2.3

Pin a specific version. Use for reproducible deployments.

Behind an SSO reverse proxy?

If your Crit instance sits behind an SSO reverse proxy (Cloudflare Access, Tailscale Funnel, oauth2-proxy, etc.), the local CLI can't authenticate to /api/reviews from the terminal — the proxy redirects to a login page. Set proxy_auth: true in your ~/.config/crit/crit.json to route Share, Pull, Re-share, and Unpublish through a browser popup that carries your existing proxy session cookie.

~/.config/crit/crit.json
{
"share_url": "https://crit-web.your-company.example",
"proxy_auth": true
}

proxy_auth is a CLI-side setting, not a server env var. The terminal commands crit share, crit fetch, and crit unpublish remain unavailable behind SSO — use the in-browser buttons. For terminal or CI access, ask the proxy admin about Authorization: Bearer passthrough on /api/reviews*, which works with the existing crit auth device flow.

If your proxy strips or rewrites WebSocket upgrades, set CRIT_LIVEVIEW_TRANSPORT=longpoll on the server to skip the WebSocket attempt and connect via long-polling directly. Default is websocket with automatic fallback after 2.5s.

Trusted proxy header

If your proxy already authenticates users (oauth2-proxy, Cloudflare Access, Google IAP, Pomerium, Authelia), you can have Crit pick up the signed-in user's email from a request header instead of running its own OAuth.

Reverse Proxy
CRIT_TRUSTED_PROXY_USER_HEADER

Request header containing the authenticated user's email.

required
CRIT_TRUSTED_PROXY_CIDRS

Comma-separated CIDR ranges your proxy forwards from, e.g. 10.0.0.0/8,172.16.0.0/12. Mandatory — Crit refuses to start if the header is set without CIDRs.

required
CRIT_LIVEVIEW_TRANSPORT websocket

Set to longpoll if your proxy strips or rewrites WebSocket upgrades.

optional

Common header names

X-Auth-Request-Email (oauth2-proxy), Cf-Access-Authenticated-User-Email (Cloudflare Access), X-Goog-Authenticated-User-Email (Google IAP), X-Pomerium-Claim-Email (Pomerium), Remote-Email (Authelia).

Your proxy must overwrite the header on every request, not append to incoming values. This authenticates browser-driven flows only (review pages, dashboard, the in-browser Share popup). The CLI's direct /api/reviews uploads still require a bearer token from crit auth — use proxy_auth: true on the CLI side if the proxy blocks direct API access.

Known proxy behaviors

Two common proxy quirks are handled automatically:

  • Double-encoding — some proxies re-compress responses that are already gzipped, producing garbled output. Crit sets Cache-Control: no-transform on every response, which tells conformant proxies to leave the body alone.
  • Redirect-collapse loops — when no auth backend is configured, crit-web renders inline auth gates instead of HTTP redirects. This avoids the situation where a proxy silently follows the redirect, the browser URL never changes, and the page enters a reload loop that trips rate limiting.

Keep going.

Now that the server is up, point the CLI at it or wire up your editor.