Epure
Self-hostingInstallation

Installation

Two containers: Epure + PostgreSQL 16. No Redis. No separate worker fleet.

Laptop install is below. Production is a separate path: deploy/ compose overlay, deploy/env.production.example, HTTPS in front. Do not publish the laptop compose file.

Requirements

Docker Engine with Compose v2 (docker compose, not the legacy docker-compose binary), plus disk for images and the Postgres volume. Production also needs a DNS name and a reverse proxy that terminates HTTPS.

If docker compose up fails immediately, install or start Docker first — Epure docs assume a working Engine; see Troubleshooting.

Local

Clone and enter

Compose files live at the public product repo root.

Clone the public tree

command

git clone https://github.com/epure-sh/epure.git
cd epure

No .env required. docker compose up is enough for localhost:8080.

If 8080 is taken: cp .env.example .env, set EPURE_PORT, recreate epure (public URL follows on localhost). Or ./configure --quick. Recreate epure after any .env change.

Start

docker compose up

Pin production with EPURE_IMAGE=ghcr.io/epure-sh/epure:v0.1.7.

ServiceImagePorts (.env defaults)
epureghcr.io/epure-sh/epure:v0.1.7host ${EPURE_PORT} → container 8080 (default 8080)
postgrespostgres:16-alpinehost ${POSTGRES_HOST_PORT} → container 5432 (default 5433)

Laptop compose allows the documented example DB passwords only while EPURE_PUBLIC_URL is localhost. Point that URL at a real hostname and boot refuses to start until you switch to the production overlay.

Health

curl -sS http://localhost:8080/health
{"status":"ok"}

Dashboard: http://localhost:8080.

Create access

Register at /login. New workspaces get Acme Web + Acme API with sample errors (preview banner). Setup runs only when you create an additional project.

For high-volume local UX fixtures with fixed DSN UUIDs (never production):

./scripts/seed.sh --email you@example.com --password 'choose-a-password'
# or: ./scripts/seed.sh --link-user you@example.com

Passwords are never written into seed SQL.

Then Quickstart: copy the DSN and capture an exception.

Production

Sign-in only works on HTTPS

The production overlay sets a Secure session cookie. If you open http://YOUR_SERVER:8080, the browser drops that cookie. A correct password sends you back to the login form with no error.

Use a DNS name, such as https://errors.example.com. A raw IP cannot get a browser-trusted certificate. Open ports 80 and 443. Start with --profile tls so Caddy serves HTTPS. Then open that HTTPS URL. Do not bookmark port 8080.

Fresh Postgres volume. Do not reuse a laptop data directory.

Production env file

./configure --prod

Or cp deploy/env.production.example .env and replace every CHANGE_ME (openssl rand -hex 24). Set:

VarValue
EPURE_PUBLIC_URLhttps://errors.example.com (must be https://, DNS name, not a raw IP)
EPURE_SITE_ADDRESSerrors.example.com (hostname only; required for --profile tls)
EPURE_CORS_ORIGINSYour app origins, comma-separated. Not *
POSTGRES_PASSWORDSuperuser. Applied on first empty volume only
EPURE_INGEST_PASSWORDIngest role. Set on every boot from this file
EPURE_APP_PASSWORDDashboard role. Set on every boot from this file
EPURE_REGISTRATIONLeave unset until the first account exists, then false

Leave EPURE_DEV_SEED unset. Overlay forces EPURE_SESSION_SECURE=1 and refuses example passwords (epure, epure_ingest, epure_app, CHANGE_ME).

DNS and TLS

Point a DNS name at the box. A raw IP will not get a browser-trusted certificate, and sign-in on http://IP:8080 will not stick.

The production overlay can start Caddy for you. ./configure --prod writes EPURE_SITE_ADDRESS. The next step's command uses --profile tls, which publishes ports 80 and 443 and reverse-proxies to Epure. Open the HTTPS URL. If you open port 8080 over HTTP, the login page explains why the session cookie was dropped.

