Skip to content
 
 

Repository files navigation

🌐 Trackarr

A modern, high-performance private BitTorrent tracker

Three containers — Nuxt 4 web · Nitro API · Go tracker — backed by PostgreSQL and Redis.

Node.js Nuxt Go TypeScript License

FeaturesArchitectureQuick StartStatic deploymentDocumentationLive Demo

Trackarr — tracker state and latest releases


✨ Features

Privacy & authentication

  • 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.

Browse, upload & operate

  • 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 types02e04, 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 filenameSousou.no.Frieren.S01E10.MULTi.1080p answers 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/fiche turns 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/admin covers 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/stats is 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).

Tracker protocols

  • 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; stateless connection_id = HMAC-SHA256(secret, ip ‖ minute), so no per-id memory.
  • BEP 41 URL_DATA passkeyudp://host:6969/announce/PASSKEY works as-is in every modern client.
  • Multi-tier .torrent files — generator advertises HTTP + UDP independently. TRACKER_UDP_ENABLED=false disables UDP and drops it from new .torrent files 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_v2 gives 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 truthminimumratio and minimumseedtime carry 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.

Bonus economy & resilience

  • 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_credit and invite effects.
  • 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).

Federation (inter-instance)

  • 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, swarm per 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); only ip/port/isSeeder cross the wire and ratio/HnR stay local. See Federation.

🏗️ Architecture

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 /announce every few minutes. A static Go binary on scratch (~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/web is Nuxt SSR — rendered shell + page chunks. They scale and redeploy independently.
  • Distroless everywhere. apps/web + apps/api run on gcr.io/distroless/nodejs24-debian13:nonroot; apps/tracker runs on scratch; the static frontend runs on cgr.dev/chainguard/nginx. No shells, no package managers, non-root by default.

More details: Architecture guide.


🚀 Quick Start

Prerequisites

  • Docker 20+ and Docker Compose v2
  • For production: a domain name, ports 80/443 open, an ACME_EMAIL for Let's Encrypt.

Pre-built images

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.

Production deployment

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 -d

Point 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.


🪶 Static deployment (no SSR)

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 -d

The 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.

☸️ Kubernetes

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.example

Dependencies 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.


🏗️ Tech stack

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.


📸 Screenshots

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.

Torrent list

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.

Torrent details

Forum — sections, threads and cross-instance federated threads.

Forum

Profile — ratio, bonus points, and the announce URL carrying your passkey.

User profile

Admin dashboard — live swarm vital signs, with growth, peers, Redis and database size plotted over the retained history.

Admin dashboard

Moderation queue — every upload that isn't accepted yet, with its conversation thread.

Moderation queue

Federation — link the tracker to instances you trust, and pick exactly what each link shares.

Federation


🤝 Contributing

  1. Fork the repository
  2. Create a feature branch (git checkout -b feature/amazing)
  3. Commit your changes (git commit -m 'Add amazing feature')
  4. Push to the branch (git push origin feature/amazing)
  5. 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.


🙏 Acknowledgements

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

👥 Contributors

Thanks to all our contributors! Sorted by number of commits.

Avatar Contributor Commits
Dim145 4
IkiaeM 4

📄 License

MIT License — see LICENSE for details.


Built with ❤️ for the P2P community

Back to top

About

Run your own private tracker in less than 5min

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Used by

Contributors

Languages