# DecoTV, a self-hosted video frontend, and the details that decide a deployment

> DecoTV is a Next.js 16 frontend for searching and playing video from sources you supply yourself, packaged as a versioned Docker image with four storage backends. The engineering worth reading is the reverse proxy header handling, the build scripts that run before next build, and the storage decision; the licensing is a contradiction you have to resolve yourself.

**Decohererk/DecoTV** — 基于最新版LunaTV二次开发的一个开箱即用的、跨平台的影视聚合播放站。【原KatelyaTV】

- Repository: https://github.com/Decohererk/DecoTV
- Website: https://v.katelya.eu.org
- Stars: 2,336 · Forks: 1,080
- Language: TypeScript
- License: NOASSERTION
- Published: 2026-09-30 · Updated: 2026-09-30 · Language: en
- Canonical page: https://hysenlabs.com/projects/decohererk-decotv

## What you get after deploying DecoTV: a shell and a player

The most useful sentence in this README is a warning, and it appears twice. After deployment the project is an empty shell with no built-in video sources and no live sources, and the configuration has to be collected by the person deploying it. Everything else in the feature list is the machinery around that gap.

So DecoTV is a frontend, not a service. The project describes itself as a ready-to-use cross-platform video aggregation player, built on Next.js 16, Tailwind CSS 4 and TypeScript 5, and developed as a secondary iteration on the latest LunaTV, originally KatelyaTV. The public homepage sits at a domain in the README, and the stack in `package.json` is consistent with that description: App Router and Turbopack for the framework, ArtPlayer and HLS.js plus `flv.js` for playback, `cheerio` for the scraping the source configs require, and `opencc-js` for Chinese conversion.

The features that survive the empty-shell caveat are the interesting ones for a self-hoster. Multi-source aggregated search returns results from every configured source at once. Multi-audio-track switching appears only when a video actually has two or more tracks, which is a small design decision that shows attention to detail. A private library mode browses OpenList, Aliyun Alist, Emby or Jellyfin through a server-side proxy that is explicitly there to keep stream addresses and credentials out of the browser. Server-side transfer downloads run through FFmpeg with retry and timeout handling, and live sources are auto-detected across m3u8, flv and mp4. PWA caching and a Google Cast path are there for the living-room case. None of that requires you to have content; it requires you to have configured some.

## Four storage backends behind one NEXT_PUBLIC_STORAGE_TYPE value

The single most consequential configuration decision is where state lives, and DecoTV makes it one variable with four legal values. The `.env.example` file lists the options as `localstorage`, `redis`, `upstash` and `kvrocks`, with `NEXT_PUBLIC_STORAGE_TYPE` set to `localstorage` in the shipped default.

The four options are genuinely different in what they cost you. Local storage needs no server at all, which is why the README advertises a database-free mode, at the cost of no cross-device sync. Upstash is described in the example file as recommended for Vercel and ClawCloud deployments, and it needs `UPSTASH_URL` and `UPSTASH_TOKEN`, with an explicit note not to use the `UPSTASH_REDIS_REST_*` names. Redis needs `REDIS_URL` and the README labels that option as carrying some risk of data loss, with an inline comment in the compose example telling you to enable persistence or lose data on upgrade or restart. Kvrocks is the recommended option, a Redis-compatible store from the Apache project.

There is a naming detail that will bite somebody. The selector carries the `NEXT_PUBLIC_` prefix, which in Next.js means the value is exposed to the client bundle, even though what it selects is the server-side store. That is harmless on its own, but it means a typo or a stale client bundle can leave the browser believing in one backend while the server is configured for another, and the failure mode is a login page that never stops asking for credentials. The README's compose example sets the selector and the matching URL in the same `environment` block, which is the pattern to copy.

The identity side is small and worth noting. `ADMIN_PASSWORD` sets the administrator password, and `NEXT_PUBLIC_AUTH_MODE` is documented as `password` by default with a `public` option described as a passwordless family mode recommended only for a trusted internal network. The same block carries `PUBLIC_ALLOW_ADMIN` defaulting to false, with a comment that opening the admin panel in public mode is extremely risky. That default is the right one.

## Reverse proxy headers, the port trap, and when Secure cookies appear

This is the section that will save you an afternoon, and it is unusually specific for a project README.

