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 / CORSOn 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):
| Command | When |
|---|---|
./configure --quick | Auto-pick ports if 8080/5433 are busy |
./configure --dev | Vite CORS for localhost:5173 |
./configure --prod | VPS: HTTPS hostname + generated passwords. Then --profile tls |
What to change
| Change | Vars |
|---|---|
| Dashboard / ingest host port | EPURE_PORT (public URL follows on localhost) |
| Pin the app image | EPURE_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 / OAuth | EPURE_CORS_ORIGINS, EPURE_SESSION_SECURE, GOOGLE_* |
| Close public sign-up | EPURE_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 --prodOr 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.
| Binding | Dev compose | Prod overlay |
|---|---|---|
| App HTTP | ${EPURE_PORT:-8080}:8080 | same (upstream for TLS proxy) |
| Postgres | ${POSTGRES_HOST_PORT:-5433}:5432 on host | no 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 missing | What happens | What to do |
|---|---|---|
DATABASE_URL | Process exits on migrate | Match compose credentials; wait for postgres healthy |
EPURE_INGEST_DATABASE_URL | Ingest writes fail (500 storage_error) | Copy ingest URL from this page |
EPURE_APP_DATABASE_URL | Dashboard /api/v1 500s | Copy 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.
| Variable | Required | Default / compose | Description |
|---|---|---|---|
DATABASE_URL | Yes (compose) | postgres://epure:epure@postgres:5432/epure | Migrations / bootstrap role |
EPURE_INGEST_DATABASE_URL | Yes (compose) | postgres://epure_ingest:epure_ingest@postgres:5432/epure | Ingest writes (DSN path) |
EPURE_APP_DATABASE_URL | Yes (compose) | set from EPURE_APP_PASSWORD in prod | Dashboard API with RLS |
POSTGRES_PASSWORD | Prod overlay | — | Superuser. First empty volume only |
EPURE_INGEST_PASSWORD | Prod overlay | — | Applied to role epure_ingest on every boot |
EPURE_APP_PASSWORD | Prod overlay | — | Applied to role epure_app on every boot |
EPURE_IMAGE | No | ghcr.io/epure-sh/epure:v0.1.7 | Production image pin. Add a row to lib/changelog/releases.ts when a newer release ships |
EPURE_PORT | No | 8080 | Host port Compose publishes. Not read by the binary |
POSTGRES_HOST_PORT | No | 5433 | Host port for Postgres. Unused in the prod overlay |
EPURE_BIND | No | 0.0.0.0:8080 | Listen address inside the container. Leave this; change EPURE_PORT |
EPURE_MODE | No | all | Only all |
EPURE_CORS_ORIGINS | No | * (.env.example) | Comma-separated origins for ingest CORS |
EPURE_ARTIFACTS_DIR | No | /data/artifacts | JS/TS source map storage |
EPURE_SESSION_SECURE | No | 0 dev / 1 prod overlay | Session cookie Secure. On HTTP the login page says the browser will drop it |
EPURE_SITE_ADDRESS | With --profile tls | — | Hostname only, for the Caddy service in the prod overlay |
EPURE_PUBLIC_URL | No | auto from EPURE_PORT on localhost | Public origin for OAuth and absolute links; required HTTPS in prod |
EPURE_DEV_SEED | No | off | 1 applies fixed-UUID scaffold (scripts/seed-dev.sql) on startup — no accounts/passwords; dev only |
EPURE_REGISTRATION | No | open | false, 0, off, no, or disabled blocks new workspaces. Login and invitation links stay open. Recreate the container after changing it |
EPURE_WEBHOOK_ALLOW_PRIVATE | No | off | 1 allows private/loopback webhook targets (dev/tests only) |
GOOGLE_CLIENT_ID | No | empty | Enables Google sign-in when set |
GOOGLE_CLIENT_SECRET | No | empty | Google OAuth client secret |
GOOGLE_REDIRECT_URI | No | {EPURE_PUBLIC_URL}/api/v1/auth/google/callback | Must match the Google console |
RUST_LOG | No | typically info | Log filter |
EPURE_VELOCITY_WINDOW_SECS | No | internal default | Velocity 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.
| Secret | Handling |
|---|---|
| Postgres passwords | Laptop defaults are example strings, refused when EPURE_PUBLIC_URL is not localhost. Production: deploy/env.production.example |
| Google client secret | Env only; never commit |
| DSN public key | Ingest-only; safe in client apps. Rotate/revoke from Settings if abused |
| Session cookie | HttpOnly; __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_URL | Migrations on startup |
epure_ingest | Ingest path: DSN-scoped writes |
epure_app | Dashboard /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.
Related
- Installation
- Upgrades
- Troubleshooting
- Settings: cookies and OAuth in the UI
Installation
Install Epure with Docker Compose on a VPS or import one-click deploy templates for Render, Railway, Coolify, and Dokploy. Two containers, PostgreSQL 16, GET /health, then point your Sentry SDK DSN.
Upgrades
Five-step upgrade: backup Postgres, pull the GHCR image, confirm health, roll back with the dump.