Getting started
This guide takes you from nothing to a running jack instance: configured against your Radarr/Sonarr, registered there as an indexer and download client, with the management UI up and ready to connect to friends. Docker Compose is the recommended way to self-host jack — two images are published to GitHub Container Registry on every push to main, so there's nothing to build.
Before you start
You'll need:
- Docker with Compose.
- Radarr and/or Sonarr already running, and their API keys (Settings → General → API Key).
- To know your media paths — the paths inside your Radarr/Sonarr containers where media lives (e.g.
/movies,/tv, or/data/media). jack streams files using those exact paths, so you'll mirror them.
If you haven't yet, skim What is jack? — the rest of this guide assumes you know what a source, destination, and peer are.
1. Create the project files
jack runs from three files: a compose file, an env file for secrets, and a config file. Create a folder with this layout:
jack/
├── docker-compose.yml
├── .env
└── config/
└── config.jsoncStart from the examples below — they're working templates, and the rest of this guide assumes them as the starting point: the next steps walk you through adapting each part to your setup.
services:
jack:
# Pre-built image published to GitHub Container Registry on every push to main.
image: ghcr.io/roziscoding/jack:main
container_name: jack
restart: unless-stopped
ports:
- '${JACK_PORT:-5225}:5225'
environment:
# production disables pino-pretty and emits structured JSON logs
ENVIRONMENT: production
LOG_LEVEL: info
PORT: 5225
APP_CONFIG_PATH: /config/config.jsonc
# Enables the management API (on MANAGEMENT_PORT, default 5226) that the
# jack-ui service talks to. The UI injects this same value as its
# X-Management-Key, so both read one variable and always match. To run jack
# headless, drop the jack-ui service below and this line.
JACK_MANAGEMENT_KEY: ${JACK_MANAGEMENT_KEY:?set JACK_MANAGEMENT_KEY in a .env file next to this compose (any long random string)}
# *arr API keys, forwarded from .env so config.jsonc can reference them
# as { "env": "RADARR_API_KEY" } instead of inlining the secret. Add one
# line per env-referenced secret; unused ones can stay empty.
RADARR_API_KEY: ${RADARR_API_KEY:-}
SONARR_API_KEY: ${SONARR_API_KEY:-}
volumes:
# App config. Drop your config.jsonc in a ./config dir next to this file
# (start from the config.jsonc template in this examples/ folder).
- ./config:/config
# Media library. jack streams files to peers using the absolute path each
# Radarr/Sonarr stores for the file (i.e. the path INSIDE the *arr
# container), so jack must see those files at that SAME path. The
# `/data/media` target below is a placeholder — change it to match your
# *arr's media path(s), and add one mount per path if they use several
# (e.g. /movies and /tv).
- ${MEDIA_PATH:-./data/media}:/data/media
# Completed downloads dir — must match `downloads.completedPath` in
# config.jsonc. IMPORTANT: mount this SAME folder into your Radarr and
# Sonarr containers at the SAME path (e.g. /data/torrents). jack writes
# finished files here and *arr resolves that literal path in its own
# filesystem to import them — if they don't match, *arr can't import.
# (No watch folder: *arr hands grabs to jack over the qBittorrent API.)
- ${TORRENTS_PATH:-./data/torrents}:/data/torrents
# If Radarr / Sonarr are defined in this same compose file, wait for them to
# be healthy before starting jack. Auto-registration only runs at startup, so
# this makes the Torznab indexer + qBittorrent client register on first boot
# instead of needing a jack restart. (Needs `healthcheck` blocks on those
# services — the linuxserver.io images ship with them.)
# depends_on:
# radarr: {condition: service_healthy}
# sonarr: {condition: service_healthy}
# If Radarr / Sonarr run in their own compose network, attach this service
# to it so `jack.internalUrl` / server URLs resolve by container name.
# Otherwise reach them via host IP.
# networks:
# - media
# Management console — a Nuxt BFF that proxies jack's management API. Published
# alongside the backend on every push to main. Open it at http://localhost:3000.
jack-ui:
image: ghcr.io/roziscoding/jack-ui:main
container_name: jack-ui
restart: unless-stopped
depends_on:
- jack
ports:
- '${JACK_UI_PORT:-3000}:3000'
environment:
# The UI reaches jack's management API over the compose network by service
# name. Must match jack's MANAGEMENT_PORT (default 5226). No host port for
# 5226 is published — the UI is the only thing that needs it.
JACK_MANAGEMENT_API_URL: http://jack:5226
# Inject mode: the BFF adds X-Management-Key on every call, so the browser is
# never prompted. Same variable as jack's JACK_MANAGEMENT_KEY above, so they match.
JACK_MANAGEMENT_KEY: ${JACK_MANAGEMENT_KEY:?set JACK_MANAGEMENT_KEY in a .env file next to this compose (any long random string)}
# Only used in cookie-prompt mode (when JACK_MANAGEMENT_KEY is unset): seals
# the session cookie and must be >= 32 chars. Left unset here because this
# compose runs in inject mode, where sessions aren't used. If you switch to
# cookie mode (remove JACK_MANAGEMENT_KEY above), uncomment this so a real
# key is required — an empty value would break cookie sealing.
# JACK_SESSION_KEY: ${JACK_SESSION_KEY:?set a >= 32-char random string for cookie mode}
# networks:
# media:
# external: true{
// Every "apiKey" below, and every value under a "headers" object, can be
// written in THREE ways:
// 1. a plain string: "apiKey": "my-secret"
// 2. a reference to an env variable: "apiKey": { "env": "MY_SECRET" }
// 3. a reference to a secret file: "apiKey": { "file": "/run/secrets/my-secret" }
// Env/file forms keep secrets out of this file (recommended for deployments).
// File paths must be absolute. All forms resolve to the same string.
// Identity of THIS jack instance. Required for the Torznab indexer.
// `internalUrl` must be reachable from your own *arr apps; peers use the
// separate URL they configure for this instance in their `peers` list.
"jack": {
"internalUrl": "http://jack:5225"
// Peers authenticate with per-peer API keys (managed in the UI's "API keys"
// section); your Radarr/Sonarr authenticate with the managed key jack
// registers for them automatically. See README → "API keys".
},
// Downloads: jack registers itself in your *arr as a qBittorrent download
// client, so grabs are handed to jack directly (no watch folder). Finished
// files are written here for your *arr to import. Path is *inside the container*.
"downloads": {
"completedPath": "/data/torrents/completed",
// Optional download hardening knobs — values below are the defaults, so you
// can delete any line to keep the default. (Note the comma after
// "completedPath" above is required once any field follows it.)
"maxConcurrentDownloads": 3,
"maxDownloadAttempts": 13,
"retryBaseDelayMs": 1000,
"retryMaxDelayMs": 1800000
},
// Your Radarr/Sonarr servers. apiKey is the *arr API key: exactly 32 hex
// chars (Settings -> General). Each server can be a:
// - "source": its library is shared with your peers (read via /peer).
// - "destination": jack registers itself there as a Torznab indexer +
// qBittorrent download client, and triggers imports.
// - both: share your library AND search your friends', symmetric.
// "autoregister" controls the indexer/client registration (destinations only):
// { "enable": true, "priority": 1 }
// The qBittorrent client is always registered at *arr's lowest priority so real
// torrents from your other indexers never get routed to jack (Jack grabs still
// reach it via the indexer binding, which *arr resolves before priority).
"servers": [
// {
// "name": "Main Radarr",
// "type": "radarr",
// "url": "http://radarr:7878",
// "apiKey": { "env": "RADARR_API_KEY" },
// "headers": { "X-Forwarded-User": "jack" },
// "source": true,
// "destination": true,
// "autoregister": { "enable": true, "priority": 1 }
// },
// { "name": "Main Sonarr", "type": "sonarr", "url": "http://sonarr:8989", "apiKey": "0123456789abcdef0123456789abcdef" }
],
// Other jack instances you search (peer-to-peer). Sources only.
// Prefer https:// — the API key travels in the X-Api-Key header, so a plain
// http:// peer exposes it in transit (jack warns at startup for http:// peers).
"peers": [
// {
// "name": "friend",
// "url": "https://their-jack.example.com",
// "apiKey": "their-shared-secret",
// "headers": {
// "CF-Access-Client-Secret": { "env": "FRIEND_CF_SECRET" },
// "CF-Access-Client-Id": { "file": "/run/secrets/friend_cf_client_id" }
// }
// }
]
}# Copy to ".env" (same folder) and fill in.
# The real .env is gitignored — never commit your actual secrets.
# Management API key shared by the backend and UI containers.
# Generate one with: openssl rand -base64 32
JACK_MANAGEMENT_KEY=replace-with-a-long-random-string
# *arr API keys (Settings → General → API Key), referenced from config.jsonc
# as { "env": "RADARR_API_KEY" } / { "env": "SONARR_API_KEY" } so the secrets
# stay out of the config file. Unused ones can stay empty.
RADARR_API_KEY=
SONARR_API_KEY=
# Optional overrides:
# JACK_PORT=5225
# JACK_UI_PORT=3000
# MEDIA_PATH=./data/media
# TORRENTS_PATH=./data/torrents
# ── Only for compose-with-otel.yml ──────────────────────────────────────────
# OpenObserve root login (also what you use to log into the UI at :5080).
O2_USER=root@example.com
O2_PASS=change-me-please
# Basic-auth header jack sends to OpenObserve's OTLP endpoint.
# Generate it from the SAME creds above:
# printf '%s:%s' "$O2_USER" "$O2_PASS" | base64 -w0
# then paste the output below, keeping the "Authorization=Basic%20" prefix
# (%20 is the space after "Basic"):
OTLP_AUTH=Authorization=Basic%20REPLACE_WITH_BASE64_OF_USER_COLON_PASS
# O2_PORT=5080Or, if you'd rather not copy-paste, fetch them:
mkdir jack && cd jack
curl -LO https://raw.githubusercontent.com/roziscoding/jack/main/examples/docker-compose.yml
curl -L -o .env https://raw.githubusercontent.com/roziscoding/jack/main/examples/.env.example
mkdir config
curl -L -o config/config.jsonc https://raw.githubusercontent.com/roziscoding/jack/main/examples/config.jsonc2. Fill in config.jsonc
Open config/config.jsonc in your editor. The template is heavily commented; these are the parts that matter:
jack.internalUrl
The address your own Radarr/Sonarr will use to reach jack — it's what jack registers as the indexer and download-client URL. If your *arr apps run on the same Docker network as jack, the default http://jack:5225 (the container name) is correct. If they run elsewhere, use the host's IP or domain:
{
"jack": {
"internalUrl": "http://jack:5225"
}
}downloads
Where jack writes finished downloads (path inside the container). Keep the default unless you have a reason not to — you'll mount a host folder here in step 4:
{
"downloads": {
"completedPath": "/data/torrents/completed"
}
}servers
One entry per Radarr/Sonarr. Set each server's URL, and reference the API keys from the environment — you'll put the actual values in .env in the next step, and the compose file forwards them into the container:
{
"servers": [
{
"name": "Main Radarr",
"type": "radarr",
"url": "http://radarr:7878",
"apiKey": { "env": "RADARR_API_KEY" }
},
{
"name": "Main Sonarr",
"type": "sonarr",
"url": "http://sonarr:8989",
"apiKey": { "env": "SONARR_API_KEY" }
}
]
}By default each server is both a source (share its library with friends) and a destination (search your friends' libraries from its UI). Set "source": false or "destination": false to opt out of either — see the configuration reference for these and the other per-server options.
INFO
You can also inline a key as a plain string ("apiKey": "abc123..."), but it's not recommended — the config file then holds live secrets. See ConfigSecret for all the forms, including reading from a secret file.
peers
Leave it empty (or delete it) for now — you'll add friends through the management UI in step 6.
3. Fill in .env
Open .env and fill in the values:
JACK_MANAGEMENT_KEY— the secret gating the management API (and the UI's access to it). Both containers read this same variable: the backend requires it on every management request, and the UI injects it, so they always match. Generate one:bashopenssl rand -base64 32RADARR_API_KEY/SONARR_API_KEY— the *arr API keys yourconfig.jsoncreferences (Settings → General → API Key).
4. Line up the mounts
The compose file mounts three host paths into jack. Two of them *must mirror your arr containers — this is the part people get wrong, so take a minute here:
| Mount | Purpose |
|---|---|
./config → /config | Config, database, and logs |
${MEDIA_PATH:-./data/media} → /data/media | Your media, so jack can stream it to peers |
${TORRENTS_PATH:-./data/torrents} → /data/torrents | Download path |
Media
jack streams files to peers using the absolute path each *arr stores for the file — the path inside the Radarr/Sonarr container. Mount your media into jack at that same path. The /data/media target in the compose file is a placeholder: if your Radarr sees movies at /movies and Sonarr sees shows at /tv, replace it with one mount per path:
volumes:
- /srv/media/movies:/movies
- /srv/media/tv:/tvDownload path
jack's downloads live under this mount — in particular, finished files are written to the literal downloads.completedPath, and your *arr imports them by resolving that same path in its own filesystem. So mount the same host folder into jack and into Radarr/Sonarr at the same container path:
# jack (already in the compose file)
volumes:
- ./data/torrents:/data/torrents# radarr AND sonarr
volumes:
- ./data/torrents:/data/torrentsUse a dedicated folder — don't point it at a folder another download client already writes to. jack runs as uid/gid 1000 (matching the linuxserver.io defaults), so make sure the folder is writable by that user.
Networking
If Radarr/Sonarr run in their own compose network, uncomment the networks: block at the bottom of the compose file so jack joins it — otherwise container names like http://radarr:7878 and http://jack:5225 won't resolve. Optionally also uncomment depends_on so jack starts after your *arr apps are healthy and registration succeeds on first boot.
5. Start it
docker compose up -d
docker compose logs -f jackIn the logs you should see:
Server listening— jack is up.Registered Jack as Torznab indexerandRegistered Jack as qBittorrent download client— once per destination server.
Verify from the outside:
curl http://localhost:5225/ping
# {"status":"OK"}(The Docker image also wires /ping up as its HEALTHCHECK, so docker ps reports container health automatically.)
Then open the management UI at http://localhost:3000. The Overview page shows your servers and whether each connector initialized cleanly. In Radarr/Sonarr, you'll find a new Jack indexer under Settings → Indexers and a Jack download client under Settings → Download Clients — both tests should pass.
If something's off, the troubleshooting guide covers the common failures.
6. Connect with a friend
jack is useful once you're peered with someone (they run jack too — send them this page). Peering is symmetric and takes one exchange in each direction:
- In the management UI, go to API keys and issue a key named after your friend. Send them that key plus the URL where your peer API is reachable from the internet (a reverse-proxied
https://address — notjack.internalUrl). - They do the same for you, and you add them under Peers in your UI with the URL and key they sent.
See API keys & peering for how the keys are scoped and why each peer gets their own.
7. Use it
That's it — from here everything happens in your normal *arr workflow. Search for a movie or episode as you always would: releases your friends have show up as Jack indexer results, and grabbing one pulls the file straight from their server into your library.
Next steps
- How it works — what actually happens on search and grab.
- Management UI — day-to-day operation and access control.
- Configuration reference — every key, including the download tuning knobs.
- Running without Docker — for bare-metal setups.