DecoTV decides what protocol and external host it is being reached on from four sources: the request URL, `X-Forwarded-Proto`, `X-Forwarded-Host`, or the standard `Forwarded` header. If you run Nginx or OpenResty in front of the container, the README gives you exactly what to pass:

```nginx
proxy_set_header Host $http_host;
proxy_set_header X-Forwarded-Host $http_host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
```

The trap is in the third line of that guidance. When you reverse proxy over HTTPS on a port other than 443, you must pass `$http_host` including the port, otherwise the browser loses the port on the follow-up requests to the m3u8 proxy address and playback fails after the page itself loads. That is a specific, reproducible bug and the README calls it out by name.

The cookie behaviour follows from the same detection. Over HTTPS, the login cookie gets the `Secure` attribute. Direct HTTP access on a LAN and HTTP reverse proxying do not set it, which the README notes so that browsers will still store the authentication state. And the guidance ends with a warning not to rely on the container's `NODE_ENV=production` to work out the access protocol, which is the kind of thing you only write after getting it wrong.

For a first run without a proxy, the README says plain Docker port mapping on the LAN works, with an example address on port 3000. Public deployment it describes as strongly recommended over HTTPS, and the reasoning is the cookie attribute plus the fact that a proxied m3u8 URL is a bearer of your session.

## Kvrocks on compose, and why the image tag matters more than latest

The recommended deployment is a two-container compose file on a bridge network, and the interesting part is not the syntax but the tag discipline. Trimming the README's example to the essentials:

```yaml
services:
  decotv-core:
    image: ghcr.io/decohererk/decotv:latest # 或使用 :v1.0.0 固定版本
    container_name: decotv-core
    restart: on-failure
    ports:
      - '3000:3000'
    environment:
      - USERNAME=admin
      - PASSWORD=admin_password
      - NEXT_PUBLIC_STORAGE_TYPE=kvrocks
      - KVROCKS_URL=redis://decotv-kvrocks:6666
  decotv-kvrocks:
    image: apache/kvrocks
    container_name: decotv-kvrocks
```

The full example continues with a `kvrocks-data` volume mounted at `/var/lib/kvrocks`, a `depends_on` relationship, and a `decotv-network` bridge that both containers join. Note that Kvrocks listens on 6666, not on the Redis default of 6379, which is why the URL in the example is not the string you would expect.

The tag policy is the part the README argues for at length. `latest` is the newest build, and the project also publishes per-version tags such as `v1.0.0`:

```bash
docker pull ghcr.io/decohererk/decotv:latest
docker pull ghcr.io/decohererk/decotv:v1.0.0
docker pull ghcr.io/decohererk/decotv:v0.9.0
```

The stated reasons for version tags are knowing what you are running, avoiding surprise updates in production, being able to roll back, and keeping a team on one version. One operational detail is easy to miss and is spelled out: restarting a container that runs `latest` does not pull a new image, so updates require an explicit `docker pull` and a recreate. Pinning a version tag makes that a decision instead of an accident.

For ARM boxes and routers there is a separate path. The README points to a full guide in `docs` for OpenWrt targets such as soft routers, ARM boxes and Raspberry Pi, and the same prebuilt image is pulled there, with instructions for building on an external host and exporting the image if your device cannot pull from the registry directly.

## The Dockerfile, FFmpeg, and a non-root user with uid 1001

The `Dockerfile` is a conventional three-stage build on `node:20-alpine`, and reading it tells you what the runtime actually needs.

The first stage exists only to install dependencies. It enables corepack, prepares pnpm, copies nothing but `package.json` and `pnpm-lock.yaml`, and runs `pnpm install --frozen-lockfile`. That is the right shape for layer caching, and the frozen lockfile is the right choice for an image build, since a silently updated dependency set in a release image is exactly the kind of thing you cannot debug later.

The second stage copies `node_modules` from the first, copies the source, and sets a block of build-time arguments: `BUILD_TIMESTAMP`, `GIT_COMMIT_SHA`, `GIT_COMMIT_DATE` and `GIT_REF_NAME`, each promoted into a `NEXT_PUBLIC_` variable as well. Those end up in the client bundle, which is how the UI can display what it is running. It also sets `DOCKER_ENV=true` and `DOCKER_BUILD=true` explicitly during the build, so the application can tell it is being containerised even at build time.