If you already run nginx, Caddy, or Traefik, skip --profile tls. Forward Host, X-Forwarded-For, and X-Forwarded-Proto to 127.0.0.1:8080 (or your EPURE_PORT). Allow POST and OPTIONS on /api/{project_id}/envelope/ and /api/{project_id}/store/.

Caddy on the host (ports 80 and 443 open), if you are not using --profile tls:

errors.example.com {
  reverse_proxy 127.0.0.1:8080
}

nginx:

server {
  listen 443 ssl;
  server_name errors.example.com;
  # ssl_certificate …;
  location / {
    proxy_pass http://127.0.0.1:8080;
    proxy_set_header Host $host;
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    proxy_set_header X-Forwarded-Proto $scheme;
  }
}

Session cookies are Secure-only. Start TLS before you expect browser login to work.

Start the overlay

docker compose -f docker-compose.yml -f deploy/docker-compose.prod.yml --profile tls up -d

--profile tls is required unless you already terminate HTTPS yourself. Without it, nothing listens on 443, and the login form on port 8080 cannot keep you signed in.

Postgres is not published on the host. Migrations run on boot, then the process sets ingest/app role passwords from .env. They are not the strings in old SQL files.

Health on HTTPS

curl -sS https://errors.example.com/health
{"status":"ok"}

On the box, curl -sS http://127.0.0.1:8080/health also works. The dashboard URL is EPURE_PUBLIC_URL.

Register

Open https://errors.example.com/login. If that host does not load, fix DNS and Caddy before you try a password. The first email/password register becomes owner of a new workspace. Do not run ./scripts/seed.sh here.

After that account exists, set EPURE_REGISTRATION=false and recreate epure. Sign-in stays available. The public URL cannot open another workspace. Invitation links still work. Configuration.

After that account exists, set EPURE_REGISTRATION=false and recreate epure. Sign-in stays available. The public URL cannot open another workspace. Invitation links still work. Configuration.

Then Quickstart with your public DSN host.

PaaS and panels

Same two containers as local install — epure + postgres:16-alpine — with production DB roles and HTTPS-facing URLs. Templates live in the public product repo; you import from Git, you do not export a marketplace package to use them.

PlatformEntryRepo path
RenderDeploy to Renderrender.yaml (repo root)
RailwayDrag compose onto project canvasdeploy/railway/docker-compose.yml + shared secrets (README)
CoolifyDocker Compose build pack, public GitBase dir deploy/templates/coolify
DokployCompose from Gitdeploy/templates/dokploy/docker-compose.yml

After deploy: GET /health on the public origin → register at /login → Quickstart. Narrative + FAQ: One-click deploy Epure. Marketing: Self-host § Deploy.

Not the laptop compose file

Root docker-compose.yml allows example DB passwords only while EPURE_PUBLIC_URL is localhost. PaaS templates and Production use EPURE_ALLOW_EXAMPLE_PASSWORDS=0 and separate passwords for epure, epure_ingest, and epure_app.

Existing Postgres volume

Changing POSTGRES_PASSWORD in .env does not retarget a volume that already initialized. Keep the original superuser password, or rotate it inside Postgres, then match DATABASE_URL.

Confirm the install

  • GET /health → {"status":"ok"} on the public origin
  • Login works; session persists across refresh (HTTPS in production)
  • Create DSN → SDK or Try ingest → 202 → issue in UI
  • Prod: Postgres not published on the host
  • Prod: backups for postgres_data (and artifacts_data if you upload maps)
Why does the app exit on migrate?

Postgres is not healthy, DATABASE_URL is wrong, or example passwords were used with a non-localhost EPURE_PUBLIC_URL. Wait until postgres is healthy, then read docker compose logs epure. Recreate epure after .env changes: Troubleshooting.

Can I use the laptop compose file in production?

No. That file allows example DB passwords only for localhost. Use deploy/docker-compose.prod.yml and deploy/env.production.example.

Do I need Redis?

No. The stack is two containers. See Configuration.

Render, Railway, Coolify, or Dokploy?

Import the checked-in templates from GitHub (PaaS and panels). Optional marketplace listings are separate from Git import. Compare with VPS production steps above.

Next