| FazBrowse GitHub Viewer | Trending | | Home |
| Tools: [Download Repo ZIP] [Original HTTPS Page] |
| Name | Name | Last commit date | ||
|---|---|---|---|---|
Self-hosted IPTV server for live TV and sports
Aggregates live TV channels and sports events into a single M3U playlist with full EPG guide data.
Streams are fully proxied so upstream sources are never exposed to clients.
Designed for Jellyfin, Emby, Plex, and other apps that take an M3U playlist and an XMLTV guide.
It can also answer as an HDHomeRun tuner — Jellyfin, Plex and Emby can all add it that way, and for
Plex and Emby it is the path their Live TV is built around.
The web dashboard is available in 19 languages.
🌐 New here? Follow the step-by-step setup guide at rebeliptv.com.
The following leagues currently have working live event feeds:
| League | Sport |
|---|---|
| NHL | Ice Hockey |
| NFL | American Football |
| NBA | Basketball |
| MLB | Baseball |
| MLS | Soccer (US) |
| FIFA | Soccer (World Cup / int'l) |
ESPN is the source of truth for all schedules, team names, and scores — scrapers only contribute stream URLs that are cross-referenced against ESPN's canonical game data. Other leagues will be added as upstream feeds become available. To request a new league, open a feature request.
Prefer a guided walkthrough? The setup guide at rebeliptv.com builds a Docker configuration tailored to your system (ports, timezone, in-app updates) and walks you through connecting Jellyfin / Plex. The manual setup below is the reference.
Create a docker-compose.yml:
services:
iptv:
image: rebeliptv/iptv:latest
container_name: iptv
hostname: iptv # lets clients on this compose network reach us by name
ports:
- "8080:8080"
# All environment variables are OPTIONAL — the defaults work out of
# the box. Uncomment any you need (see Configuration below):
# environment:
# TZ: "America/New_York" # set to YOUR timezone, e.g. Europe/London, America/Chicago, Etc/UTC
# HEADLESS: "true" # no browser on this box — see Headless Mode below
volumes:
- iptv-data:/app/data
- /var/run/docker.sock:/var/run/docker.sock # enables one-click in-app updates
stop_grace_period: 30s
restart: unless-stopped
volumes:
iptv-data:docker compose up -dHeads-up: the /var/run/docker.sock mount lets the container update itself from the dashboard. It also gives the container control of the Docker daemon on the host — if you'd rather not allow that, remove the line and update with the standard docker pull method below instead.
docker run -d \
--name iptv \
-p 8080:8080 \
-v iptv-data:/app/data \
-v /var/run/docker.sock:/var/run/docker.sock \
--restart unless-stopped \
rebeliptv/iptv:latest
# Environment variables are optional (defaults work). Add -e flags as needed, e.g.:
# -e TZ=America/New_York (set to YOUR timezone — e.g. Europe/London, Etc/UTC)When a newer version is available, an Update now button appears on the Dashboard and in Settings → Server — click it and the server downloads the new version and restarts itself, with your data kept and no command line needed. This works out of the box with the Compose and docker run setups above, which mount the Docker socket so the container can update itself.
The button shows only on the :latest image when a newer release exists; it's hidden on version-pinned images (e.g. :1.3.0), which keep updating with the standard docker pull method below.
If a server ever gets too far out of date to run at all, it stops serving channels — but it still answers its usual address with a page telling you so, carrying the same Update now button and the commands to run if one-click updates aren't available on your setup. Left alone for half an hour it will go ahead and update itself, since a server in that state isn't serving anything anyway.
docker pull rebeliptv/iptv:latest
docker compose up -dYour data lives in the iptv-data volume, so this just swaps in the new image — nothing is lost.
docker restart is not an update. It starts the same container again on the same old image, so the new version you just pulled is never used and the server comes back reporting that it's still out of date. docker compose up -d is what replaces the container — it notices the newer image and recreates it for you.
If nothing on the box has a browser — a Kodi/LibreELEC box, a NAS, a headless server you only reach over SSH — set HEADLESS=true:
environment:
HEADLESS: "true"The update prompt is part of the dashboard, so an instance nobody opens never gets told a new version exists, and older versions eventually stop working as sources change. In headless mode the server checks for itself and installs new versions on its own, waiting for a moment when nothing is streaming so it never interrupts a viewer. It also skips the first-run setup screen and starts building your playlist straight away, so a fresh install needs no visit to the dashboard at all.
This needs the /var/run/docker.sock mount from the setup above (the same one the Update now button uses) and the :latest image tag. Without them the server logs how to update by hand instead. Everything else — the dashboard, settings, the API — works exactly as it always did; the flag only changes what happens when nobody is watching.
Playlist and EPG endpoints can optionally be protected with an API key.
Without a key, playlist and EPG are accessible to anyone on your network.
Recommended tuner settings:
Or add it as an HDHomeRun tuner. Jellyfin takes both types — pick HDHomeRun instead of M3U Tuner at step 2 and enter <server-ip>:8080. The stream you get is identical either way. The M3U route is listed first here for one reason: your playlist URL is protected by your API key and the tuner's endpoints cannot be, so on a network you do not fully trust, M3U is the safer of the two. See Plex & Emby for the details that apply to any HDHomeRun client, and apply whichever of the tuner settings above Jellyfin offers for that tuner type.
Plex and Emby both build their Live TV around HDHomeRun network tuners, so this server can answer as one. That is the supported path for both — no Plex Pass, and no M3U-tuner flow that moves between releases. (Jellyfin can use the same tuner; see Jellyfin for why the M3U route is suggested there instead.) The rest of this section applies to any HDHomeRun client.
Turn on Act as an HDHomeRun tuner in Settings → Connect (it is on by default), then add the server by address.
Important
The tuner's endpoints are not protected by your API key. The HDHomeRun protocol has no field to carry one, so anything that can reach this server can read your channel lineup. Turn it on for a trusted or local network — not one exposed to the internet. Your playlist and guide URLs are still key-protected as normal.
Plex
Emby
Same shape: Live TV → Add tuner → HDHomeRun, enter <server-ip>:8080, then add the same EPG URL as an XMLTV guide source.
How many channels at once
Both clients ask the tuner how many streams it can serve. That number is Settings → Connect → Simultaneous streams, and it is a real limit on every way of connecting — see Simultaneous Streams.
Plex via M3U instead (not recommended)Plex does have an M3U path, but it requires an active Plex Pass, is not officially supported, and the steps have changed between Plex releases. Prefer the tuner above. If you still want it, follow Plex's own current M3U / XMLTV instructions — they are the authority on their own flow — and give it:
| Plex asks for | Give it |
|---|---|
| Tuner / M3U | http://<server-ip>:8080/playlist?key=YOUR_KEY |
| XMLTV guide | http://<server-ip>:8080/epg?key=YOUR_KEY |
| File | URL |
|---|---|
| M3U Playlist | http://<server-ip>:8080/playlist?key=YOUR_KEY |
| EPG Guide (XML) | http://<server-ip>:8080/epg?key=YOUR_KEY |
| EPG Guide (Gzip) | http://<server-ip>:8080/epg.gz?key=YOUR_KEY |
Note: The ?key= parameter is only required if an API key has been generated in the web dashboard under Settings.
Settings → Connect → Simultaneous streams sets how many channels this server will play at once.
It applies to every way of connecting — the HDHomeRun tuner, an M3U playlist, and this dashboard. Watching the same channel on several devices counts as one, because they share a single feed out to the source; it is a limit on distinct channels, not on people.
| Default | 4 |
| Range | 1 – 32 |
Ask for a channel beyond the limit and it shows a stream limit reached card rather than failing, then starts on its own as soon as another channel stops — no need to go back and pick it again.
Set it to match what the line behind your channels can actually carry. Too high and the source refuses first, which reaches you as its error rather than ours, with nothing to explain it. Too low and you meet the card sooner than you need to.
Note
Before this release the number was only advertised to Plex, Emby and Channels DVR — nothing enforced it. It is now a real limit, so a setup regularly playing more than 4 channels at once needs the number raised.
Access the dashboard at http://<server-ip>:8080.
On first launch a short setup wizard walks you through choosing a channel source, picking your content (local market, live sports on/off, which channel countries to carry), optionally adding a playlist key, optionally setting a dashboard login, and copying your playlist / guide URLs into your player. You can skip it and change anything later in Settings.
Click any channel to see:
Click any sport event to see:
Baseball and soccer games render a layout built for that sport — team comparison bars, pitching matchup, season leaders, recent form and the injury report for baseball; recent form, head-to-head, top scorers and a live match timeline for soccer. Other sports use the standard layout.
| Variable | Default | Description |
|---|---|---|
| PORT | 8080 | Server port |
| HOST | 0.0.0.0 | Bind address |
| CRON_SCHEDULE | 30 * * * * | Data refresh schedule (cron) |
| TZ | Etc/UTC | Timezone. Also picks the default local market for the pinned ABC / CBS / NBC / FOX channels — override it in Settings → Content → Local Market |
| HEADLESS | false | For boxes with no browser — see Headless Mode |
| TRUSTED_PROXIES | (none) | Only if you run behind a reverse proxy — see Reverse Proxy |
Sports options — whether to carry sports at all, sports-only mode, and which leagues to carry — are now dashboard settings under Settings → Sports, not environment variables. (An existing deployment's SPORTS_EVENTS / SPORTS_MODE / LEAGUES env values are still honoured once, as a one-time seed on upgrade; after that the dashboard owns them.)
Source mode (local scraping vs hosted feeds) is a dashboard setting, not an environment variable — see Hosted Feeds.
| Schedule | Meaning |
|---|---|
| 30 * * * * | Every hour at 30 minute mark (default) |
| */30 * * * * | Every 30 minutes |
| 0 */3 * * * | Every 3 hours |
| 0 0 * * * | Once daily at midnight |
If some providers are blocked or geo-restricted in your region, switch to Hosted Feeds (Settings → Source mode). Your instance then pulls ready-made channels, guide, and events from Rebel IPTV instead of scraping the blocked providers itself, so regional blocks no longer matter.
By default your instance builds everything itself — it scrapes the sources, assembles the playlist and guide, and proxies the streams. If you'd rather your box not run the scrapers at all, switch it to hosted feeds from the dashboard: Settings → Source mode → "Use Rebel IPTV playlist".
Your instance then pulls its channels, live sports events, and guide from Rebel IPTV's hosted service and serves them through your usual playlist, guide, and stream endpoints — so the feeds are assembled and kept healthy centrally instead of on your hardware. Nothing changes on the player side (Jellyfin/Plex/Emby keep working exactly as before); only where the feeds come from changes. In this mode there's no local scraping. Leave the toggle off to keep your instance fully self-contained.
Your channel lineup is the same either way. Channels carry the same IDs in both modes (they resolve against the same curated directory), so toggling doesn't reshuffle your lineup or break your client's channel mappings and bookmarks — only where the feeds come from changes. Switching takes effect live: your instance rebuilds its channels, guide, and events from the newly-selected source, and the toggle is disabled while that repopulates, re-enabling once it finishes.
On startup and once an hour, the server scrapes channel data, normalizes names, detects categories, and checks stream health. Offline channels are rechecked every 5 minutes.
Channel numbers are stable — every channel has a fixed number baked into the build, so your Jellyfin/Plex bookmarks survive cache purges and upgrades. Numbers are allocated in ranges:
| Range | Use |
|---|---|
| 1–4999 | Curated broadcast networks and cable channels |
| 5000+ | Live sports events (assigned per refresh) |
| 10000+ | Custom M3U sources (one range per source) |
From Settings → Channel Lineup you can disable channels you don't watch (they drop out of your playlist, guide, and counts) and drag the rest into your own order. These are per-instance preferences saved with your data and layered on top of the built-in numbers, so they persist across restarts and upgrades.
Local broadcast stations are identified by their FCC call sign. For example, "ABC (KABC) Los Angeles" becomes KABC Los Angeles CA (ABC) — call sign, market, and network — and gets a logo showing the network mark with the call sign and city. Because the city and network are in the name, you can find a local station by searching for its city (e.g. Los Angeles) or network (e.g. ABC) in your player.
Channels are grouped by category in the M3U playlist using group-title. Multiple categories are supported (e.g., a local station gets both "Local" and "News").
Add your own M3U playlists (URL or file upload) from the Settings page. Custom channels are merged into your playlist and EPG, numbered in the 10000+ range, and persist across restarts.
Each source can be edited after adding — rename it, change the URL, or upload a replacement M3U file — without losing its channel-number slot. Turn on the optional Validate toggle and the server health-checks each stream and marks offline ones red. While a source is being fetched or validated in the background, the entry shows a "loading" pill and updates automatically when the check completes.
Guide listings. A guide is picked up automatically when your playlist links one. Plenty of playlists don't — an uploaded file, or a provider that publishes its guide at a separate address — so each source also takes an EPG URL of your own, when you add it and when you edit it. Leave it empty and whatever your playlist links is used. Guide entries are matched to your channels by tvg-id, and by name when the id doesn't line up, so a hand-made M3U with no ids still gets its listings. The sources list shows how many guide entries each source holds, and when it holds none it says why — the playlist links no guide, the guide couldn't be downloaded, or the guide covers none of your channels.
Sport events are scraped, then cross-referenced with ESPN's scoreboard API for accurate start times and canonical team names. Events are sorted by start time across all leagues.
Each event gets EPG entries with pregame, game, and postgame blocks:
| Sport | Pregame | Game | Postgame |
|---|---|---|---|
| NFL | 30 min | 3.5-4 hrs | 30 min |
| NHL | 30 min | 3 hrs | 30 min |
| NBA | 30 min | 2.5 hrs | 30 min |
| MLB | 30 min | 3.5 hrs | 30 min |
| MLS | 30 min | 2 hrs | 30 min |
An event joins your playlist and guide as soon as a feed is found for it — usually well before kick-off — with the start time in the guide telling you when it begins. Waiting until the feed was confirmed live meant the game only appeared about a minute after it had already started, so a player that reads your lineup once each morning never saw that night's games at all. A finished game is not dropped at the final whistle either: it stays listed for as long as its feed keeps running, so the channel rides through post-game coverage instead of cutting off mid-broadcast.
Event channels are numbered from 5000 in start-time order.
Pick a team under Settings → Content → Favourite Teams and it gets a channel of its own. Whenever that team plays — home or away — the channel carries the game, so you can tune straight to your team instead of hunting through the night's fixtures for them. The rest of the time it shows a card telling you when they are next on, and the guide lists their upcoming games.
Team channels are numbered from 4000, just below the individual events, and you choose the order by dragging them. That number is fixed and does not reshuffle, so it is safe to map in your player. A team channel carries the same feed as the event channel for that game, so watching one costs no extra load on your box.
Requires live sports to be on (Settings → Content → Sports).
The server always keeps a small cushion of each live feed in memory before delivering it to your player, so a brief upstream stall drains that cushion instead of interrupting playback. Channels hold a few seconds; live events hold longer, because an event feed is the more fragile of the two.
If your feeds still freeze for a second or two when a provider hiccups, Settings → Playback → Stream buffering adds more on top of that cushion. The trade-off is start-up time: the extra seconds you add are seconds a channel takes to begin after you tune in — that wait is the buffer filling — and it then plays that much further behind live. Buffering applies in both local and hosted-feed modes and to any player (Jellyfin, Plex, Emby, or the built-in web player); some players add a little of their own start-up time on top. Leave the setting at zero for the closest thing to live.
Everything that needs to survive a restart lives in the /app/data volume, encrypted at rest:
The snapshot is AES-256-GCM encrypted, so the on-disk file is opaque — running sqlite3 iptv.db.gcm just reports "file is not a database". While the server is running it works against a plaintext copy held in memory that never touches persistent storage; on a clean shutdown that copy is re-encrypted back into the snapshot.
Channel numbers and the curated channel directory are baked into the image — not in persistent data — so cache purges never reshuffle numbers.
The server auto-detects the correct base URL from X-Forwarded-Proto and X-Forwarded-Host headers.
Set TRUSTED_PROXIES to your proxy's address so the server knows which X-Forwarded-For headers to believe when it records who is connecting — a comma-separated list of addresses or CIDR ranges:
environment:
- TRUSTED_PROXIES=172.16.0.0/12Leave it unset and the server trusts no proxy at all, which is the safe default: without it, anyone could put whatever they liked in that header and, for instance, sidestep the dashboard's login rate limit by pretending to be a different visitor each try. Everything still works unset — only the client addresses in your logs and rate limiting are affected.
location / {
proxy_pass http://127.0.0.1:8080;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-Host $host;
}labels:
- "traefik.enable=true"
- "traefik.http.routers.iptv.rule=Host(`iptv.example.com`)"
- "traefik.http.services.iptv.loadbalancer.server.port=8080"Found a bug or want a feature? Open an issue using one of the templates:
Proprietary -- free for personal, non-commercial use. See LICENSE for details.
Forking this repository is a violation of the license. This software may only be used in its published binary/container form. Forking, copying, or otherwise reproducing this repository creates an unauthorized derivative work, and all forks are subject to takedown.
| Back | FazBrowse Home | New Git URL |