Skip to content

Configuration

Every Repro setting is a single environment variable. The bundled compose.yaml reads them from .env in the same directory. This page is the exhaustive reference; grab .env.example for a well-commented template.

Required

You need exactly four values to boot:

VariableDescriptionGenerate with
POSTGRES_PASSWORDPostgres password. Kept internal to the Docker network.openssl rand -hex 32
BETTER_AUTH_URLBase URL your dashboard is served at. Must match the origin visitors use (session cookies).
BETTER_AUTH_SECRETSession-cookie signing secret.openssl rand -hex 32
ATTACHMENT_URL_SECRETSigns the attachment links embedded in GitHub issues and comments. See the warning below.openssl rand -hex 32

Without these four the stack refuses to start (compose refuses interpolation; the dashboard refuses to boot).

Rotating ATTACHMENT_URL_SECRET breaks existing image links

The dashboard signs the links to screenshots it embeds in GitHub issues and to images pasted into comments. These links don't expire in practice: issue screenshots are valid for about 100 years and comment images for one year. Changing ATTACHMENT_URL_SECRET invalidates every link signed so far. Each screenshot already embedded in a GitHub issue shows as a broken image, and so does every image pasted into a comment, in both the dashboard and GitHub. There's no way to re-sign old links, so rotate only if the secret has leaked.

A signed link needs no sign-in. Anyone who can see the GitHub issue can open its screenshot, and on a public repository that's everyone.

Hostnames

Sign-in only accepts requests whose origin is BETTER_AUTH_URL. If people reach the same install on more than one hostname, list the others here.

VariableDefaultDescription
BETTER_AUTH_TRUSTED_ORIGINSExtra origins, comma-separated. Each entry needs its scheme; the dashboard refuses to boot otherwise.

For example:

bash
BETTER_AUTH_TRUSTED_ORIGINS=https://feedback.internal.example.com,https://*.example.com

This only relaxes the origin check. Session cookies still belong to the hostname that set them, so someone who signs in on one hostname is signed out on the other, and OAuth callbacks and emailed magic links always use BETTER_AUTH_URL. For most installs, one canonical hostname (with the others redirecting to it at the proxy) is simpler.

Compose overrides

Optional knobs the bundled compose reads:

VariableDefaultDescription
REPRO_VERSIONlatestPin the image tag. Recommended in prod so you control when you upgrade.
PORT3000Host port the dashboard binds to. Container internal port is always 3000.
POSTGRES_USERreproPostgres user name.
POSTGRES_DBreproPostgres database name.

The compose file wires DATABASE_URL internally to the postgres service — don't set it yourself when using compose.

Storage

See Storage for per-provider endpoints.

VariableDefaultDescription
STORAGE_DRIVERlocallocal (Docker volume) or s3 (any S3-compatible endpoint).
STORAGE_LOCAL_ROOT/data/attachmentsPath inside the container. Compose mounts the volume here.
S3_BUCKETBucket name (s3 only).
S3_REGIONRegion (s3 only). auto for R2; any string for MinIO.
S3_ACCESS_KEY_IDAccess key (s3 only).
S3_SECRET_ACCESS_KEYSecret (s3 only).
S3_ENDPOINTLeave empty for AWS; per-provider URL otherwise.
S3_VIRTUAL_HOSTEDfalsetrue for AWS S3 (virtual-hosted addressing); false elsewhere.

Email

Magic-link sign-in + invite delivery.

VariableDefaultDescription
MAIL_PROVIDERconsoleconsole (logs the link to stdout), smtp (real email), ethereal (nodemailer preview).
SMTP_HOSTSMTP server hostname (when MAIL_PROVIDER=smtp).
SMTP_PORT587SMTP port.
SMTP_USERSMTP auth user.
SMTP_PASSSMTP auth password.
SMTP_FROMRepro <no-reply@localhost>From: header on sent mail.

console is the right default for the first boot — grab the magic-link URL from docker compose logs dashboard | grep link:, paste it into your browser, and press Sign in on the page that opens. Switch to smtp once you want real users to receive email.

OAuth sign-in

Buttons are hidden on the sign-in page when the secrets are blank. Leave the fields empty to disable a provider.

VariableDescription
GITHUB_CLIENT_IDGitHub OAuth App client id.
GITHUB_CLIENT_SECRETGitHub OAuth App client secret.
GOOGLE_CLIENT_IDGoogle OAuth client id.
GOOGLE_CLIENT_SECRETGoogle OAuth client secret.

Callback URLs you'll need in the OAuth app configuration:

  • <BETTER_AUTH_URL>/api/auth/callback/github
  • <BETTER_AUTH_URL>/api/auth/callback/google

GitHub Issues sync

