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 unless you keep the old value

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 on its own 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.

To rotate safely, move the old value into ATTACHMENT_URL_SECRET_PREVIOUS (see below). Old links keep working; new links use the new secret. If the old secret has leaked, leave it out of ATTACHMENT_URL_SECRET_PREVIOUS and accept the broken links.

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.

Secret rotation ​

Both optional. Neither is needed for a fresh install.

VariableDefaultDescription
ATTACHMENT_URL_SECRET_PREVIOUS—Retired ATTACHMENT_URL_SECRET values, comma-separated. Used only to verify links signed before a rotation; new links are always signed with ATTACHMENT_URL_SECRET. Drop a value from the list once you no longer need links signed with it to work.
COMMENT_SIGNING_SECRETvalue of BETTER_AUTH_SECRETSigns the hidden footer on comments the dashboard posts to GitHub, so the webhook can tell its own comments from real ones. Set it so that rotating BETTER_AUTH_SECRET (which signs out everyone) does not also make every comment already on GitHub look foreign. Changing this value has the same effect on existing comments.
bash
# rotation example
ATTACHMENT_URL_SECRET=<new value from openssl rand -hex 32>
ATTACHMENT_URL_SECRET_PREVIOUS=<old value>

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_ORIGINS—Extra 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_BUCKET—Bucket name (s3 only).
S3_REGION—Region (s3 only). auto for R2; any string for MinIO.
S3_ACCESS_KEY_ID—Access key (s3 only).
S3_SECRET_ACCESS_KEY—Secret (s3 only).
S3_ENDPOINT—Leave 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_HOST—SMTP server hostname (when MAIL_PROVIDER=smtp).
SMTP_PORT587SMTP port.
SMTP_USER—SMTP auth user.
SMTP_PASS—SMTP 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_ID—Numeric App ID from the App's settings page.
GITHUB_APP_PRIVATE_KEY—Literal PEM contents (newlines as \n) or an absolute path to a .pem.
GITHUB_APP_WEBHOOK_SECRET—Whatever 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_ID—Client ID from the App's settings page.
GITHUB_APP_CLIENT_SECRET—A 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_URL—Postgres 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.