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. 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,
// Once your *arr confirms it imported a download, drop jack's copy from
// completedPath (a plain unlink: if *arr hardlinked the file, the library
// keeps it; if it copied, only jack's copy goes). Toggleable from the
// management UI, and the only key here that applies without a restart.
"unlinkImportedFiles": false
},
// 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, and 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"
}
}Worth deciding now: jack's copy of a download outlives the import. Radarr and Sonarr read the finished file out of this folder and write your library copy, but nothing cleans up jack's, so the folder grows with every grab. Add unlinkImportedFiles to have jack drop its copy once the import is confirmed:
{
"downloads": {
"completedPath": "/data/torrents/completed",
"unlinkImportedFiles": true
}
}It's off by default because it deletes files. The operation is a plain unlink of that one file: if your *arr hardlinked into the library, the library keeps the data; if it copied, you get the space back. You can also flip it later from the management UI without a restart. See After the import for the full picture, and unlinkImportedFiles for the exact guarantees.
Every other key in this block is an optional tuning knob with a sensible default, documented under downloads. All of them are editable later from the management UI, so you don't have to get them right here.
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. The configuration reference covers 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, since 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, which 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. 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, not one 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. Without that, 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 who also runs jack (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
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.