Skip to content

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

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

yml
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
jsonc
{
  // 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" }
    //   }
    // }
  ]
}
ini
# 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=5080

Or, if you'd rather not copy-paste, fetch them:

bash
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.jsonc

2. 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:

jsonc
{
  "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:

jsonc
{
  "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:

jsonc
{
  "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:

    bash
    openssl rand -base64 32
  • RADARR_API_KEY / SONARR_API_KEY — the *arr API keys your config.jsonc references (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:

MountPurpose
./config/configConfig, database, and logs
${MEDIA_PATH:-./data/media}/data/mediaYour media, so jack can stream it to peers
${TORRENTS_PATH:-./data/torrents}/data/torrentsDownload 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:

yaml
volumes:
  - /srv/media/movies:/movies
  - /srv/media/tv:/tv

Download 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:

yaml
# jack (already in the compose file)
volumes:
  - ./data/torrents:/data/torrents
yaml
# radarr AND sonarr
volumes:
  - ./data/torrents:/data/torrents

Use 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

bash
docker compose up -d
docker compose logs -f jack

In the logs you should see:

  • Server listening — jack is up.
  • Registered Jack as Torznab indexer and Registered Jack as qBittorrent download client — once per destination server.

Verify from the outside:

bash
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:

  1. 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 — not jack.internalUrl).
  2. 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

Released under the GPL-3.0 License.