Tunarr: a CalVer tag scheme, a 1.2.0 package version and a container on 0.0.0.0
Create a classic TV experience using your own media - IPTV backed by Plex/Jellyfin/Emby/NFO
At a glance
- What is it?
- Tunarr turns media libraries into live TV channels that Plex, Jellyfin or Emby treat as a network tuner. The packaging tells the sharper story: date-based release tags against a 1.2.0-dev.1 package version, an image built on another project's ffmpeg base, and a default bind address of every interface.
- Who is it for?
- Use Tunarr when you already run Plex, Jellyfin or Emby with local media and want scheduled channels out of it, and skip it if your setup depends on a hosted tuner or on broadcast metadata you would have to source elsewhere. Before you deploy, settle four things.
- Can I use it commercially?
- Yes. Zlib is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
- Is it still maintained?
- Yes. The repository received new commits within the last day.
- What is it written in?
- Mainly TypeScript, according to GitHub's language statistics.
Answers come from the project's GitHub data, last synced on October 6, 2026, and from our analysis. They are not legal advice.
Editorial analysis
The tuner is a device the server is asked to trust, not a patch against it
The mechanism is narrower than the feature list suggests. Tunarr presents itself as an HDHomeRun tuner on the network, and Plex, Jellyfin or Emby is configured to add it as a tuner device; anything the server then knows how to play arrives through that device. Alternatively you take the M3U URL and hand it to an IPTV player such as Tivimate or UHF, or to Dispatcharr, Threadfin or xTeVe, and the channels play there instead. Nothing in the repository patches the media server, changes its licence state or intercepts its traffic, and no step here depends on defeating a check. What the project cannot answer is whether your server's own terms and paid tier cover live TV from a third party device, so that question belongs to the server you are attaching to, not to this code. Everything below is packaging and configuration.
Release tags are dated, while package.json still says 1.2.0-dev.1
Two version schemes are running at once. The releases are tagged by date, v2026.9.3 on 2026-09-29, v2026.9.2 on 2026-09-28 and v2026.9.1 on 2026-09-26, while `package.json` declares `"version": "1.2.0-dev.1"` for the workspace root. The last push to the default branch is dated 2026-10-03, days after the newest tag. Nothing reconciles the two, so a bug report that names one of them tells you nothing about the other, and the version string a running container reports is not the tag you pulled. The same file also pins the toolchain twice over, `"engines": {"node": "22"}` alongside `"packageManager": "[email protected]"`, and its `preinstall` hook runs `npx only-allow pnpm` so an npm install fails on purpose.
The official image is another project's ffmpeg build, pinned at tag 7.1.1
The Dockerfile does not install ffmpeg. Its first two lines are build arguments: `ARG base_image=ghcr.io/ersatztv/ersatztv-ffmpeg` and `ARG base_image_tag=7.1.1`, followed by `FROM ${base_image}:${base_image_tag} AS ffmpeg-base`. So the ffmpeg and ffprobe binaries every transcode profile depends on come from the ErsatzTV image, and the two symlinks that put them on the default path are created afterwards. Pinning a tag is the right habit, but it also means the ffmpeg version in your channel lineup is decided in a different repository, and an ffmpeg upgrade arrives when that base tag moves. Transcoding is where this matters most, since the advertised hardware paths, Nvidia NVENC, VAAPI, Intel QuickSync and macOS VideoToolbox, all sit behind those binaries.
The container binds every interface and opens the discovery port the README never mentions
Three lines set the network posture. `ENV TUNARR_BIND_ADDR=0.0.0.0` binds all interfaces rather than loopback, `EXPOSE 8000` publishes the web interface, and `EXPOSE 1900/udp` opens the SSDP port used for tuner discovery. The README's compose example maps `8000:8000` and then says Tunarr will be available at `http://localhost:8000`, which is true from the machine itself and quiet about everyone else:
services:
tunarr:
image: chrisbenincasa/tunarr:latest
container_name: tunarr
ports:
- 8000:8000
environment:
- TZ=America/New_York
volumes:
- ./tunarr-data:/config/tunarr
restart: unless-stoppedOn a laptop that is harmless; on a host with a routable address, or a NAS with port forwarding, the media server's control panel and its channel configuration are on the network unless you put something in front. The compose file also sets `TZ` and mounts a relative `./tunarr-data` path, so the data directory location depends on where you run the command from.
The build hardcodes x86_64 musl paths while an arm64 image is advertised
Inside the dependency step the Dockerfile deletes dpkg's records for libc, reinstalls the package, installs musl-dev and then runs `ln -s /usr/lib/x86_64-linux-musl/libc.so /lib/libc.musl-x86_64.so.1`. That symlink target is an x86_64 path written into the build. The installation table in the README lists ARM, Raspberry Pi and similar, with a Docker image for `linux/arm64`. The same instructions cannot be reused verbatim on that architecture, which means the arm64 image is built by a different path than the one shown here, and a change to this file does not automatically reach it. The comment above the Node install says as much in a different register: node is still present for some dev tools, for now.
Two build steps pipe a remote script straight into the image
One step imports the NodeSource signing key with `curl -fsSL https://deb.nodesource.com/gpgkey/nodesource-repo.gpg.key | gpg --dearmor`, and another runs `curl -sfS https://dotenvx.sh | sh`. Both fetch whatever that URL serves at build time with no version, checksum or commit pinned, so two builds of the same source can differ, and a compromised endpoint would land inside the image rather than fail the build. A third step installs `corepack@latest`, which is unpinned even though `package.json` pins `[email protected]`, so the package manager that installs the workspace is itself floating. The runtime entrypoint is dotenvx, configured as `ENTRYPOINT [ "dotenvx", "run", "--", "/tunarr/tunarr" ]` with `CMD [ "server" ]`, which is how environment files get into the process.
patches/ holds dependency patches, and some versions live outside package.json
The build copies a `patches` directory alongside `server`, `shared`, `web` and `types`, which is the pnpm convention for carrying modifications to dependencies that have no upstream release. Nothing in the repository explains which packages are patched or why, so a build that behaves differently from a clean install of the declared versions has an explanation somewhere in that directory rather than in the lockfile. Version resolution is split as well. `@types/node` is pinned to an exact 22.10.7, while `typescript` and `vitest` are declared as `catalog:` and `catalog:vitest`, which means their versions come from the pnpm workspace catalog instead of this file. Contributors reading package.json alone will not find the full dependency picture.
Five badge anchors and a screenshots table that contains no screenshots
The header of the README is five anchors pointing at the releases page, the Docker Hub repository, the stargazers list, the Discord invite and the LICENSE file. None of them wraps an image or any text, so the row renders as five empty links. Below the feature list, a Screenshots heading introduces a two cell table whose cells contain the captions Channel Management and Channel Configuration and nothing else. Both are the kind of thing that survives from a template or a conversion and is easy to miss when you are reading the page for install instructions. The documentation itself is not affected: installation, channel creation, scheduling and transcoding each have their own page under tunarr.com, and the README links to all four plus the root.
Editorial conclusion
Use Tunarr when you already run Plex, Jellyfin or Emby with local media and want scheduled channels out of it, and skip it if your setup depends on a hosted tuner or on broadcast metadata you would have to source elsewhere. Before you deploy, settle four things. Read the terms of the server you plan to point at it, because the repository answers no licensing question and the decision is yours: Tunarr asks your media server to treat it as a network tuner, and nothing in this project patches or unlocks that server. Put the container behind a reverse proxy or a firewall rather than mapping 8000 to a shared host, since TUNARR_BIND_ADDR defaults to 0.0.0.0 and UDP 1900 is open for discovery. Decide whether you want a dated tag such as v2026.9.3 or the 1.2.0-dev.1 in package.json, because the two schemes do not line up. And read the Docker build before you rebuild it, since two of its steps pipe a remote script straight into the image.
Frequently asked questions
What does Tunarr do?
It builds custom live TV channels out of media libraries you already have, whether Plex, Jellyfin, Emby or local files, and streams them as broadcast style channels. You watch them by adding Tunarr's spoofed HDHomeRun tuner to Plex, Jellyfin or Emby, or by taking the M3U URL into an IPTV player such as Tivimate or UHF.
How do I install Tunarr?
With Docker Compose, using the image chrisbenincasa/tunarr:latest with port 8000 mapped and ./tunarr-data mounted at /config/tunarr, then docker compose up -d. The README also lists standalone binaries for Linux, macOS and Windows from the releases page, the Unraid Community App Store, a Proxmox LXC helper script, and a linux/arm64 Docker image.
Which media servers and IPTV clients does Tunarr connect to?
Plex, Jellyfin, Emby and local file libraries are the media sources, and the spoofed HDHomeRun tuner is added to Plex, Jellyfin or Emby. The M3U output is documented for Dispatcharr, Threadfin, xTeVe or any IPTV client, and channels can also stream directly in a browser.
Why does the Tunarr container listen on every network interface?
The Dockerfile sets TUNARR_BIND_ADDR=0.0.0.0 and exposes port 8000 along with 1900/udp for SSDP tuner discovery, while the README tells you the service is at http://localhost:8000. Mapping 8000 on a shared or reachable host therefore exposes more than the local machine.
Which version of Tunarr does the Docker image contain?
package.json declares version 1.2.0-dev.1 while the releases are tagged by date, v2026.9.3, v2026.9.2 and v2026.9.1. The two schemes are not reconciled anywhere in the repository, so the tag you pull and the version string inside it will not match.
Official sources
Add this badge to your README
If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.
[](https://hysenlabs.com/projects/chrisbenincasa-tunarr)