17 Commits

Author SHA1 Message Date
benvin c4d4dd75a7 Merge pull request 'Repin UPSTREAM_REF to jellyfin-ha-src main' (#13) from benvin/jellyfin-v0.3.2 into main
ci/woodpecker/tag/docker Pipeline was successful
Reviewed-on: #13
2026-09-13 21:06:33 +10:00
unkin-agent 521ef065ca Repin UPSTREAM_REF to jellyfin-ha-src main
ci/woodpecker/pr/build Pipeline was successful
Picks up cross-replica auth read-through, bare Jellyfin__ env config,
the min(uuid) anti-join fix for browse/search/Recently Added, and
scan-leader gating tied to Redis configuration.
2026-09-13 20:25:42 +10:00
benvin e103b57e97 Merge pull request 'fix: make plugin sync safe for concurrent replica starts' (#12) from benvin/fix-plugin-sync-race into main
Reviewed-on: #12
2026-09-13 18:57:11 +10:00
unkin-agent e0a195179b fix: make plugin sync safe for concurrent replica starts
ci/woodpecker/pr/build Pipeline was successful
Two replicas starting together on the shared /config volume race the
unconditional rm+cp, so one dies with a permission error or a plugin
UnauthorizedAccessException. Skip the sync once the correct version
is already present, stage new/changed versions and move them into
place with an atomic rename, and treat a lost rename race or an
already-removed stale version as success rather than failure.
2026-09-13 18:49:52 +10:00
benvin 64b6c01e91 Merge pull request 'build: repin UPSTREAM_REF to fixed PostgreSQL upgrade path' (#10) from benvin/jellyfin-v0.3.1 into main
ci/woodpecker/tag/docker Pipeline was successful
Reviewed-on: #10
2026-09-12 18:17:41 +10:00
unkin-agent c8960dbe82 build: repin UPSTREAM_REF to fixed PostgreSQL upgrade path
ci/woodpecker/pr/build Pipeline was successful
v0.3.0 shipped a regenerated initial migration that broke upgrades
on existing Postgres databases. Pin the fork commit restoring the
baseline plus forward migrations and the EncoderPreset fallback.
2026-09-12 18:09:24 +10:00
benvin 8631c39979 Merge pull request 'Track Jellyfin v12.0' (#9) from benvin/jellyfin-v12 into main
ci/woodpecker/tag/docker Pipeline was successful
Reviewed-on: #9
2026-09-12 13:20:54 +10:00
unkin-agent f7ea7675cb build: repin UPSTREAM_REF to merged v12.0 source
ci/woodpecker/pr/build Pipeline was successful
2026-09-12 12:51:45 +10:00
unkin-agent 2924cbb63d build: track jellyfin v12.0
ci/woodpecker/pr/build Pipeline was successful
- pin UPSTREAM_REF to the v12.0 rebase commit
- move publish to dotnet SDK 10.0 and the runtime to aspnet:10.0
- pin jellyfin-web to 12.0+deb12
- bake LDAP Authentication 24.0.0.0 (targetAbi 12.0.0.0)
- build PR images with the CA-baked buildx plugin
2026-09-12 01:27:48 +10:00
benvin 26d399b8d5 Merge pull request 'Bake LDAP + SSO auth plugins into the image' (#8) from benvin/auth-plugins into main
ci/woodpecker/tag/docker Pipeline was successful
Reviewed-on: #8
2026-08-29 12:03:44 +10:00
unkin-agent ac61265ea0 Route LDAP plugin through artifactapi remote for secure image builds.
ci/woodpecker/pr/build Pipeline was successful
Replace direct repo.jellyfin.org download with artifactapi.k8s.syd1.au.unkin.net
remote. SHA256 pin guarantees integrity over HTTP. Both plugins now consistent
in sourcing from artifactapi infrastructure.
2026-08-29 12:00:03 +10:00
unkin-agent 72290bbacd fix(ci): pull SSO plugin via artifactapi github proxy
ci/woodpecker/pr/build Pipeline failed
The PR build failed at the plugins stage: the CI build network can reach
artifactapi and package mirrors (repo.jellyfin.org) but not github.com
directly, so the SSO plugin download from github failed (curl exit 7).
Route the SSO fetch through the artifactapi github remote proxy instead;
SSO_SHA256 still pins the exact bytes. LDAP is unchanged.
2026-08-26 23:28:56 +10:00
unkin-agent 2001204e0b Bake LDAP + SSO auth plugins into the image
ci/woodpecker/pr/build Pipeline failed
Phase-1 SSO/app-passwords for jellyfin needs the ldapauth and sso plugins
present without relying on the in-app catalog (which the plugins-baked PVC
would otherwise let drift). Pin the newest release of each whose targetAbi
is <= the pinned server version (10.11.6) and let the image own the version.

- Add a plugins build stage that downloads, sha256-verifies (matching each
  release's published .sha256), and unpacks the plugin zips into versioned
  dirs baked at /usr/share/jellyfin/plugins-baked.
- LDAP Authentication 22.0.0.0 (targetAbi 10.11.2.0; v23 needs 10.11.9).
- SSO Authentication 4.0.0.4 (targetAbi 10.11.0.0).
- Add docker-entrypoint.sh that syncs baked plugin dirs into /config/plugins
  on every start, removing any stale versioned dir of the same plugin so the
  image controls the version across restarts; preserves plugin configurations.
- Point ENTRYPOINT at the new script.
2026-08-26 22:13:40 +10:00
benvin 4a4965921f Merge pull request 'fix: align runtime image .NET to the app (9.0)' (#7) from benvin/jellyfin-dotnet9 into main
ci/woodpecker/tag/docker Pipeline was successful
Reviewed-on: #7
2026-08-15 18:44:51 +10:00
unkin-agent c7409d0812 fix: align runtime image .NET to the app (9.0)
ci/woodpecker/pr/build Pipeline was successful
The runtime image based on aspnet:10.0 provides only .NET 10.x, but the
publish step builds framework-dependent against SDK 9.0, so the app requires
Microsoft.NETCore.App 9.0.0 and crashes on start under 10.x.

Pin the runtime base to aspnet:9.0 to match the SDK 9.0 publish.
2026-08-15 18:40:05 +10:00
benvin 248027aaea Merge pull request 'ci: use CA-baked plugin-docker-buildx image for artifactapi push' (#6) from benvin/jellyfin-buildx-plugin-image into main
ci/woodpecker/tag/docker Pipeline was successful
Reviewed-on: #6
2026-08-15 18:20:05 +10:00
unkin-agent 0020ee2a58 ci: use CA-baked plugin-docker-buildx image for artifactapi push
ci/woodpecker/pr/build Pipeline was successful
Point the release docker step at the custom
artifactapi.k8s.syd1.au.unkin.net/docker-internal/plugin-docker-buildx
image, which bakes artifactapi's internal Vault CA into
/etc/docker/certs.d/<registry>/ca.crt.

- Swap the docker step image from upstream woodpeckerci/plugin-docker-buildx
  to the CA-baked custom image.
- Drop the separate ca-trust step (and its almalinux9-base dependency) that
  staged the CA into the workspace.
- Repoint buildkit_config ca= at the in-image baked CA path so the buildx
  docker-container builder still hands the CA to buildkitd for the push.
2026-08-15 18:06:11 +10:00
5 changed files with 121 additions and 36 deletions
+2 -2
View File
@@ -5,7 +5,7 @@ steps:
# Clone the pinned upstream jellyfin-ha source and publish the .NET server
# into ./publish-output (consumed by Dockerfile.runtime).
- name: publish
image: mcr.microsoft.com/dotnet/sdk:9.0
image: mcr.microsoft.com/dotnet/sdk:10.0
commands:
- |
REF=$$(cat UPSTREAM_REF)
@@ -26,7 +26,7 @@ steps:
# Validate the runtime image builds (no push on PRs).
- name: docker-build
image: woodpeckerci/plugin-docker-buildx
image: artifactapi.k8s.syd1.au.unkin.net/docker-internal/plugin-docker-buildx:latest
settings:
repo: artifactapi.k8s.syd1.au.unkin.net/docker-internal/jellyfin-ha
dockerfile: Dockerfile.runtime
+9 -28
View File
@@ -5,7 +5,7 @@ when:
steps:
# Same publish step as the PR pipeline: clone pinned upstream + dotnet publish.
- name: publish
image: mcr.microsoft.com/dotnet/sdk:9.0
image: mcr.microsoft.com/dotnet/sdk:10.0
commands:
- |
REF=$$(cat UPSTREAM_REF)
@@ -24,44 +24,25 @@ steps:
memory: 6Gi
cpu: 4
# Stage the internal (Vault) CA into the shared workspace so the buildkit push
# below can verify artifactapi's TLS cert. almalinux9-base already trusts the
# unkin CA (it is the image the RPM release pipelines use to reach artifactapi
# over HTTPS), so its consolidated trust bundle contains the chain we need.
- name: ca-trust
image: git.unkin.net/unkin/almalinux9-base:20260606
commands:
- cp /etc/pki/tls/certs/ca-bundle.crt "$${CI_WORKSPACE}/artifactapi-ca.crt"
depends_on: [publish]
backend_options:
kubernetes:
serviceAccountName: default
resources:
requests:
memory: 256Mi
cpu: 250m
limits:
memory: 512Mi
cpu: 1
# Build the runtime image and push it to the artifactapi local docker registry.
# buildkit_config points buildkit at the staged CA so the TLS handshake with
# artifactapi (Vault-signed cert) verifies; buildx copies the referenced CA
# into the buildkitd container under /etc/buildkit/certs when it creates the
# builder. CI_WORKSPACE is runtime-only so the path is the fixed workspace path.
# The plugin image bakes artifactapi's internal (Vault) CA at
# /etc/docker/certs.d/<registry>/ca.crt; buildkit_config points the buildx
# docker-container builder at that in-image CA. buildkitd runs in its own
# container and performs the push, so it needs the CA via --config even though
# the plugin image already trusts it — buildx copies the referenced file in.
- name: docker
image: woodpeckerci/plugin-docker-buildx
image: artifactapi.k8s.syd1.au.unkin.net/docker-internal/plugin-docker-buildx:latest
settings:
registry: artifactapi.k8s.syd1.au.unkin.net
repo: artifactapi.k8s.syd1.au.unkin.net/docker-internal/jellyfin-ha
dockerfile: Dockerfile.runtime
buildkit_config: |
[registry."artifactapi.k8s.syd1.au.unkin.net"]
ca = ["/woodpecker/src/git.unkin.net/unkin/jellyfin-ha/artifactapi-ca.crt"]
ca = ["/etc/docker/certs.d/artifactapi.k8s.syd1.au.unkin.net/ca.crt"]
tags:
- ${CI_COMMIT_TAG}
- latest
depends_on: [ca-trust]
depends_on: [publish]
backend_options:
kubernetes:
serviceAccountName: default
+41 -5
View File
@@ -18,10 +18,46 @@ RUN apt-get update \
&& echo "deb [arch=amd64 signed-by=/usr/share/keyrings/jellyfin.gpg] https://repo.jellyfin.org/debian bookworm main" \
> /etc/apt/sources.list.d/jellyfin.list \
&& apt-get update \
&& apt-get install -y --no-install-recommends "jellyfin-web=10.11.6+deb12" \
&& apt-get install -y --no-install-recommends "jellyfin-web=12.0+deb12" \
&& rm -rf /var/lib/apt/lists/*
# ── Plugin stage ──────────────────────────────────────────────────────────────
# Download and verify the auth plugins, unpacked into versioned dirs baked into
# the image and synced into /config/plugins at start (docker-entrypoint.sh).
# Versions are the newest each plugin publishes whose targetAbi <= the pinned
# Jellyfin server version (12.0.0):
# LDAP Authentication 24.0.0.0 targetAbi 12.0.0.0
# SSO Authentication 4.0.0.4 targetAbi 10.11.0.0 (newest release; loads on 12.0)
# sha256 pins make each fetch reproducible.
FROM --platform=linux/amd64 debian:bookworm-slim AS plugins
RUN apt-get update \
&& apt-get install -y --no-install-recommends curl ca-certificates unzip \
&& rm -rf /var/lib/apt/lists/*
ARG LDAP_URL=http://artifactapi.k8s.syd1.au.unkin.net/api/v1/remote/jellyfin/files/plugin/ldap-authentication/ldap-authentication_24.0.0.0.zip
ARG LDAP_SHA256=3be1f9d6a6ce9ea375e556dd30136d178a8dbe35cbe866d30d3451dc3ff7e804
# LDAP is served through artifactapi remote. SSO is served through the artifactapi
# github proxy, which the CI build network can reach (github is not directly reachable).
ARG SSO_URL=http://artifactapi.k8s.syd1.au.unkin.net/api/v1/remote/github/9p4/jellyfin-plugin-sso/releases/download/v4.0.0.4/sso-authentication_4.0.0.4.zip
ARG SSO_SHA256=c09f16ba31059a434ddd7f811e4f9608d4b4c4514cc80a5bf1ca33bee61e1107
WORKDIR /plugins
RUN set -eu; \
curl -fsSL "$LDAP_URL" -o ldap.zip; \
echo "$LDAP_SHA256 ldap.zip" | sha256sum -c -; \
mkdir -p "LDAP Authentication_24.0.0.0"; \
unzip -oq ldap.zip -d "LDAP Authentication_24.0.0.0"; \
curl -fsSL "$SSO_URL" -o sso.zip; \
echo "$SSO_SHA256 sso.zip" | sha256sum -c -; \
mkdir -p "SSO Authentication_4.0.0.4"; \
unzip -oq sso.zip -d "SSO Authentication_4.0.0.4"; \
rm -f ldap.zip sso.zip
# ── Runtime stage ─────────────────────────────────────────────────────────────
# .NET 10 runtime: matches the SDK 10.0 publish step (framework-dependent) and the
# fork's net10.0 TFM, so the app's required Microsoft.NETCore.App 10.0 is present.
# Keep in lockstep with the `mcr.microsoft.com/dotnet/sdk` major in .woodpecker/*.yaml.
FROM --platform=linux/amd64 mcr.microsoft.com/dotnet/aspnet:10.0
# FFmpeg and the native deps required by SkiaSharp and fontconfig.
@@ -39,6 +75,9 @@ WORKDIR /jellyfin
COPY publish-output/ .
# jellyfin-web client assets from the webclient stage.
COPY --from=webclient /usr/share/jellyfin/web ./jellyfin-web/
# Baked auth plugins; docker-entrypoint.sh syncs these into /config/plugins.
COPY --from=plugins /plugins /usr/share/jellyfin/plugins-baked
COPY --chmod=0755 docker-entrypoint.sh /usr/local/bin/docker-entrypoint.sh
# Jellyfin default ports
EXPOSE 8096
@@ -51,7 +90,4 @@ ENV JELLYFIN_DATA_DIR=/config \
JELLYFIN_CACHE_DIR=/cache \
JELLYFIN_LOG_DIR=/config/log
ENTRYPOINT ["./jellyfin", \
"--datadir", "/config", \
"--cachedir", "/cache", \
"--webdir", "/jellyfin/jellyfin-web"]
ENTRYPOINT ["/usr/local/bin/docker-entrypoint.sh"]
+1 -1
View File
@@ -1 +1 @@
2e1e445e470c2f2c1520f66678a73faa226c2058
ec581b5e5f156edb862d01f4bd07d9ba51903ed6
+68
View File
@@ -0,0 +1,68 @@
#!/bin/sh
# Sync image-baked plugins into the /config (datadir) plugins directory on
# start. /config is an RWX volume shared across replicas, so plugins baked
# into the image are invisible until copied in here -- and this must be safe
# when several replicas start (or restart) at the same instant:
# - skip entirely once the correct version is already in place, so the
# steady state (almost every start) never touches the shared volume;
# - install a new/changed version via copy-to-staging + atomic rename, so
# no reader (another replica, or this container's own jellyfin process)
# ever observes a partially-written plugin directory. A replica that
# loses the rename race just discards its own copy -- that's success,
# not an error;
# - drop stale, differently-versioned copies of the same plugin afterwards
# so they don't shadow the current one. Best-effort: another replica may
# already be doing, or have finished, the same cleanup.
# Deliberately no locking: a lock held by a replica that dies mid-sync would
# wedge every future start on this volume, which is worse than the race it
# would prevent.
set -eu
BAKED_DIR=/usr/share/jellyfin/plugins-baked
PLUGIN_DIR=/config/plugins
STAGING_DIR="$PLUGIN_DIR/.sync-tmp"
REPLICA=$(hostname)
if [ -d "$BAKED_DIR" ]; then
mkdir -p "$PLUGIN_DIR" "$STAGING_DIR"
for src in "$BAKED_DIR"/*; do
[ -d "$src" ] || continue
name=$(basename "$src") # e.g. "LDAP Authentication_24.0.0.0"
base=${name%_*} # plugin name without the trailing _<version>
target="$PLUGIN_DIR/$name"
if [ ! -d "$target" ]; then
# Build the new version privately (keyed by this replica's own
# hostname, so concurrent replicas never share a staging path), then
# move it into place in one atomic rename. mv -T fails with
# "Directory not empty" if another replica's rename already won --
# that's fine, our copy just becomes garbage we discard.
staging="$STAGING_DIR/$REPLICA.$name"
rm -rf "$staging"
cp -a "$src" "$staging"
if mv_err=$(mv -T "$staging" "$target" 2>&1); then
:
elif [ -d "$target" ]; then
rm -rf "$staging"
else
echo "docker-entrypoint: failed to install plugin $name: $mv_err" >&2
exit 1
fi
fi
# Remove any other version of this plugin so it can't shadow the one
# above. Another replica may be racing the same cleanup, or have already
# finished it -- an entry that's already gone is success, not an error.
for existing in "$PLUGIN_DIR/$base"_*; do
[ -e "$existing" ] || continue
[ "$existing" = "$target" ] && continue
rm -rf "$existing" 2>/dev/null || true
done
done
fi
exec ./jellyfin \
--datadir /config \
--cachedir /cache \
--webdir /jellyfin/jellyfin-web \
"$@"