Skip to content

Environment variables

Authoritative list of every environment variable read by Trackarr at runtime — sourced directly from the three application binaries (apps/api, apps/web, apps/tracker) and the shared packages/db connector.

The compose files (docker-compose.prod.yml, docker-compose.loadtest.yml, docker-compose.static.yml) and the Caddyfile read a few operator-side variables on top — those are flagged in the Read by column.

Generate secrets

Anything marked required in the Default column must be set explicitly. The recommended way is openssl rand -hex 32 (32-byte hex string, 64 chars).

Required

The application refuses to start without these. The tracker (Go binary) prints FATAL: <var> is required and exits non-zero; the API throws on boot.

VariableRead byPurpose
DATABASE_URLapi, trackerPostgreSQL DSN. In compose this points at PgBouncer (postgres://…@pgbouncer:6432/…).
REDIS_URLapi, trackerRedis DSN. redis:// or rediss:// for TLS. Pair with REDIS_PASSWORD when needed.
IP_HASH_SECRETapi, trackerDaily-rotated salt for SHA-256 peer-IP fingerprinting. Also keys the UDP connection_id HMAC.
NUXT_SESSION_SECRETapi, webh3 sealed-cookie key (iron-webcrypto). 32+ chars. Fallback for CHANNEL_ENCRYPTION_KEY.
ADMIN_API_KEYapiBearer token for internal admin endpoints. The API returns 503 while it is unset.

Application & runtime

VariableRead byDefaultPurpose
NODE_ENVapi, web, composedevelopmentToggles production-grade defaults (log level, error masking, TLS).
NITRO_PORTapi (compose)4000Nitro listener port inside the API container.
NITRO_HOSTapi (compose)0.0.0.0Bind interface.
TZapi (compose)Europe/ParisContainer timezone — used for bonus_grants.created_at and similar.
LOG_LEVELapiinfo (prod), debugtrace/debug/info/warn/error. Wires into the slog handler.
UPLOADS_DIRapi./data/uploadsFilesystem root for logos, favicons, NFOs, inside the API container. Ignored when STORAGE_DRIVER=s3.
API_INTERNAL_URLwebhttp://api:4000Used by Nuxt SSR to fetch from the API across the docker network.
IMAGE_TAGcomposelatestGHCR tag pulled by docker-compose.prod.yml. Pin in production.

Database (PostgreSQL)

DATABASE_URL is the canonical DSN. The split DB_* variables are read only by compose to render the URL and pass POSTGRES_* to the postgres image — the running app never reads them directly.

VariableRead byDefaultPurpose
DATABASE_URLapi, trackerPostgreSQL DSN. Required.
MIGRATIONS_DATABASE_URLapiDATABASE_URLDirect postgres DSN (bypasses PgBouncer) used by the boot migrations.
TRACKER_DB_MAX_CONNStracker20Postgres pool cap per tracker instance; multiply by the instance count when sizing PgBouncer.
TRACKER_SYNCHRONOUS_COMMITtrackeroffApplied to the tracker's connections only, never the cluster: losing the last few announce deltas in a crash is acceptable, losing a password change is not. One of on, off, local, remote_write, remote_apply — anything else and the tracker refuses to start. Measured effect at realistic rates: within noise, because the 30 ms announce floor hides it.
TRACKER_STATS_FLUSH_INTERVALtracker60sHow long byte credits are batched in Redis before they are applied to users. A member seeding N torrents updates their own row N times per announce interval; the additions are commutative, so nothing requires applying them one at a time. Measured over five minutes of traffic at 1 964 credited announces/s and 50 000 members, at steady state: disabled 113–125 MB of WAL and 589 200 row writes, 60s 45.8 MB and 226 315, 300s 7.5 MB and 50 000. The cost is staleness — the counters lag by up to one window. The only gate that reads them is the announce ratio check, which already sees a 60 s-old value from the passkey cache and only repeats once per announce interval per torrent; past 60 s it is the member who notices their own ratio trailing. 0s restores the per-announce write.
TRACKER_STATS_FLUSH_CHUNKtracker10Members written per transaction during a flush. Do not raise without measuring: past a few dozen, HOT pruning can no longer recycle in-page space, every row migrates and rewrites all seven indexes, and a single-block flush writes MORE WAL than the individual writes it replaces (45 317 rows in one transaction: 19% HOT; in chunks of ten: 99.8%).
DB_USERcomposetrackerPostgres role; renders into the DSN.
DB_PASSWORDcomposetrackerPostgres password; renders into the DSN.
DB_NAMEcomposetrackarrDatabase name.
DB_PORTcompose5432Listener port (host side).
DB_POOL_MAXapi10pg.Pool size per Nitro worker.
DB_SSLapitrue (prod)true/false. Forces pg.ssl regardless of NODE_ENV.
DB_SSL_REJECT_UNAUTHORIZEDapitrueDisable cert validation when set to false.
DB_SSL_CAapiunsetInline PEM CA bundle (escape newlines as \n).
DB_DEBUGapifalseWhen true, Drizzle logs every query.

Redis

VariableRead byDefaultPurpose
REDIS_URLapi, trackerDSN. Required.
REDIS_PASSWORDapi, trackerunsetOptional in both. When set, sent as AUTH; when unset, the client connects unauthenticated and the API logs a warning in production.
REDIS_KEY_PREFIXapi, trackerot:Namespace for every key. Must match across api ⇄ tracker.
REDIS_TLSapifalsetrue forces TLS even when the URL scheme is redis://.
REDIS_TLS_REJECT_UNAUTHORIZEDapitrueDisable cert validation when set to false.
REDIS_TLS_CAapiunsetInline PEM CA bundle for Redis TLS.
REDIS_TLS_CA_FILEapiunsetPath to a CA bundle file (overrides REDIS_TLS_CA).
REDIS_TLS_CERT_FILEapiunsetPath to a client certificate (mTLS).
REDIS_TLS_KEY_FILEapiunsetPath to the matching client key (mTLS).

Tracker (Go binary)

VariableRead byDefaultPurpose
TRACKER_HTTP_PORTtracker8080BEP 3 (HTTP) announce port.
TRACKER_UDP_PORTtracker6969BEP 15 (UDP) announce port.
TRACKER_UDP_ENABLEDtracker, apitrueToggles the UDP listener and UDP-tier inclusion in .torrent downloads. Three processes read this: the tracker (listener), the API stats endpoint (/api/stats/public), and /api/torrents/:hash/download.
TRACKER_UDP_SCRAPE_ENABLEDtrackertrueAnswers BEP 15 scrape requests. false makes the tracker drop them silently — BEP 15 has no field for authentication data in a scrape request, so a private tracker that does not want its swarm sizes published closes the door rather than guarding it. Announce is unaffected. The reference Caddy configuration does not route HTTP /scrape either, for the same reason.
TRACKER_DEBUGtrackerfalseVerbose announce + dispatch logging.
TRACKER_PEER_TTLtracker24hHow long a peer's Redis entry survives between announces. Go duration syntax (24h, 90m, 7200s). Values below 15m are clamped — anything shorter would zero the delta computation. Raise it for very forgiving deployments; lower it on huge swarms to reclaim Redis memory.
TRACKER_INTERNAL_URLapihttp://tracker:8080Internal URL the API hits for /api/tracker-health.
TRACKER_HEALTH_URLapiTRACKER_INTERNAL_URLOverride the health-probe URL specifically.
TRUST_PROXYapi, trackerfalseHonour X-Forwarded-For / X-Real-IP for client IP. Set to true only behind a trusted reverse proxy. The rightmost token is used (the one your proxy appended).
TRUST_CF_CONNECTING_IPapi, trackerfalseHonour CF-Connecting-IP. Keep false unless fronted by Cloudflare with ingress locked to its IP ranges — otherwise the header is client-spoofable (the Caddyfile strips it by default) and trusting it bypasses IP bans/rate limits. Requires TRUST_PROXY=true.
NUXT_PUBLIC_TRACKER_HTTP_URLapi, webhttp://localhost:8080/announceAnnounce URL embedded in .torrent files (runtime).
NUXT_PUBLIC_TRACKER_UDP_URLapi, webunsetUDP tier added when TRACKER_UDP_ENABLED=true and this URL is set.
NUXT_PUBLIC_TRACKER_WS_URLapi, webunsetReserved; WebTorrent (wss) is not implemented yet — don't advertise it.

Tracker tunables aren't env-driven

Announce interval (1800 s), min interval (900 s) and per-response peer cap (50 HTTP / 100 UDP) are hard-coded in apps/tracker/internal/server/response.go. Rebuild the binary if you need to change them.

Object storage (uploads)

Uploaded files go to a filesystem directory by default. STORAGE_DRIVER=s3 sends them to an S3-compatible bucket instead, which is what removes the API's need for a shared volume when running several replicas — see Object storage.

The credentials are read through the same readSecret() path as the rest, so each also accepts a Docker secret via <NAME>_FILE.

VariableRead byDefaultPurpose
STORAGE_DRIVERapifsfs or s3. Anything else refuses to start.
S3_ENDPOINTapirequiredBase URL of the store, e.g. https://s3.eu-west-3.amazonaws.com or http://minio:9000.
S3_BUCKETapirequiredBucket name.
S3_REGIONapius-east-1Part of the SigV4 signing scope, so it must match the bucket's or every request is a 403.
S3_ACCESS_KEY_IDapirequiredAlso S3_ACCESS_KEY_ID_FILE.
S3_SECRET_ACCESS_KEYapirequiredAlso S3_SECRET_ACCESS_KEY_FILE.
S3_SESSION_TOKENapiTemporary STS credentials. Also S3_SESSION_TOKEN_FILE.
S3_PREFIXapi(empty)Key prefix, so one bucket can hold more than one thing. Changing it hides existing objects.
S3_FORCE_PATH_STYLEapitrueendpoint/bucket/key rather than bucket.endpoint/key. Needed by MinIO, RustFS, Ceph RGW; set false for AWS.
S3_CREATE_BUCKETapifalseCreate the bucket on the first write if it is missing.
S3_TIMEOUT_MSapi30000Per-request timeout against the store.

required means required when STORAGE_DRIVER=s3; with the default fs none of these are read.

Security & sessions

VariableRead byDefaultPurpose
NUXT_SESSION_SECRETapi, webRequired. h3 sealed-cookie key. 32+ chars.
IP_HASH_SECRETapi, trackerRequired. Salt for IP fingerprints + UDP connection-id HMAC.
ADMIN_API_KEYapiRequired. Bearer token for internal admin endpoints.
CHANNEL_ENCRYPTION_KEYapifalls back to NUXT_SESSION_SECRETAES key for notification-channel configs at rest (SMTP password, Telegram token, VAPID private key). Generate with openssl rand -hex 32.
TRUST_PROXYapi, trackerfalseSee Tracker — same flag, same semantics, same caveats.
TRUST_CF_CONNECTING_IPapi, trackerfalseSee Tracker — only enable behind Cloudflare with locked ingress.
SAFE_FETCH_ALLOW_HOSTSapi— (empty)Comma-separated exact hostnames allowed to bypass the SSRF private/loopback block. Needed only to federate with a peer reachable over a private network (LAN / VPN / docker service name). List trusted peer hosts only; empty leaves SSRF protection unchanged.
CREDENTIAL_ENCRYPTION_KEYapifalls back to CHANNEL_ENCRYPTION_KEY, then NUXT_SESSION_SECRETAES key for the two account secrets at rest: users.auth_verifier (which IS the login credential, not a hash of it) and users.totp_secret. Set it to keep credentials separate from notification-channel secrets. Generate with openssl rand -hex 32.
CREDENTIAL_ENCRYPTION_SALTapitrackarr:credentials:v1KDF salt for the key above. Never change it on a deployment that already holds encrypted credentials — every account would stop being able to log in.
FEDERATION_REQUIRE_AUDIENCEapifalseRefuse anything that carries only an audience-less signature — both the inbound S2S request signature and a relay's countersignature on a record. Each now binds the recipient's instanceId, which stops one partner replaying a signed request, or forwarding a countersignature made for somebody else, to another instance. Until every peer runs a version that emits the bound form, an attacker can strip it and downgrade. Turn this on once all partners are upgraded — turned on too early it silently stops accepting a partner's relayed records.

Two-factor auth & WebAuthn

VariableRead byDefaultPurpose
TRACKARR_NAMEapiTrackarrDisplay name shown by authenticator apps as the TOTP issuer and the WebAuthn RP name.
WEBAUTHN_RP_IDapiinferred from OriginPasskey-bound hostname. Pin explicitly behind a load balancer.
WEBAUTHN_ORIGINapiinferred from OriginOrigin allow-list for assertions.

The Force 2FA enforcement scope (off / staff / all) is configured at runtime in /admin/settings and persisted to the settings table, not via env.

Metadata providers

All optional. Without a key, the matching source returns 503 for lookup routes and the upload picker simply hides the column.

VariableRead byDefaultPurpose
TMDB_API_KEYapiunsetTMDb v3 key or v4 Read-Only Access Token (auto-detected). Films + TV.
IGDB_IDapiunsetTwitch Client ID. Required to enable the IGDB games source. Pair with IGDB_SECRET.
IGDB_SECRETapiunsetTwitch Client Secret. Used to mint the IGDB app-access token (cached ~60 days).
METADATA_WARM_INTERVAL_MSapi20000How often the background warmer performs one metadata lookup. Floor 5000. Lower it to fill a fresh cache faster, at the cost of provider quota. See Metadata providers.
GOOGLE_BOOKS_API_KEYapiunsetFallback for the Open Library books source. Open Library itself needs no key.

Notifications

The in-app feed (/notifications + bell) always works. External transports (SMTP, Telegram, Discord, ntfy, Web Push, …) are configured in /admin/notifications and persisted encrypted in notification_channels — no env vars beyond CHANNEL_ENCRYPTION_KEY.

The three webhook URLs below are operator alerts for the API process itself (admin/security events), not user-facing notification channels.

VariableRead byDefaultPurpose
DISCORD_WEBHOOK_URLapiunsetWebhook target for operator alerts emitted by apps/api/utils/alerts.ts.
SLACK_WEBHOOK_URLapiunsetSame, Slack-shaped payload.
SECURITY_WEBHOOK_URLapiunsetSame, generic security webhook (custom integrations).
ENABLE_DEV_ALERTSapifalseWhen NODE_ENV !== 'production', alerts are dropped unless this is true.

Background jobs

VariableRead byDefaultPurpose
BONUS_COLLECTION_INTERVALapi3600000Bonus-collector tick period (ms). Cross-replica SETNX lock; persisted last-tick timestamp ensures exactly one tick across the fleet.
STATS_COLLECTION_INTERVALapi3600000site_stats snapshot period (ms). Same lock model.
BAN_EXPIRY_INTERVALapi300000Ban-expiry cron tick (ms). Sweeps is_banned=true AND banned_until<NOW() and fires account_unbanned. SETNX lock.
REQUEST_AUTO_VALIDATE_INTERVALapi600000Upload-request auto-validate cron tick (ms). Pays the filler when a request sits in filled past the admin-tuned timeout. SETNX lock.

Prometheus metrics

The metrics listener is opt-in and binds its own port — firewall it independently of the public API.

VariableRead byDefaultPurpose
METRICS_ENABLEDapifalseMaster switch (true/1/on/yes).
METRICS_HOSTapi0.0.0.0Bind address.
METRICS_PORTapi9090Bind port.
METRICS_PATHapi/metricsScrape path.
METRICS_AUTH_TOKENapiunsetWhen set, scrapes must present Authorization: Bearer <token>.
METRICS_PEER_CACHE_MSapi30000TTL of the cached Redis SCAN over peers:* used by the gauge.

See Prometheus Metrics reference for the gauge list and example PromQL.

System metadata (/api/admin/system/*)

VariableRead byDefaultPurpose
TRACKARR_REPOapiDim145/opentrackerGit repo (owner/repo) used by /api/admin/system/update to query GitHub for newer tags.
TRACKARR_RUNTIMEapinativeSet to docker inside containers so the admin dashboard renders the right "running in" badge.
APP_VERSIONapiinherited from package.jsonOverride the version reported by /api/admin/system/version.

Static SPA build (build-time)

When building the static frontend (apps/web/Dockerfile.static, docker-compose.static.yml):

VariableDefaultPurpose
NUXT_STATIC_BUILDfalseWhen true, nuxi generate produces the SPA bundle (no SSR).
NUXT_PUBLIC_TRACKER_HTTP_URLunsetBuild-time fallback. The runtime value is fetched from /api/runtime-config on boot, so the same image works across domains.
NUXT_PUBLIC_TRACKER_UDP_URLunsetBuild-time fallback (same mechanism).

Caddy & TLS (compose-only)

VariableRead byDefaultPurpose
DOMAINcaddylocalhostMain hostname; renders into the Caddyfile vhost block.
TRACKER_DOMAINcaddytracker.localhostSubdomain dedicated to announce traffic (also serves /announce).
ACME_EMAILcaddyadmin@example.comLet's Encrypt registration email.

Removed / deprecated

These were read by previous versions and no longer have any effect. If you carry them over from a <= v0.5.x .env, delete them — the new code ignores them silently and that's confusing.

  • TRACKER_HTTP_URL, TRACKER_UDP_URL, TRACKER_WS_URL → replaced by the NUXT_PUBLIC_* variants
  • NUXT_PUBLIC_SITE_NAME, NUXT_PUBLIC_SITE_URL → site title/URL are now in the settings table, edited from /admin/settings
  • NUXT_PUBLIC_ANNOUNCE_URL → use NUXT_PUBLIC_TRACKER_HTTP_URL
  • POSTGRES_USER, POSTGRES_PASSWORD, POSTGRES_DB, POSTGRES_HOST, POSTGRES_PORT → renamed to DB_* to match the compose surface
  • PORT → was never a real default; the API binds NITRO_PORT (4000)

Example .env

A minimum-viable production .env:

bash
# Domain + TLS
DOMAIN=tracker.example.com
TRACKER_DOMAIN=tracker.example.com
ACME_EMAIL=admin@example.com

# Secrets — generate each with `openssl rand -hex 32`
NUXT_SESSION_SECRET=...
IP_HASH_SECRET=...
ADMIN_API_KEY=...
CHANNEL_ENCRYPTION_KEY=...

# Database
DB_USER=tracker
DB_PASSWORD=...
DB_NAME=trackarr

# Redis
REDIS_PASSWORD=...

# Announce URLs surfaced in .torrent files
NUXT_PUBLIC_TRACKER_HTTP_URL=https://tracker.example.com/announce
NUXT_PUBLIC_TRACKER_UDP_URL=udp://tracker.example.com:6969/announce

# Optional metadata sources
# TMDB_API_KEY=...
# IGDB_ID=...
# IGDB_SECRET=...
# GOOGLE_BOOKS_API_KEY=...

Never commit .env

Add it to .gitignore (already done in the repo). Use .env.example as the template for documentation and CI.

Federation

Inter-instance federation is opt-in — off until the owner enables it in /admin/federation. It introduces no required variables: the instance's Ed25519 private key is encrypted with the same key as notification-channel secrets (CHANNEL_ENCRYPTION_KEY, falling back to NUXT_SESSION_SECRET).

VariableRead byDefaultPurpose
FEDERATION_SYNC_INTERVALapi900000 (15 min)Catalogue-sync cron period in ms. The cron is a no-op while federation is disabled.
TRACKER_FEDERATION_SWARMtrackerfalseMaster switch for swarm cross-announce on the Go tracker. Leave off unless you've deliberately enabled opt-in swarm federation.

See the Federation guide for the full feature, trust model and security notes.

NOTE

Outbound federation calls go through the SSRF-hardened safeFetch, which blocks loopback and private IP ranges. Federating two instances on the same LAN/host therefore needs public hostnames (an explicit host allow-list is not implemented yet).

Released under the MIT License.