Self-hostingInstallation

Installation

Clone Epure, start Rust + PostgreSQL 16, and confirm GET /health.

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

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

Requirements

Docker Compose v2, disk for images plus the Postgres volume. Production also needs a DNS name and a reverse proxy that terminates HTTPS.

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

Ports live in one file. Copy only if 8080 is taken:

cp .env.example .env

Change EPURE_PORT and EPURE_PUBLIC_URL together. Recreate epure after any .env change.

Start

docker compose up

Compose pulls ghcr.io/epure-sh/epure:latest. Pin a tag with EPURE_IMAGE in .env.

ServiceImagePorts (.env defaults)
epureghcr.io/epure-sh/epure:latesthost ${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, or for local smoke only (never production):

./scripts/seed-dev.sh

Login: dev@epure.local / devpassword

Then Quickstart: copy the DSN and capture an exception.

Production

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

Copy the production env file

cp .env.production.example .env

Replace every CHANGE_ME. Generate passwords with openssl rand -hex 24. Set:

VarValue
EPURE_PUBLIC_URLhttps://errors.example.com (must be https://)
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

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 the hostname at the box. Put Caddy (or nginx) in front of 127.0.0.1:8080 (or your EPURE_PORT). Forward Host, X-Forwarded-For, X-Forwarded-Proto. Allow POST and OPTIONS on /api/{project_id}/envelope/ and /api/{project_id}/store/.

Caddy (ports 80 and 443 open):

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;
  }
}

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

Start the overlay

docker compose -f docker-compose.yml -f docker-compose.prod.yml up -d

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. The first email/password register becomes owner of a new workspace. Do not run ./scripts/seed-dev.sh here.

Then Quickstart with your public DSN host.

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 ingest202 → 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 docker-compose.prod.yml and .env.production.example.

Do I need Redis?

No. The stack is two containers. See Configuration.

Next