# unarr **Your self-hosted media agent in a single binary.** Organize your library, stream and transcode to any device on your network, and connect your debrid account — all from one headless daemon with a web dashboard, WireGuard split-tunnel, and Cloudflare Funnel remote access. **[Website & docs](https://unarr.app)** · **[Install guide](https://unarr.app/cli)** · **[Get an API key](https://unarr.app)** > Pairs with the **[unarr.app](https://unarr.app)** web app: rich metadata from > TMDB, a 0–100 quality score per release, and one-tap play to your TV, phone, or > browser. --- ## Quick start ### 1. First-time setup (interactive wizard) ```bash docker run -it --rm \ -v ~/.config/unarr:/config \ unarr/cli setup ``` The wizard asks for your unarr API key (free at [unarr.app](https://unarr.app), under **Profile → API keys**) and your media directory. ### 2. Run the daemon ```bash docker run -d --name unarr \ --restart unless-stopped \ --network host \ -v ~/.config/unarr:/config \ -v ~/Media:/downloads \ -v unarr-data:/data \ unarr/cli ``` That's it — `unarr` now runs headless, ready to stream and manage your library. `--network host` lets it reach your TV, phone, and Chromecast on the LAN. --- ## Docker Compose ```yaml services: unarr: image: unarr/cli:latest pull_policy: always container_name: unarr restart: unless-stopped network_mode: host # recommended — reaches devices on your LAN environment: - TZ=UTC - PUID=1000 # your host user id — see PUID/PGID below (NAS ≠ 1000) - PGID=1000 # - UNARR_API_KEY=your_key_here volumes: - ~/.config/unarr:/config - ~/Media:/downloads - unarr-data:/data volumes: unarr-data: ``` ```bash docker compose run --rm unarr setup # one-time wizard docker compose up -d # start the daemon ``` --- ## Volumes | Path | Purpose | |--------------|-------------------------------------------| | `/config` | Configuration file (`config.toml`) | | `/downloads` | Your media library | | `/data` | Internal state & cache | ## Environment variables | Variable | Description | Default | |----------------------|------------------------------|---------------------| | `UNARR_API_KEY` | unarr API key | from config | | `UNARR_API_URL` | API endpoint | `https://unarr.app` | | `UNARR_DOWNLOAD_DIR` | Media directory | `/downloads` | | `UNARR_CONFIG_DIR` | Config directory | `/config` | | `UNARR_COUNTRY` | Country code (ISO 3166) | `US` | | `TZ` | Timezone | `UTC` | | `PUID` | User id the agent runs as | `1000` | | `PGID` | Group id the agent runs as | `1000` | Any config value can be overridden by its matching `UNARR_*` environment variable. ### PUID / PGID (NAS users: read this) The container runs as `1000:1000` unless you say otherwise, which matches a typical Linux desktop but **not** a NAS. If `/config` or `/downloads` is owned by a different user on the host, the agent gets *permission denied* on its first write. Pass your own ids: ```bash docker run -d --name unarr \ -e PUID=1026 -e PGID=100 \ -v /volume1/docker/unarr:/config \ -v /volume1/media:/downloads \ unarr/cli ``` | Platform | How to find your ids | |-------------|-------------------------------------------------------| | Synology | SSH in, run `id ` (uids usually start 1024) | | unRAID | `99` / `100` (the standard `nobody:users`) | | QNAP | SSH in, run `id admin` | | Linux/macOS | `id -u` / `id -g` (often already `1000`) | **What the entrypoint actually does with the mounts.** It starts as root, then: - `/config`, `/data` and the home directory are chowned **recursively** to `PUID:PGID` — they are small and entirely owned by the agent. - `/downloads` gets **only its mount point** chowned. Your library can be terabytes and a recursive chown on every start would stall the container for minutes. - It then drops to `PUID:PGID` and everything unarr **creates** from that point is owned by `PUID:PGID`. So **files that were already in your library keep their original owner**. If they belong to a different uid, unarr can still fail to rename, move or delete *those* files. That is fixed once, on the host, not by the container: ```bash chown -R 1026:100 /volume1/media # your PUID:PGID ``` **Running as root** is opt-in: set `PUID=0` explicitly. Anything else keeps the privilege drop, and if privileges cannot be dropped the container refuses to start rather than writing root-owned files onto your shares. **Kubernetes.** The image has no baked `USER`, so a pod with `runAsNonRoot: true` must also set `runAsUser` (and `runAsGroup`): ```yaml securityContext: runAsNonRoot: true runAsUser: 1026 runAsGroup: 100 supplementalGroups: [100] ``` With `runAsUser` set, the container is already unprivileged: the entrypoint skips the chown and the privilege drop entirely and execs the agent directly, so the mount permissions must be right on the host (or via `fsGroup`). **Supplementary groups.** The privilege drop keeps every group the runtime granted with `--group-add` / `group_add` (e.g. a NAS "media" gid a share depends on), and also adds the owner group of the GPU nodes under `/dev/dri`, so hardware transcode works with just `--device /dev/dri`. ## Networking **Host mode (recommended)** — `--network host` / `network_mode: host`. Lets the agent reach your TV, phone, and Chromecast directly on the LAN for local streaming, with no port mapping. **Bridge mode** — more isolated; map the agent's stream/control ports yourself: ```yaml ports: - "11818:11818" - "11819:11819" ``` ## Hardware transcode The image ships the NVIDIA runtime env, so GPU transcode works out of the box: - **NVIDIA:** add `--gpus all` - **Intel QSV / VA-API:** pass `--device /dev/dri` (the agent joins the device's render group on its own) If the hardware encoder is present but can't be opened, the agent logs `test encode failed - not using it` at startup and transcodes in software. ## Reconnecting after removing the agent from the dashboard Deleting the agent under **Profile → Agents** revokes this container's credential for good: the daemon logs `This agent was removed from your account` (`agent_revoked`), forgets the dead credential, records the delete in `/config/revoked.json`, and stays up doing nothing. **A plain restart keeps it disconnected** — a NAS reboot or an image update must not undo a delete you made on purpose. There is no `unarr login` in a container; reconnecting is one environment variable plus a restart: - **Provisioned with `UNARR_API_KEY`:** add `UNARR_RECONNECT=1` to the container's environment (Docker GUI, compose file, or `docker run -e`) and restart. It registers as a new machine under a new name. Remove the variable afterwards: while it is set, the next restart reconnects again after any future delete. - **Provisioned with `UNARR_AUTHKEY`:** auth-keys are single-use, so the one in the container is spent. Generate a new one under **Profile → Agents**, replace `UNARR_AUTHKEY`, and restart. A fresh key counts as a deliberate reconnect on its own — no `UNARR_RECONNECT` needed. With the old key the container restarts into `auth-key already used`. - Deleting `/config/revoked.json` by hand does the same as `UNARR_RECONNECT=1`. ## Running commands Use `docker exec` for one-off commands while the daemon is running: ```bash docker exec unarr unarr status docker exec unarr unarr doctor # diagnose config / connectivity docker exec unarr unarr support-bundle # one redacted file to attach to an issue ``` --- ## Health check The image ships a `HEALTHCHECK`, so a container whose daemon has died shows as `unhealthy` instead of `running`: ```bash docker ps # STATUS column shows (healthy) docker inspect --format '{{.State.Health.Status}}' unarr docker compose up -d --wait # blocks until the daemon is really up ``` It runs `unarr doctor --quick`, which checks only local things — config readable, download dir writable, disk space, and whether the daemon process is actually alive — and **makes no network call**. That is deliberate: a probe that reached the API would flip the container unhealthy on any internet blip, and Docker's response to unhealthy is to restart. Warnings never mark the container unhealthy for the same reason. Run it yourself to see what it sees: ```bash docker exec unarr unarr doctor --quick docker exec unarr unarr doctor --quick --json # exit 1 when unhealthy ``` --- ## Tags | Tag | Description | |------------|----------------------------------------------| | `latest` | Latest release | | `X.Y.Z` | Exact version (e.g. `1.3.0-beta`) | | `X.Y` | Latest patch within a minor (e.g. `1.3`) | Pin a tag in production (`unarr/cli:1.3`) for reproducible deploys. ## Supported architectures Multi-arch image — Docker pulls the right one automatically: - `linux/amd64` - `linux/arm64` (Apple Silicon, Raspberry Pi 4/5, ARM servers) ## Image details - **User:** starts as root only to prepare the mounts, then drops to `PUID:PGID` (default `1000:1000`) — the agent itself runs as **non-root**. Kubernetes `runAsNonRoot` needs an explicit `runAsUser` (see PUID/PGID above) - **Entrypoint:** `unarr start` (daemon mode) - **Bundled `ffmpeg` / `ffprobe`** for transcode & inspection — nothing else to install - **Signed releases** — binaries are published as **[GitHub Releases](https://github.com/Unarr-app/unarr-cli/releases)**; `checksums.txt` is ed25519-signed and the self-updater verifies it before applying --- ## Other install methods Not using Docker? Install the native binary instead: ```bash # Linux / macOS curl -fsSL https://unarr.app/install.sh | sh # macOS (Homebrew) brew install unarr-app/tap/unarr # Windows (PowerShell) irm https://unarr.app/install.ps1 | iex # Go toolchain go install github.com/Unarr-app/unarr-cli/cmd/unarr@latest ``` ## Links - **Website & docs:** https://unarr.app - **CLI install guide:** https://unarr.app/cli - **Source:** https://github.com/Unarr-app/unarr-cli - **Releases:** https://github.com/Unarr-app/unarr-cli/releases ## License MIT.