The third stage is the interesting one. It installs `ca-certificates` and `ffmpeg` with `apk add --no-cache`, because server-side transfer downloads depend on `ffmpeg` and `ffprobe` and the README's comment says a VPS image needs them working out of the box. It then creates a group at gid 1001 and a `nextjs` user at uid 1001 and runs as that user rather than root. `FFMPEG_DOWNLOAD_DIR` is set to a path under the app directory, `HOSTNAME` is `0.0.0.0` and `PORT` is `3000`.

The cost of that design is image size. Carrying a full FFmpeg build in a Node Alpine image for a feature many deployments will never touch is a real trade, and it is the price of the server-side download path being available without a second container.

## Why plain next build is not the build: gen:manifest and gen:version

The scripts in `package.json` contain a trap that will cost you ten minutes the first time you try to build this yourself. Both `dev` and `build` are composite commands:

```json
"dev": "pnpm gen:manifest && pnpm gen:version && next dev -H 0.0.0.0",
"build": "pnpm gen:manifest && pnpm gen:version && next build",
```

`gen:manifest` runs `node scripts/generate-manifest.js` and `gen:version` runs `node scripts/generate-version-metadata.js`, both before Next.js is invoked at all. So `next build` on its own produces a build that is missing generated files, and the correct entry point is `pnpm build`, which the Dockerfile also uses. A related detail: `postbuild` echoes a message that sitemap generation is disabled, so do not go looking for a sitemap.

The rest of the script list is a well-equipped JavaScript project rather than a demo. There is `typecheck` running `tsc --noEmit --incremental false`, `test` and `test:watch` on Jest 29, `lint` and a stricter `lint:strict` that fails on any warning, `format` and `format:check` on Prettier, and `lint:fix` that runs ESLint over `src` before formatting. `prepare` installs husky, and the top-level `commitlint.config.js` plus the husky directory mean commit messages are checked, not merely suggested.

There are container shortcuts too, `docker:build` and `docker:run`, which build the local tag and map port 3000. Those exist for iteration; the registry image is what the README tells you to deploy.

One inconsistency is worth flagging for anyone auditing the deployment story. The README states the project only supports Docker or Docker-based platforms, yet the tree still carries `vercel.json` and a `vercel-build` script, the example environment file recommends Upstash for Vercel, and the danmaku section mentions a public relay for Vercel deployments. Those look like leftovers from an earlier deployment model, and the README's Docker-only statement is the one to plan against.

## DecoTV against upstream LunaTV, and against just running Jellyfin

Two comparisons, and the second one settles the question for a lot of households.

Against the upstream it is derived from, the difference is packaging and integration. DecoTV is a secondary development on the latest LunaTV, formerly KatelyaTV, and what it adds is the delivery story: prebuilt multi-arch images on a registry with per-version tags for rollback, a compose file with the database already wired, FFmpeg in the runtime for server-side transfers, a reverse proxy recipe that has been debugged against real m3u8 playback, and a private library mode that talks to OpenList, Alist, Emby and Jellyfin. The trade is distance from upstream. A `private` flag in `package.json` means you are not consuming a package from a registry, you are consuming an image or a checkout, and the Docker-only statement means your deployment options are narrower than the tree suggests.

Against running Jellyfin or Emby directly, the difference is what kind of library you have. Those projects are built for media you own: a hard drive, a NAS, a disc library. DecoTV's distinguishing features, aggregated search across several third-party source configurations, live source handling across m3u8, flv and mp4, danmaku with TMDB matching, and PanSou cloud-drive search, are all about assembling content you did not host. If your media is yours, the honest comparison is that Jellyfin gives you a maintained server, a real user model and hardware transcoding, and DecoTV gives you a nicer search box over sources you maintain yourself.

That framing also explains the ad-skipping feature the README marks as experimental, and the double circuit breaker it claims isolates adult content from configuration through to the proxy layer. Both are quality-of-life features for one kind of deployment and irrelevant noise in the other. Choosing between the two projects is mostly choosing what your library is.

## A custom licence behind an MIT badge, and a README with rules about promotion

Licensing here needs a warning rather than a summary. GitHub could not classify the licence for this repository and shows it as Other, a custom licence, with the text in the `LICENSE` file. The README, however, carries a shield badge reading License-MIT-green, and its table of contents includes a License section. Those two statements do not agree, and the `LICENSE` file is the only authority.

