There is no way to install this: no service unit, no image, no configuration example that is not the repository's own #115

Closed
opened 2026-08-13 22:59:44 +02:00 by Claude · 1 comment
Collaborator

Everything measured so far runs out of a checkout with cargo run. To be an install there needs to be:

  • a systemd unit, with the hardening upstream's own packaging uses and a User=/Group= that owns the data directory
  • a container image, since that is how most Jellyfin installs actually run — and it has to carry a jellyfin-ffmpeg, because M5 settled that a stock distro ffmpeg answers differently about itself and earns arg strings no real install gets
  • a documented data-directory layout and a configuration example that is not config.toml at the repository root
  • a first-run story that matches what M9 built: no configuration file means the wizard, which is already the behaviour and is not written down anywhere an operator would look

None of this is research. It is filed so that "production-ready" has something concrete under it, and so the ffmpeg pinning constraint is not rediscovered by whoever writes the image.


Where to start

Nothing here is research; it is filed so that "production-ready" has something
concrete under it and so the constraints below are not rediscovered.

  • A systemd unit, with a User=/Group= that owns the data directory and
    the hardening upstream's own packaging uses (take it from upstream's debian/
    rather than writing one from memory).
  • A container image. Most Jellyfin installs are containers. It must carry a
    jellyfin-ffmpeg and not a distro build — M5 settled that upstream patches
    its own (tonemapx, alphasrc, the rkrga filters exist in no distro build)
    and that a stock binary answers differently about itself, which means it earns
    arg strings no real install gets. reference/pin.env is where the version
    that matters is pinned.
  • The data directory layout, written down, and a configuration example that
    is not the repository's own config.toml. README.md documents running from
    a checkout; an operator needs the other thing.
  • The first-run story, which already works and is undocumented: no
    configuration file means the wizard (M9), and that is the supported way to set
    a server up.

How to know it is done

Someone who has never seen the repository installs it from the artefact, reaches
the wizard, adds a library, and plays something — on a machine that is not this
one.

House rules

CLAUDE.md is the short version and these are the parts that bite: tests run
under cargo nextest; upstream's source is the checkout in upstream/ at the
pin
(v12.0-rc5, 6d501ba4) and not the clone beside the repository;
generated files are regenerated rather than edited; a comment says what the code
does, not how it got there; and Dn references stay out of code and comments.
ARCHITECTURE.md decides which crate a thing goes in and which dependency edges
are illegal, DECISIONS.md says why a rule exists.

The reference pair is cargo xtask reference up (and reference provision,
which is idempotent); a run is
cargo run -p jellyfin-harness -- replay --upstream URL --ours URL --scenario FILE --out DIR,
with diff-db for the two databases. crates/jellyfin-harness/README.md says
what a diff means and what an ignore entry has to carry — every entry needs a
reason and an expiry milestone, and an adds-only history is the smell the risk
table names.

