Epure
Self-hostingConfiguration

Configuration

Default path

docker compose up — no .env, no configure step. Dashboard at http://localhost:8080.

Render, Railway, Coolify, Dokploy

PaaS and panel installs use the production role passwords from the template compose files, not the laptop defaults. See PaaS and panels.

When you need to change something, use one file: copy .env.example → .env, edit, recreate epure.

cp .env.example .env   # only if 8080/5433 conflict or you want demo seed / CORS

On localhost, EPURE_PUBLIC_URL follows EPURE_PORT automatically. Set it for ngrok, a custom hostname, or production HTTPS.

Production login needs the HTTPS URL

./configure --prod refuses a raw IP. The start command is docker compose -f docker-compose.yml -f deploy/docker-compose.prod.yml --profile tls up -d. Open https://your-hostname/login. Port 8080 is the app inside the proxy. Signing in there drops the Secure cookie and returns you to the form.

Optional helpers (same result as editing .env):

CommandWhen
./configure --quickAuto-pick ports if 8080/5433 are busy
./configure --devVite CORS for localhost:5173
./configure --prodVPS: HTTPS hostname + generated passwords. Then --profile tls

What to change

ChangeVars
Dashboard / ingest host portEPURE_PORT (public URL follows on localhost)
Pin the app imageEPURE_IMAGE=ghcr.io/epure-sh/epure:v0.1.7
Postgres on the host (psql, cargo test)POSTGRES_HOST_PORT; host DATABASE_URL* in .env.dev
CORS / cookie / OAuthEPURE_CORS_ORIGINS, EPURE_SESSION_SECURE, GOOGLE_*
Close public sign-upEPURE_REGISTRATION=false

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:

./configure --prod

Or copy deploy/env.production.example → .env and 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 overlay—Superuser. First empty volume only
EPURE_INGEST_PASSWORDProd overlay—Applied to role epure_ingest on every boot
EPURE_APP_PASSWORDProd overlay—Applied to role epure_app on every boot
EPURE_IMAGENoghcr.io/epure-sh/epure:v0.1.7Production image pin. Add a row to lib/changelog/releases.ts when a newer release ships
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. On HTTP the login page says the browser will drop it
EPURE_SITE_ADDRESSWith --profile tls—Hostname only, for the Caddy service in the prod overlay
EPURE_PUBLIC_URLNoauto from EPURE_PORT on localhostPublic origin for OAuth and absolute links; required HTTPS in prod
EPURE_DEV_SEEDNooff1 applies fixed-UUID scaffold (scripts/seed-dev.sql) on startup — no accounts/passwords; dev only
EPURE_REGISTRATIONNoopenfalse, 0, off, no, or disabled blocks new workspaces. Login and invitation links stay open. Recreate the container after changing it
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, and the session cookie as credentials. The ingest DSN uses a public key — paste it into Sentry.init like any other public ingest credential.

SecretHandling
Postgres passwordsLaptop defaults are example strings, refused when EPURE_PUBLIC_URL is not localhost. Production: deploy/env.production.example
Google client secretEnv only; never commit
DSN public keyIngest-only; safe in client apps. Rotate/revoke from Settings if abused
Session cookieHttpOnly; __Host- prefix when secure

Never ship EPURE_DEV_SEED=1 or ./scripts/seed.sh into 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.

How do I stop strangers from registering?

Create your account first. Set EPURE_REGISTRATION=false in .env and recreate epure. The login page hides Register. POST /api/v1/auth/register without an invite returns 403. Sign-in, existing Google accounts, and invitation links still work. Unset the variable (or set true) and recreate to open registration again.

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, recreate epure. On localhost the public URL follows the port automatically. Or ./configure --quick / ./configure --port 3000. 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 deploy/env.production.example. Migrations scramble those role passwords; boot then sets them from .env.