Self-hostingConfiguration

Configuration

One `.env` for ports, public URL, CORS, and auth: required vs optional, and what to do when a var is missing.

All operator knobs live in .env. Copy .env.example, edit, recreate epure. Compose interpolates that file; the binary still binds 8080 inside the container.

cp .env.example .env
ChangeVars
Dashboard / ingest host portEPURE_PORT and EPURE_PUBLIC_URL
Pin the app imageEPURE_IMAGE
Postgres on the host (psql, cargo test)POSTGRES_HOST_PORT and the host DATABASE_URL* lines
CORS / cookie / OAuthEPURE_CORS_ORIGINS, EPURE_SESSION_SECURE, GOOGLE_*

Compose injects the three database URLs for the epure container (postgres:5432). After migrate, boot sets epure / epure_ingest / epure_app passwords from those URLs. A missing migrate URL exits on startup.

Production .env

Laptop .env.example is not a production template. On a VPS:

cp .env.production.example .env

Fill every CHANGE_ME (openssl rand -hex 24). Example passwords and leftover CHANGE_ME values are refused unless EPURE_PUBLIC_URL is localhost. Full walkthrough: Installation.

Production .env (fill CHANGE_ME)

command

EPURE_PORT=8080
EPURE_PUBLIC_URL=https://errors.example.com
EPURE_CORS_ORIGINS=https://app.example.com
POSTGRES_PASSWORD=CHANGE_ME
EPURE_INGEST_PASSWORD=CHANGE_ME
EPURE_APP_PASSWORD=CHANGE_ME

What ports does compose publish?

Defaults: app HTTP 8080, Postgres on the host 5433. Override with EPURE_PORT and POSTGRES_HOST_PORT in .env. The prod overlay keeps the app port and publishes no Postgres port.

BindingDev composeProd overlay
App HTTP${EPURE_PORT:-8080}:8080same (upstream for TLS proxy)
Postgres${POSTGRES_HOST_PORT:-5433}:5432 on hostno host publish

Inside Docker, the app reaches Postgres at postgres:5432. Host-side tools (cargo test, psql) use 5433 unless you changed POSTGRES_HOST_PORT.

Env by job

Copy the block that matches your failure. Each tab lists what breaks when a variable is missing.

Required. Compose sets these for the epure container. Host-side binaries use localhost:5433.

Copy database URLs (inside Compose)

Missing any of the three: migrate or ingest or dashboard queries fail on start or first request.

command

DATABASE_URL=postgres://epure:epure@postgres:5432/epure
EPURE_INGEST_DATABASE_URL=postgres://epure_ingest:epure_ingest@postgres:5432/epure
EPURE_APP_DATABASE_URL=postgres://epure_app:epure_app@postgres:5432/epure

Copy database URLs (host tools)

command

DATABASE_URL=postgres://epure:epure@localhost:5433/epure
EPURE_INGEST_DATABASE_URL=postgres://epure_ingest:epure_ingest@localhost:5433/epure
EPURE_APP_DATABASE_URL=postgres://epure_app:epure_app@localhost:5433/epure
If missingWhat happensWhat to do
DATABASE_URLProcess exits on migrateMatch compose credentials; wait for postgres healthy
EPURE_INGEST_DATABASE_URLIngest writes fail (500 storage_error)Copy ingest URL from this page
EPURE_APP_DATABASE_URLDashboard /api/v1 500sCopy app URL; RLS role is epure_app

Full variable table

Compose defaults are for local and dev. Production changes session security, public URL, CORS origins, and Postgres passwords.