The README also contains instructions that no MIT text would produce. It asks readers not to publish videos or articles promoting the project on Bilibili, Xiaohongshu, WeChat public accounts, Douyin, Toutiao or other mainland Chinese social platforms, and it states that it does not authorise any technology weekly or monthly project or site to list the project. A licence that restricts who may talk about the software is not MIT, whatever the badge says. If you need to redistribute, embed, or offer this as a service to users, read the `LICENSE` file and take the custom terms at face value. This is a description of what the files say rather than legal advice.

The release record is healthier. There are tagged releases, with v1.5.0 on 2026-06-08, v1.4.0 on 2026-03-26 and v1.3.0 on 2026-02-12, so roughly every six to eight weeks through the first half of 2026, and the manifest version matches the newest tag at 1.5.0. The last push was on 2026-07-16 and the repository is not archived. There is a `CHANGELOG` file at the top level, though without a file extension, plus a `VERSION.txt` and a `SECURITY.md`.

So the code is moving and the packaging is disciplined. The one thing to settle before you build on it is not technical: read the `LICENSE` file and find out which of those two statements is true.

## Conclusion

Adopt DecoTV if you want a video search and playback frontend you host yourself, with server-side FFmpeg transfers and a private library front end for OpenList, Alist, Emby or Jellyfin, and you are willing to assemble the sources yourself. Do not adopt it expecting a turnkey catalogue, and do not build on it commercially without reading the LICENSE file, because the repository records a custom licence while the README carries an MIT badge. Verify first by pinning an image tag rather than latest, setting NEXT_PUBLIC_STORAGE_TYPE and the matching URL variable together, and confirming your reverse proxy passes X-Forwarded-Proto and a port-bearing X-Forwarded-Host.

## FAQ

### How do I deploy DecoTV?

The README says the project only supports Docker or Docker-based platforms, and the image is published at ghcr.io/decohererk/decotv with both a latest tag and per-version tags such as v1.0.0. The recommended setup is a compose file with the app on port 3000 and a Kvrocks container, and there is a separate guide for OpenWrt targets such as ARM boxes and Raspberry Pi.

### What storage options does NEXT_PUBLIC_STORAGE_TYPE support?

The example environment file lists four options: localstorage, redis, upstash and kvrocks, with localstorage as the shipped default and no database at all. Kvrocks is the recommended option, Upstash is described as recommended for Vercel and ClawCloud, and the Redis option is labelled as carrying some risk of data loss unless persistence is enabled.

### Does DecoTV include any video or live sources?

No. The README states twice that after deployment the project is an empty shell with no built-in video sources and no live sources, and that you have to collect the configuration yourself. The aggregation search, live playback and download features work against sources you configure.

### Which reverse proxy headers does DecoTV need?

DecoTV works out the real protocol and external host from the request URL, X-Forwarded-Proto, X-Forwarded-Host or the standard Forwarded header. The README's Nginx snippet sets Host, X-Forwarded-Host, X-Real-IP, X-Forwarded-For and X-Forwarded-Proto, and warns that a non-443 HTTPS port must be passed with the port included or m3u8 proxy requests will lose it.

### Does DecoTV set Secure cookies over HTTPS?

Yes when the detected access protocol is HTTPS. Direct HTTP access on a LAN and HTTP reverse proxying do not set the Secure attribute, which is what lets the browser keep the authentication state in those setups. The README also warns against using the container's NODE_ENV=production value to determine the access protocol.

### What licence is DecoTV released under?

GitHub could not classify the licence and shows it as Other, a custom licence, with the text in the LICENSE file, while the README carries an MIT badge and also asks readers not to promote the project on several Chinese social platforms. Read the LICENSE file before redistributing or offering it as a service.

## Sources

- [Decohererk/DecoTV on GitHub](https://github.com/Decohererk/DecoTV)
- [Issues](https://github.com/Decohererk/DecoTV/issues)
- [Project website](https://v.katelya.eu.org)
- [README](https://github.com/Decohererk/DecoTV/blob/main/README.md)
- [Releases](https://github.com/Decohererk/DecoTV/releases)

---

Hysen Labs editorial analysis, written from the project's own repository and release notes. Cite the canonical page: https://hysenlabs.com/projects/decohererk-decotv
