A modern, high-performance private BitTorrent tracker
Three containers — Nuxt 4 web · Nitro API · Go tracker — backed by PostgreSQL and Redis.
Features • Architecture • Quick Start • Static deployment • Documentation • Live Demo
- Zero-Knowledge auth — PBKDF2-600k + SHA-256 verifier; the password never leaves the browser. Same flow drives sign-in, passkey rotation, and change-password.
- Proof of Work on registration stops drive-by signup spam.
- Hashed IPs — SHA-256 with daily-rotating salt; no raw IP persisted. Banning a user atomically banlists their last-known IP.
- Privacy toggles — hide last-seen on public profile (mods/admins always see the truth).
- Three keys, not one — the announce passkey, an RSS/Torznab key and an API key, each revocable on its own. Handing a feed URL to a third party no longer hands over the credential that announces for you. See Keys.
- Login history — every attempt to open a session, refused ones included, with the method and a daily-hashed address. Members read their own; staff read a member's from their profile, which is where account sharing gets noticed.
- Your data, both ways — one-click JSON export of everything the instance holds about you (GDPR Art. 15 / 20) alongside self-service erasure (Art. 17). Both behind a fresh-login step-up; the export names every omission and why.
- Rich media metadata — TMDb (films + TV), IGDB (games), Open Library + Google Books (books); user locale drives the lookup language.
- A catalogue that takes what you type —
s02e04,1080p,2019,@member, an IMDb / TMDb / TVDB link or an ISBN all become removable chips, and every filter lives in the URL. Works are grouped before releases (one card per film or series, unfolding into seasons, episodes and files), the rail counts every facet it offers, and a search that finds nothing proposes what to drop rather than apologising. See The catalogue. - Found by the title, not just the filename —
Sousou.no.Frieren.S01E10.MULTi.1080panswers to "frieren", because the titles the metadata providers return are indexed alongside the release names, and a near miss comes back as a suggestion. - Dedicated upload page — auto title + tags from filename, multi-source search picker, duplicate preflight, conditional ID block per category, Tiptap WYSIWYG description, NFO drag-drop (CP437 → UTF-8).
- Release sheet builder — a four-step wizard at
/torrents/ficheturns a video file into a BBCode sheet, an NFO and a normalised release name, then hands all three to the upload form. MediaInfo runs in the browser through WebAssembly and reads only the chunks it asks for, so a 40 GB remux is analysed without ever being uploaded. Every dropdown keeps an "Other…" entry, and bitrate/size unit selectors change the frame of reference without touching the value. - Operator console —
/admincovers users, categories, roles, invites, branding, panic, tags, Torznab, reports, HnR. - Torrent lifecycle — a release can be marked superseded by a better one (the older stays online and keeps its swarm), a dead swarm can ask its past snatchers for a reseed, and one request tells a client which of its torrents this tracker still serves. See Torrent lifecycle.
- IRC announce channel — one line in a channel per accepted upload, the mechanism autobrr and autodl-irssi race on, with the autobrr indexer definition generated from the announce template in force so the format and the parser cannot disagree. Off by default; one bot however many API instances. See IRC announce.
- Site statistics —
/statsis the site looking at itself: what the catalogue holds, how it grew, what is being grabbed, and a year in review — plus your own year. No per-member volume anywhere, because no setting lets a member opt out of one. See Site statistics. - Saved searches — a stored filter notifies its owner when a matching upload is accepted; the server-side half of autobrr, for members with no seedbox. See Saved searches.
- Invite tree — who vouched for a member and who they let in, ten generations either way. Erased accounts keep their edges and lose their name.
- Staff audit log — every mutating request to the admin and moderation consoles leaves one append-only row: actor and role as they were, action, target, what changed, and the status — refusals included. Written by a Nitro hook rather than per route, so a staff route added tomorrow is audited before anyone writes a line for it. Admins only. See Staff audit log.
- Installable (PWA) — a manifest served by the API, so the app's name, colours and icon follow the instance's branding. No offline cache: every page here is a live view of a swarm.
- Notification fan-out — every event-emitting route hits Postgres + Redis pub/sub + the user's chosen external transport (SMTP, Telegram, Discord, ntfy, Gotify, Pushover, Slack, Mattermost, webhook, Apprise, Web Push).
- HTTP announce (BEP 3) on
8080/tcp; sub-ms p99; alloc-friendly bencode path. - UDP announce (BEP 15) on
6969/udp; ~6×–8× cheaper on the wire than HTTP; statelessconnection_id= HMAC-SHA256(secret, ip ‖ minute), so no per-id memory. - BEP 41 URL_DATA passkey —
udp://host:6969/announce/PASSKEYworks as-is in every modern client. - Multi-tier
.torrentfiles — generator advertises HTTP + UDP independently.TRACKER_UDP_ENABLED=falsedisables UDP and drops it from new.torrentfiles in one go. - BitTorrent v2 (BEP 52) — a v2 or hybrid torrent announces under a second, truncated SHA-256 infohash; the tracker resolves either form and keys both on the canonical hash, so a hybrid torrent's two swarms are one.
content_root_v2gives cross-tracker content addressing that piece length and the private flag cannot move. See BitTorrent v2. - BEP 21 partial seeds — a client holding only the files it asked for announces
event=paused; it stays in the swarm, counts as a leecher rather than a seed, and banks no seed time towards a requirement it cannot meet. - Torznab that tells the truth —
minimumratioandminimumseedtimecarry the site's real obligations to Sonarr / Radarr, and the volume factors follow the running bonus event instead of claiming normal rates during a freeleech.
- Seed-bonus points — customisable per-minute rules (time, torrent age, rarity); tiered curves with live preview; ledger-backed.
- Bonus shop — operator-curated catalogue with built-in
upload_creditandinviteeffects. - Bonus events — time-bounded Freeleech / Silverleech / custom multipliers, applied on the announce hot path.
- Per-torrent buffs — freeleech, silverleech or double-upload on one release, plus pinning. Where a buff meets a site-wide event the member gets the better of the two on each axis, never the product; a lapsed buff is neutralised in the announce query itself, so nothing has to sweep. See Per-torrent buffs.
- Panic Mode — instant AES-256-GCM encryption of torrent data + user fields; recovery requires the original Panic Password. See Panic Mode.
- Distributed rate limiting — Redis-backed sliding windows; progressive penalties; auto IP bans.
- Optional static deployment — distroless nginx serves a CSR bundle in ~28 MB (see below).
- Owner-curated network — connect independent Trackarr instances through a double opt-in, Ed25519-signed handshake; off by default, enabled from
/admin/federation. - Granular, asymmetric scopes — share/accept
catalog,social,accounts,swarmper peer and per direction; suspend / block / revoke / re-scope any peer from the owner console. - Federated catalogue — browse & search partner content (cron-synced cache + live fan-out) with cross-instance dedupe; a remote download links back to its origin instance (never your local passkey).
- Federated social & identity — follow remote uploaders, read-only sanitized comments/forum, and prove you own an account on a partner (verified-identity badge + read-only reputation; no SSO, economies stay isolated).
- Optional swarm cross-announce — opt-in per torrent and per peer, behind
TRACKER_FEDERATION_SWARM(off); onlyip/port/isSeedercross the wire and ratio/HnR stay local. See Federation.
Three independent containers behind Caddy, plus the usual Postgres + Redis. They share zero process state — Redis is the only cross-cutting bus.
┌─────────────────────────────────────────────┐
Browser ──HTTPS──► │ Caddy :80 / :443 / :443/udp (HTTP/3) │
│ /announce* → tracker :8080 (BEP 3) │
│ /api/* → api :4000 │
│ /uploads/* → api :4000 │
│ /* → web :3000 (Nuxt SSR)│
└─────────────────────────────────────────────┘
BT client ──UDP──► tracker :6969 (BEP 15, bypasses Caddy — UDP can't be reverse-proxied)
┌──────────────┐ ┌──────────────┐ ┌──────────────┐
│ apps/web │ │ apps/api │ │ apps/tracker │
│ Nuxt 4 SSR │ │ Nitro 4 │ │ Go 1.25 │
│ Vue 3 / TS │ │ Drizzle ORM │ │ sqlc │
│ (stateless) │ │ Zod, h3 │ │ HTTP + UDP │
└──────────────┘ └──────┬───────┘ └──────┬───────┘
│ │
▼ ▼
┌────────────────────────────────┐
│ PgBouncer :6432 │
└────────────────┬───────────────┘
▼
┌────────────────────────────────┐
│ PostgreSQL :5432 │
└────────────────────────────────┘
┌────────────────────────────────┐
api ──►│ Redis :6379 │◄── tracker
│ peers, sessions, rate-limit, │
│ seed-bonus, notification bus │
└────────────────────────────────┘
Why three containers
- The tracker is its own thing. It's the hot path — every BitTorrent client in the swarm hits
/announceevery few minutes. A static Go binary onscratch(~10 MB image, sub-ms p99) means a single broken Nuxt deploy can't take down announces. Two transports — BEP 3 over HTTP/8080 and BEP 15 over UDP/6969 — share one wire-agnostic processor. - API and web are split.
apps/api(Nitro standalone) owns every/api/*route, upload endpoints, metadata lookups, admin tools.apps/webis Nuxt SSR — rendered shell + page chunks. They scale and redeploy independently. - Distroless everywhere.
apps/web+apps/apirun ongcr.io/distroless/nodejs24-debian13:nonroot;apps/trackerruns onscratch; the static frontend runs oncgr.dev/chainguard/nginx. No shells, no package managers, non-root by default.
More details: Architecture guide.
- Docker 20+ and Docker Compose v2
- For production: a domain name, ports
80/443open, anACME_EMAILfor Let's Encrypt.
Trackarr publishes signed multi-arch images to GHCR — no need to build anything locally:
| Image | Role |
|---|---|
ghcr.io/dim145/opentracker/api:latest |
Nitro API |
ghcr.io/dim145/opentracker/front-ssr:latest |
Nuxt SSR web |
ghcr.io/dim145/opentracker/front:latest |
Static SPA (overlay) |
ghcr.io/dim145/opentracker/tracker:latest |
Go BitTorrent tracker |
Full list & tags: https://github.com/Dim145?tab=packages&repo_name=opentracker.
docker-compose.prod.yml pulls these by default; pin a specific tag with IMAGE_TAG=v0.17.0.
git clone https://github.com/Dim145/opentracker.git /opt/trackarr
cd /opt/trackarr
cp .env.example .env
cat >> .env <<EOF
NODE_ENV=production
DOMAIN=your-domain.com
TRACKER_DOMAIN=tracker.your-domain.com
ACME_EMAIL=admin@your-domain.com
NUXT_SESSION_SECRET=$(openssl rand -hex 32)
ADMIN_API_KEY=$(openssl rand -hex 32)
IP_HASH_SECRET=$(openssl rand -hex 32)
CHANNEL_ENCRYPTION_KEY=$(openssl rand -hex 32)
DB_PASSWORD=$(openssl rand -hex 32)
REDIS_PASSWORD=$(openssl rand -hex 32)
NUXT_PUBLIC_TRACKER_HTTP_URL=https://tracker.your-domain.com/announce
NUXT_PUBLIC_TRACKER_UDP_URL=udp://tracker.your-domain.com:6969/announce
EOF
docker compose -f docker-compose.prod.yml up -dPoint your-domain.com + tracker.your-domain.com at the VPS IP, then open
https://your-domain.com — the first user to register becomes the admin
and is prompted to set a panic password.
Updates are a git pull && docker compose -f docker-compose.prod.yml pull && up -d
(or pin IMAGE_TAG to a release). Volumes (postgres_data, redis_data,
uploads_data, caddy_data) survive rebuilds.
Full walk-through, env reference, and operations: doc/guide/getting-started.md.
A second image — ghcr.io/dim145/opentracker/front — serves a fully static SPA
from distroless Chainguard nginx:
| SSR (default) | Static | |
|---|---|---|
| Image size | 254 MB | 28 MB |
| Idle RSS | ~120 MB | ~7 MB |
| Cold start | ~2 s | <100 ms |
| Base image | distroless/nodejs24 |
chainguard/nginx |
| First paint | server-rendered HTML | SPA shell, hydrates client-side |
docker compose \
-f docker-compose.prod.yml \
-f docker-compose.static.yml \
--env-file .env \
up -dThe static bundle fetches GET /api/runtime-config on boot and patches
useRuntimeConfig().public, so the same image is portable across domains —
only the API container needs the NUXT_PUBLIC_TRACKER_*_URL vars.
A Helm chart lives in deploy/helm/trackarr and deploys
web, api and tracker, bringing its own Postgres and cache or using yours.
helm dependency build deploy/helm/trackarr
helm upgrade --install trackarr deploy/helm/trackarr -n trackarr --create-namespace \
-f deploy/helm/trackarr/values-production.yaml \
--set site.host=tracker.yourdomain.exampleDependencies are the projects' own charts rather than Bitnami's:
CloudNativePG for Postgres — its
operator-managed PgBouncer follows the primary across a failover on its own —
and the Valkey project's chart for the cache,
since Redis OSS has no official chart and Valkey is protocol-compatible. Both are
optional; point externalDatabase / externalRedis at your own instead.
The chart's README covers the parts that bite:
the ingress must set X-Forwarded-For itself (both the API and the tracker run
with TRUST_PROXY=true), uploads need ReadWriteMany because there is no S3
client yet, and it includes a throwaway k3s-in-Docker procedure for testing a
change end to end.
| Layer | Technology | Notes |
|---|---|---|
| Frontend | Nuxt 4, Vue 3, Tailwind CSS, Tiptap | SSR by default, opt-in static SPA build |
| API | Nitro 4 (Node 24), Drizzle ORM, Zod | Standalone container, distroless runtime |
| Tracker | Go 1.25, sqlc | scratch-based image, sub-ms announce p99 |
| Database | PostgreSQL 18 | gin_trgm_ops full-text, migrations at API boot |
| Connection pool | PgBouncer | Transaction-mode pooling |
| Cache / queue | Redis 8 | Peer hashes, sessions, rate-limit windows, pub/sub |
| Reverse proxy | Caddy 2 | Auto-HTTPS, HTTP/3 |
| Crypto | Web Crypto API, scrypt, AES-256-GCM | ZKE auth, Panic encryption |
| Observability | Prometheus /metrics |
Dedicated port on the API container |
| Monorepo | pnpm workspaces | packages/{shared,db} + apps/{web,api,tracker} |
Security deep-dive: doc/guide/security.md, doc/guide/zero-knowledge-auth.md, doc/guide/panic-mode.md.
Catalogue — a bar that turns what you type into filters, works grouped before releases, and a rail of counted facets. A card unfolds into its seasons and episodes without leaving the page.
Release page — what the release is, above the fold: metadata pulled from TMDb / IMDb / IGDB / Open Library against the id stored on the torrent, the decision pinned in its own column (what the download costs, how long you commit to seeding), swarm health, and the versions of the same work side by side.
Forum — sections, threads and cross-instance federated threads.
Profile — ratio, bonus points, and the announce URL carrying your passkey.
Admin dashboard — live swarm vital signs, with growth, peers, Redis and database size plotted over the retained history.
Moderation queue — every upload that isn't accepted yet, with its conversation thread.
Federation — link the tracker to instances you trust, and pick exactly what each link shares.
- Fork the repository
- Create a feature branch (
git checkout -b feature/amazing) - Commit your changes (
git commit -m 'Add amazing feature') - Push to the branch (
git push origin feature/amazing) - Open a Pull Request
The repo is a pnpm monorepo. For local hacking, see Getting Started — Local development; for running the full container stack on your laptop, see Local Production.
Trackarr is built on the shoulders of giants. Thanks to the following open-source projects:
| Project | Role |
|---|---|
| Nuxt | Fullstack Vue framework |
| Vue.js | Reactive frontend framework |
| Nitro | Universal JS server engine |
| Drizzle ORM | TypeScript ORM |
| sqlc | Go DB codegen for the tracker |
| PostgreSQL | Database |
| Redis | In-memory cache |
| ioredis | Redis client for Node.js |
| Caddy | Reverse proxy + HTTPS |
| Chainguard | Distroless container images |
| Tailwind CSS | Utility-first CSS |
| Tiptap | WYSIWYG editor |
| Chart.js | Charts & visualizations |
| Iconify | Icon framework (Phosphor set) |
| VitePress | Documentation framework |
| Pinia | State management |
| Zod | Schema validation |
| TMDb | Films + TV metadata |
| IGDB | Video-game metadata |
| Open Library | Books + ebook metadata |
| Google Books API | Books metadata (fallback) |
| web-push | RFC 8291 / 8292 push delivery |
Thanks to all our contributors! Sorted by number of commits.
| Avatar | Contributor | Commits |
|---|---|---|
| Dim145 | 4 | |
| IkiaeM | 4 |
MIT License — see LICENSE for details.
Built with ❤️ for the P2P community