Deployment Guide¶
End-to-end deployment of the BitAgent stack: the Go DHT crawler, the FastAPI dashboard, Postgres for the metadata index, and a reverse proxy for TLS / auth.
Architecture¶
┌──────────────────────┐
│ DHT swarm │
│ (BEP-5 UDP) │
└─────────────┬────────┘
│ UDP/4413
▼
┌──────────────────────┐ GraphQL + Torznab
│ bitagent (Go) │◄──────────────────────┐
│ - DHT crawler │ │
│ - classifier │ ┌───────────────────┴───────────┐
│ - Torznab adapter │ │ Sonarr / Radarr / Lidarr │
│ - GraphQL + metrics │ │ Readarr / Prowlarr │
└────┬─────────────────┘ └───────────┬───────────────────┘
│ TCP/3333 │ webhook POST
▼ ▼
┌──────────────┐ ┌────────────────────┐
│ Postgres 16 │ │ bitagent-ui │
│ (metadata) │ │ - dashboard │
└──────────────┘ │ - /api/evidence │
│ - SQLite sidecar │
└────┬───────────────┘
│ TCP/8080 (or APP_PORT)
▼
operator browser
(via Caddy / NPM / Authelia)
Prerequisites¶
- Docker 24+ and Docker Compose v2.
- 2 GiB RAM headroom for Postgres + the crawler at steady state. 4 GiB if you want headroom for the LLM-rerank classifier path.
- UDP/4413 inbound reachable from the public internet — closed ports cap DHT yield at ~20% of full rate.
- Postgres 16+ (the bundled compose service is fine; bring your own only if you have an existing cluster).
- A domain if you're going public — the
compose.public.ymlflow assumes you have DNS pointed at the Docker host and Caddy handles automatic TLS.
Reference compose layouts¶
Three canonical shapes. Pick the one that matches your network topology.
compose.tailnet.yml — private network or LAN-only¶
No TLS, REQUIRE_AUTH=false. Safe only when the dashboard is unreachable from the public internet.
services:
bitagent:
image: ghcr.io/bitagent-dev/bitagent:latest # pin a SHA in production
container_name: bitagent
ports:
- "3333:3333/tcp"
- "4413:4413/udp"
environment:
DATABASE_URL: postgresql://bitagent:bitagent@postgres:5432/bitagent
BITAGENT_DHT_BIND_ADDR: ":4413"
BITAGENT_LOG_LEVEL: info
TORZNAB_API_KEY: "" # empty = open Torznab; fine on tailnet
volumes:
- bitagent_core_data:/data
depends_on:
postgres:
condition: service_healthy
restart: unless-stopped
bitagent-ui:
image: ghcr.io/bitagent-dev/bitagent-ui:latest # pin a SHA in production
container_name: bitagent-ui
ports:
- "8080:8080"
environment:
BITAGENT_GRAPHQL_URL: http://bitagent:3333/graphql
BITAGENT_METRICS_URL: http://bitagent:3333/metrics
REQUIRE_AUTH: "false"
TMDB_API_KEY: "" # set if you want poster art
LOG_LEVEL: info
volumes:
- bitagent_ui_data:/data
depends_on:
- bitagent
restart: unless-stopped
postgres:
image: postgres:16-alpine
container_name: bitagent-postgres
environment:
POSTGRES_USER: bitagent
POSTGRES_PASSWORD: bitagent
POSTGRES_DB: bitagent
volumes:
- bitagent_postgres_data:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U bitagent -d bitagent"]
interval: 10s
timeout: 5s
retries: 6
restart: unless-stopped
volumes:
bitagent_core_data:
bitagent_postgres_data:
bitagent_ui_data:
compose.public.yml — public internet, Caddy auto-TLS, API-key auth¶
REQUIRE_AUTH=true, both dashboard and Torznab keys set, dashboard reachable at a real domain. Caddy terminates TLS via Let's Encrypt.
services:
bitagent:
image: ghcr.io/bitagent-dev/bitagent:latest
container_name: bitagent
ports:
- "4413:4413/udp"
# 3333 is internal only — Caddy proxies it
environment:
DATABASE_URL: postgresql://bitagent:${POSTGRES_PASSWORD}@postgres:5432/bitagent
TORZNAB_API_KEY: ${TORZNAB_API_KEY}
BITAGENT_LOG_LEVEL: info
volumes:
- bitagent_core_data:/data
depends_on:
postgres:
condition: service_healthy
restart: unless-stopped
bitagent-ui:
image: ghcr.io/bitagent-dev/bitagent-ui:latest
container_name: bitagent-ui
environment:
BITAGENT_GRAPHQL_URL: http://bitagent:3333/graphql
BITAGENT_METRICS_URL: http://bitagent:3333/metrics
REQUIRE_AUTH: "true"
DASHBOARD_API_KEY: ${DASHBOARD_API_KEY}
TORZNAB_API_KEY: ${TORZNAB_API_KEY}
TMDB_API_KEY: ${TMDB_API_KEY}
LOG_LEVEL: info
volumes:
- bitagent_ui_data:/data
depends_on:
- bitagent
restart: unless-stopped
postgres:
image: postgres:16-alpine
container_name: bitagent-postgres
environment:
POSTGRES_USER: bitagent
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
POSTGRES_DB: bitagent
volumes:
- bitagent_postgres_data:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U bitagent -d bitagent"]
interval: 10s
timeout: 5s
retries: 6
restart: unless-stopped
caddy:
image: caddy:2-alpine
container_name: bitagent-caddy
ports:
- "80:80"
- "443:443"
volumes:
- ./Caddyfile:/etc/caddy/Caddyfile:ro
- caddy_data:/data
- caddy_config:/config
restart: unless-stopped
volumes:
bitagent_core_data:
bitagent_postgres_data:
bitagent_ui_data:
caddy_data:
caddy_config:
Minimal Caddyfile next to it:
bitagent.example.com {
reverse_proxy bitagent-ui:8080
}
.env (next to compose, in .gitignore):
DASHBOARD_API_KEY=<openssl rand -hex 32>
TORZNAB_API_KEY=<openssl rand -hex 32, DIFFERENT KEY>
POSTGRES_PASSWORD=<openssl rand -hex 16>
TMDB_API_KEY=<your TMDB v3 key, optional>
compose.authelia.yml — public internet, SSO via Authelia¶
Same as compose.public.yml but Authelia (or oauth2-proxy / Cloudflare Access) handles the front-door auth, and the dashboard trusts the X-Forwarded-User header instead of validating an API key for browser sessions.
Diff against compose.public.yml:
bitagent-ui:
environment:
REQUIRE_AUTH: "true"
DASHBOARD_API_KEY: ${DASHBOARD_API_KEY} # still needed for /api/* scripted access
TORZNAB_API_KEY: ${TORZNAB_API_KEY}
TRUST_FORWARDED_USER: "true" # ← new
TMDB_API_KEY: ${TMDB_API_KEY}
The Caddyfile becomes:
bitagent.example.com {
forward_auth authelia:9091 {
uri /api/verify?rd=https://auth.example.com/
copy_headers Remote-User Remote-Groups Remote-Name Remote-Email
}
reverse_proxy bitagent-ui:8080
}
Authelia must inject Remote-User (or rename via Caddy's header_up X-Forwarded-User {http.reverse_proxy.header.Remote-User}).
Generating keys¶
Two distinct keys, never reuse one for the other:
# Dashboard browser/scripted auth
openssl rand -hex 32 # → DASHBOARD_API_KEY
# Torznab API for *arr
openssl rand -hex 32 # → TORZNAB_API_KEY (must differ from above)
Rotate by:
- Generate new key.
- Update
.env. docker compose up -d(the changed env triggers a recreate).- Update
*arrTorznab indexer config (or scripted callers) with the new key.
First-run checklist¶
docker compose up -d
docker compose logs -f bitagent # watch for "DHT bootstrap complete"
Within ~3 minutes the bitagent core should log DHT bootstrap. Then:
- Open
https://bitagent.example.com(orhttp://localhost:8080for tailnet). - Authenticate (API key, Authelia, or open access depending on layout).
- Wants tab — add a few targets so the classifier has bias from day one (see Wants Guide).
- Sonarr → Settings → Indexers → Add → Torznab Custom — URL
https://bitagent.example.com/torznab(orhttp://bitagent:3333/torznabif same Docker network), API Key =TORZNAB_API_KEY. Repeat for Radarr / Lidarr / Readarr. - Sonarr/Radarr → Settings → Connect → Add → Webhook — URL
https://bitagent.example.com/api/evidence, POST, triggers On Grab + On Import. Repeat for each*arr. See Evidence Pipeline. - Wait ~30 minutes for the classifier to admit the first wave.
Persistence and backups¶
Three named volumes own all persistent state:
| Volume | Holds | Lose it = |
|---|---|---|
bitagent_core_data |
DHT routing-table snapshot | Slower bootstrap; full DHT rediscovery in ~5 min |
bitagent_postgres_data |
Indexed metadata (millions of torrent rows) | Full re-crawl, days of yield |
bitagent_ui_data |
Wants, evidence log, settings overrides, audit log, poster cache | Operator profile + classifier feedback |
Back up bitagent_postgres_data and bitagent_ui_data. The core's data volume is recoverable from the public DHT.
pg_dump is fine for Postgres; for SQLite use sqlite3 .backup or just snapshot the volume — the dashboard pauses writes briefly during shutdown.
Updating¶
CI publishes a new image on every commit to main, tagged with the short SHA and :main. Pin the SHA, not :main:
image: ghcr.io/bitagent-dev/bitagent-ui:b06dd447
To upgrade:
- Bump the SHA in compose.
docker compose up -d— only affected services recreate.- Volumes survive the recreate; the dashboard re-reads the same
bitagent-ui.db.
If the new version requires an env var or compose change, the release notes will say so. Don't blind-bump across major versions.
Common pitfalls¶
- Closed UDP/4413 — the single biggest cause of poor crawl yield.
- Reusing one key for both endpoints — different threat models, different rotation cadences. Don't.
- Public internet with
REQUIRE_AUTH=false— your dashboard becomes a free Torznab provider for anyone who finds it, plus an open settings-mutation surface. Don't. TMDB_API_KEYunset — Library posters silently fall back to text. Not a bug, but ugly. Free tier is fine.- Auto-update points to
:mainor:latest— the docker daemon will sometimes serve stale digests. Pin the SHA. - Stack mounting
/dataon bind-mounts owned by root — the dashboard's container user isappuser(uid 10001). Use a named volume, orchown -R 10001 /your/host/pathfirst.