Everything measured so far runs out of a checkout with `cargo run`. To be an install there needs to be: - a systemd unit, with the hardening upstream's own packaging uses and a `User=`/`Group=` that owns the data directory - a container image, since that is how most Jellyfin installs actually run — and it has to carry a `jellyfin-ffmpeg`, because M5 settled that a stock distro ffmpeg answers differently about itself and earns arg strings no real install gets - a documented data-directory layout and a configuration example that is not `config.toml` at the repository root - a first-run story that matches what M9 built: no configuration file means the wizard, which is already the behaviour and is not written down anywhere an operator would look None of this is research. It is filed so that "production-ready" has something concrete under it, and so the ffmpeg pinning constraint is not rediscovered by whoever writes the image. --- ## Where to start Nothing here is research; it is filed so that "production-ready" has something concrete under it and so the constraints below are not rediscovered. - **A systemd unit**, with a `User=`/`Group=` that owns the data directory and the hardening upstream's own packaging uses (take it from upstream's `debian/` rather than writing one from memory). - **A container image.** Most Jellyfin installs are containers. It must carry a `jellyfin-ffmpeg` and not a distro build — M5 settled that upstream patches its own (`tonemapx`, `alphasrc`, the `rkrga` filters exist in no distro build) and that a stock binary answers differently about itself, which means it earns arg strings no real install gets. `reference/pin.env` is where the version that matters is pinned. - **The data directory layout, written down**, and a configuration example that is not the repository's own `config.toml`. `README.md` documents running from a checkout; an operator needs the other thing. - **The first-run story**, which already works and is undocumented: no configuration file means the wizard (M9), and that is the supported way to set a server up. ## How to know it is done Someone who has never seen the repository installs it from the artefact, reaches the wizard, adds a library, and plays something — on a machine that is not this one. ### House rules `CLAUDE.md` is the short version and these are the parts that bite: tests run under `cargo nextest`; upstream's source is the checkout in `upstream/` **at the pin** (`v12.0-rc5`, `6d501ba4`) and not the clone beside the repository; generated files are regenerated rather than edited; a comment says what the code does, not how it got there; and `Dn` references stay out of code and comments. `ARCHITECTURE.md` decides which crate a thing goes in and which dependency edges are illegal, `DECISIONS.md` says why a rule exists. The reference pair is `cargo xtask reference up` (and `reference provision`, which is idempotent); a run is `cargo run -p jellyfin-harness -- replay --upstream URL --ours URL --scenario FILE --out DIR`, with `diff-db` for the two databases. `crates/jellyfin-harness/README.md` says what a diff means and what an ignore entry has to carry — every entry needs a reason and an expiry milestone, and an adds-only history is the smell the risk table names.
Author
Collaborator

Closed by 6f1515d, with a0ab72e beside it.

What is there

packaging/:

Dockerfile The server, the pinned jellyfin-web, the pinned jellyfin-ffmpeg. Built by cargo xtask package
entrypoint.sh Puts the image's config in an empty /config and starts on it
config.default.toml / config.example.toml A container's first start, and a service install's — the second is the one with the commentary
compose.yaml Volumes, ports, and the render node
systemd/jellyfin-rs.service The unit, hardened
systemd/jellyfin-rs.sysusers.conf The account it runs as

docs/install.md is the operator's document: the data-directory layout for both
arrangements, the two install paths, the first-run story, the reverse proxy, and
hardware acceleration.

The pinning constraint, so it is not rediscovered

The Dockerfile declares RUST_VERSION, the two WEB_* and the seven FFMPEG_*
arguments without defaults. cargo xtask package reads each from the file
that owns it — rust-toolchain.toml and reference/pin.env — and
every_pin_the_dockerfile_asks_for_is_passed fails if one is declared and not
passed. No version is spelled twice, so a moved pin cannot leave a stale copy
behind.

Three things the artefacts settled

  • The base image is the release jellyfin-ffmpeg is packaged for, not a
    choice.
    The first build was Debian trixie and the noble deb refused to
    install: Depends: libx265-199 against trixie's libx265-215. FFMPEG_DISTRO
    is the base tag now. It also settles ICU — rust_icu links it rather than
    bundling it, so the builder and the runtime have to be the same release.
  • The hardening upstream "uses" is shipped switched off.
    debian/conf/jellyfin.service.conf at the pinned ref is the whole list
    commented out in a drop-in marked as an advanced feature, with several set to
    false where .NET needs them. Ours is on, because the binary was started
    under it and served — minus PrivateDevices, which would hide /dev/dri from
    every hardware transcode, and MemoryDenyWriteExecute.
  • The configuration file lives with the data, not under /etc. The server
    writes it: every dashboard save goes back to the file it was started on, and
    ProtectSystem=strict leaves only the state, cache and log directories
    writable.

A bug packaging found

The server only watched for SIGINT. systemd and docker both stop with
SIGTERM, so every stop was a kill rather than a drain — a0ab72e. Verified both
ways: the unit reports ExecMainCode=exited/ExecMainStatus=0, and
docker stop returns in 0.15s with shutting down logged.

How it was checked

  • systemd-analyze verify clean, and the server started under the unit's full
    hardening set with systemd-run --user — serves, and stops cleanly.
  • The image built and run: the entrypoint writes /config/config.toml, the
    encoder it measures is /usr/lib/jellyfin-ffmpeg/ffmpeg 8.1.2 (the host's own
    is 9.0.1, so the pin is what is inside), health check healthy,
    /web/index.html 200, and a second start does not rewrite the config.
  • The whole arrangement driven end to end against the fixture media: wizard,
    account, library, 4 films scanned, PlaybackInfo negotiating direct play, and
    a 206 with Content-Range: bytes 0-1023/97891.
  • cargo nextest run --workspace: 2516 passed, 0 skipped. fmt and clippy clean.

What the acceptance line still wants

"Someone who has never seen the repository installs it from the artefact … on a
machine that is not this one." Everything above ran on this machine, and no image
is published anywhere — cargo xtask package builds jellyfin-rs:dev locally.
Publishing it, and an install on another host, is the part that is not done.

Closed by 6f1515d, with a0ab72e beside it. ## What is there `packaging/`: | | | |---|---| | `Dockerfile` | The server, the pinned jellyfin-web, the pinned jellyfin-ffmpeg. Built by `cargo xtask package` | | `entrypoint.sh` | Puts the image's config in an empty `/config` and starts on it | | `config.default.toml` / `config.example.toml` | A container's first start, and a service install's — the second is the one with the commentary | | `compose.yaml` | Volumes, ports, and the render node | | `systemd/jellyfin-rs.service` | The unit, hardened | | `systemd/jellyfin-rs.sysusers.conf` | The account it runs as | `docs/install.md` is the operator's document: the data-directory layout for both arrangements, the two install paths, the first-run story, the reverse proxy, and hardware acceleration. ## The pinning constraint, so it is not rediscovered The Dockerfile declares `RUST_VERSION`, the two `WEB_*` and the seven `FFMPEG_*` arguments **without defaults**. `cargo xtask package` reads each from the file that owns it — `rust-toolchain.toml` and `reference/pin.env` — and `every_pin_the_dockerfile_asks_for_is_passed` fails if one is declared and not passed. No version is spelled twice, so a moved pin cannot leave a stale copy behind. ## Three things the artefacts settled - **The base image is the release jellyfin-ffmpeg is packaged for, not a choice.** The first build was Debian trixie and the noble deb refused to install: `Depends: libx265-199` against trixie's `libx265-215`. `FFMPEG_DISTRO` is the base tag now. It also settles ICU — rust_icu links it rather than bundling it, so the builder and the runtime have to be the same release. - **The hardening upstream "uses" is shipped switched off.** `debian/conf/jellyfin.service.conf` at the pinned ref is the whole list commented out in a drop-in marked as an advanced feature, with several set to `false` where .NET needs them. Ours is on, because the binary was started under it and served — minus `PrivateDevices`, which would hide `/dev/dri` from every hardware transcode, and `MemoryDenyWriteExecute`. - **The configuration file lives with the data, not under `/etc`.** The server writes it: every dashboard save goes back to the file it was started on, and `ProtectSystem=strict` leaves only the state, cache and log directories writable. ## A bug packaging found The server only watched for `SIGINT`. systemd and docker both stop with `SIGTERM`, so every stop was a kill rather than a drain — a0ab72e. Verified both ways: the unit reports `ExecMainCode=exited`/`ExecMainStatus=0`, and `docker stop` returns in 0.15s with `shutting down` logged. ## How it was checked - `systemd-analyze verify` clean, and the server started under the unit's full hardening set with `systemd-run --user` — serves, and stops cleanly. - The image built and run: the entrypoint writes `/config/config.toml`, the encoder it measures is `/usr/lib/jellyfin-ffmpeg/ffmpeg` 8.1.2 (the host's own is 9.0.1, so the pin is what is inside), health check `healthy`, `/web/index.html` 200, and a second start does not rewrite the config. - The whole arrangement driven end to end against the fixture media: wizard, account, library, 4 films scanned, `PlaybackInfo` negotiating direct play, and a `206` with `Content-Range: bytes 0-1023/97891`. - `cargo nextest run --workspace`: 2516 passed, 0 skipped. fmt and clippy clean. ## What the acceptance line still wants "Someone who has never seen the repository installs it from the artefact … on a machine that is not this one." Everything above ran on this machine, and no image is published anywhere — `cargo xtask package` builds `jellyfin-rs:dev` locally. Publishing it, and an install on another host, is the part that is not done.
Sign in to join this conversation.
No description provided.