Uses a GitHub App (not OAuth). See Integrations → GitHub for the walkthrough.

VariableDefaultDescription
GITHUB_APP_IDNumeric App ID from the App's settings page.
GITHUB_APP_PRIVATE_KEYLiteral PEM contents (newlines as \n) or an absolute path to a .pem.
GITHUB_APP_WEBHOOK_SECRETWhatever you pasted into the App's Webhook secret field.
GITHUB_APP_SLUGreproSlug from the App's public URL (e.g. https://github.com/apps/<slug>).
GITHUB_APP_CLIENT_IDClient ID from the App's settings page.
GITHUB_APP_CLIENT_SECRETA client secret generated on the App's settings page.
GITHUB_WEBHOOK_MAX_BYTES1048576Currently ignored. Webhook bodies over 5 MB are always rejected.

These are only needed if you create the GitHub App by hand instead of with the in-app wizard. Set all five of GITHUB_APP_ID, GITHUB_APP_PRIVATE_KEY, GITHUB_APP_WEBHOOK_SECRET, GITHUB_APP_CLIENT_ID and GITHUB_APP_CLIENT_SECRET: if any is missing, the dashboard ignores them all and uses the wizard-created app, if there is one.

Database connection + pool

DATABASE_URL is set by the compose file to the internal postgres service. If you're running the dashboard outside compose (pointing at a managed Postgres), set it yourself to postgres://user:pass@host:5432/dbname.

VariableDefaultDescription
DATABASE_URLPostgres connection string. Required if not using the bundled compose.
DB_POOL_MAX10Max concurrent connections per dashboard worker.
DB_STATEMENT_TIMEOUT_MS30000Kills any single query exceeding this many ms.
DB_IDLE_TX_TIMEOUT_MS10000Kills connections held open by a leaked BEGIN.

Intake limits

Tune the SDK intake path. Defaults are safe — only change if you have a specific reason.

VariableDefaultDescription
INTAKE_RATE_PER_KEY60Reports / minute / project.
INTAKE_RATE_PER_IP20Reports / minute / IP.
INTAKE_RATE_PER_KEY_ANON10Stricter bucket for anonymous submissions (no reporter.userId).
INTAKE_MAX_BYTES5242880Max total multipart payload (5 MB).
INTAKE_REQUIRE_DWELLtrueReject submissions that omit _dwellMs. Set false during a rolling SDK upgrade only.
INTAKE_MIN_DWELL_MS1500Minimum ms between widget-open and submit (anti-bot).
INTAKE_REPLAY_MAX_BYTES1048576Max gzipped bytes per replay attachment.
REPLAY_FEATURE_ENABLEDtrueKill-switch for session replay across the whole install.
TRUST_XFFfalseTrust X-Forwarded-For. Only set true when behind a trusted reverse proxy — spoofable.

Auth rate limits

Protects /api/auth/sign-in/* (sending a magic link, starting an OAuth sign-in) and /api/auth/magic-link/verify from credential-stuffing / enumeration. Each path has its own allowance. Session checks, sign-out, and OAuth callbacks are not limited.

VariableDefaultDescription
AUTH_RATE_PER_IP_PER_15MIN5Max attempts per IP per path per 15-minute window.
AUTH_RATE_LIMIT_ENABLEDon in prod; off in dev/testExplicit override: true force-on, false force-off.
RATE_LIMIT_STOREmemorymemory (per-worker, fine for single replica) or postgres (shared across replicas). Applies to the intake and invite limits only; the auth limits are always kept in each worker's memory.

The auth limiter finds the client IP differently from the intake limiter:

  • It reads the first address in X-Forwarded-For, whether or not TRUST_XFF is set.
  • If that header is missing, it can't tell clients apart and skips the limit, logging Rate limiting skipped: could not determine client IP address once.

So in production, run the dashboard behind a reverse proxy that overwrites X-Forwarded-For with the connecting client's address. See Reverse proxy.

Everyone behind one office NAT or VPN shares one IP, and so one allowance. When a sign-in hits the limit, people see "Too many sign-in attempts from your network" on the sign-in page. If a whole office keeps hitting it, raise AUTH_RATE_PER_IP_PER_15MIN rather than turning the limit off.

Invites

VariableDefaultDescription
INVITE_RATE_PER_ADMIN5Invite emails / minute / admin. Catches runaway loops before SMTP quota does.

Runtime

VariableDefaultDescription
NODE_ENVproductionSet by the image. Don't override in .env.
NITRO_HOST0.0.0.0Bind address. Don't override.
NITRO_PORT3000Container internal port.

Validating your config

Compose can dry-run interpolation without starting anything:

bash
docker compose config

Anything missing or malformed surfaces immediately.