VariableRequiredDefault / composeDescription
DATABASE_URLYes (compose)postgres://epure:epure@postgres:5432/epureMigrations / bootstrap role
EPURE_INGEST_DATABASE_URLYes (compose)postgres://epure_ingest:epure_ingest@postgres:5432/epureIngest writes (DSN path)
EPURE_APP_DATABASE_URLYes (compose)set from EPURE_APP_PASSWORD in prodDashboard API with RLS
POSTGRES_PASSWORDProd overlaySuperuser. First empty volume only
EPURE_INGEST_PASSWORDProd overlayApplied to role epure_ingest on every boot
EPURE_APP_PASSWORDProd overlayApplied to role epure_app on every boot
EPURE_IMAGENoghcr.io/epure-sh/epure:latestApp image Compose pulls. Pin a release tag in production
EPURE_PORTNo8080Host port Compose publishes. Not read by the binary
POSTGRES_HOST_PORTNo5433Host port for Postgres. Unused in the prod overlay
EPURE_BINDNo0.0.0.0:8080Listen address inside the container. Leave this; change EPURE_PORT
EPURE_MODENoallOnly all
EPURE_CORS_ORIGINSNo* (.env.example)Comma-separated origins for ingest CORS
EPURE_ARTIFACTS_DIRNo/data/artifactsJS/TS source map storage
EPURE_SESSION_SECURENo0 dev / 1 prod overlaySession cookie Secure
EPURE_PUBLIC_URLNohttp://localhost:8080Public origin for OAuth and absolute links
EPURE_DEV_SEEDNooff1 applies scripts/seed-dev.sql on startup (dev only)
EPURE_WEBHOOK_ALLOW_PRIVATENooff1 allows private/loopback webhook targets (dev/tests only)
GOOGLE_CLIENT_IDNoemptyEnables Google sign-in when set
GOOGLE_CLIENT_SECRETNoemptyGoogle OAuth client secret
GOOGLE_REDIRECT_URINo{EPURE_PUBLIC_URL}/api/v1/auth/google/callbackMust match the Google console
RUST_LOGNotypically infoLog filter
EPURE_VELOCITY_WINDOW_SECSNointernal defaultVelocity alert window

How should I handle secrets?

Treat Postgres passwords, the Google client secret, DSN secrets, and the session cookie as credentials.

SecretHandling
Postgres passwordsLaptop defaults are example strings, refused when EPURE_PUBLIC_URL is not localhost. Production: .env.production.example
Google client secretEnv only; never commit
DSN secretsShown once in UI; treat like API keys
Session cookieHttpOnly; __Host- prefix when secure

Never ship EPURE_DEV_SEED=1 or ./scripts/seed-dev.sh credentials to production.

Postgres roles and RLS

One database (epure), three roles.

Role (compose user)Used for
epure via DATABASE_URLMigrations on startup
epure_ingestIngest path: DSN-scoped writes
epure_appDashboard /api/v1/* with RLS enforced

Dashboard request pattern: valid cookie → org membership → SET LOCAL app.current_org_id → queries. Ingest authenticates with DSN → project_id. It does not set app.current_org_id from the client.

Events use monthly partitions events_YYYY_MM. TTL drops partitions older than project retention (14/30/90 days). Issue aggregates remain.

What is EPURE_MODE?

all, and nothing else. Split modes (ingress / worker / api) are unused on this binary.

Why does the app exit on migrate?

Wrong DATABASE_URL, or Postgres is not ready. Wait for postgres to report healthy, then read docker compose logs epure. Host tools talk to 5433, not 5432. Recreate the app container after credentials change: Troubleshooting.

Why does the browser SDK fail CORS?

The page origin is missing from EPURE_CORS_ORIGINS. List real frontend origins comma-separated, then recreate epure. * is fine locally; restrict it in production. Preflight hits OPTIONS on envelope and store: Troubleshooting.

Which database URL is required?

All three under Compose: DATABASE_URL for migrations, EPURE_INGEST_DATABASE_URL for DSN writes, EPURE_APP_DATABASE_URL for the dashboard RLS role. A missing ingest URL yields 500 storage_error; a missing app URL 500s /api/v1. Copy the blocks in the Database tab above.

Why does login 204 then 401 on HTTP?

EPURE_SESSION_SECURE=1 on plain HTTP. Set 0 for local HTTP so the cookie is Epure.sid; HTTPS needs 1 and __Host-epure.sid. Match EPURE_PUBLIC_URL to the origin the browser uses, then retry login. Troubleshooting.

How do I change the HTTP port?

cp .env.example .env. Set EPURE_PORT and EPURE_PUBLIC_URL to the same host port, then recreate epure. Leave EPURE_BIND at 0.0.0.0:8080 inside the container. Host Postgres is POSTGRES_HOST_PORT (default 5433).

Why does boot refuse example database passwords?

Documented epure / epure_ingest / epure_app strings (and leftover CHANGE_ME) are only allowed while EPURE_PUBLIC_URL is localhost. Production uses .env.production.example. Migrations scramble those role passwords; boot then sets them from .env.