Jellyfin currently has no way to limit the maximum ABI version. This means that people can install incompatible versions of a plugin. Unfortunately, the error is only visible after restarting Jellyfin. The average user expects to be offered only working and compatible versions.
The easiest method of self-hosting is to use Caddy, which is how we currently deploy on DigitalOcean. See the docker-compose.yaml and Caddyfile for reference.
All Jellyfin 12.x clients use the canonical 12/manifest.json catalog. Jellyfin 10.x clients keep their minor-version catalogs, such as 10.11/manifest.json. The manifest publisher keeps 12.0/manifest.json and 12.0/manifest-prerelease.json synchronized with their 12/ counterparts for existing direct URLs.
When migrating from 12.0/, publish the new 12/ catalogs and updated publishing scripts first, so that manifest@main contains 12/manifest.json before the Caddyfile is deployed. The commit_hash committed in this repo is only a placeholder: the deploy workflow replaces it with the current manifest@main commit, and the Caddyfile Updater keeps it in sync afterwards.
Deployment is done by the Deploy Caddyfile GitHub Actions workflow (deploy-caddy.yml). It is triggered manually only:
- Merge the Caddyfile change to
main. - Actions → Deploy Caddyfile → Run workflow (branch
main).
The workflow joins the tailnet with tailscale/github-action as an ephemeral tag:ci node and, for each server (fra, ams, germany), over Tailscale SSH:
- uploads
docker/Caddyfilewithcommit_hashset to the currentintro-skipper/manifest@maincommit, - runs
docker/deploy-remote.sh, which validates the new config inside the Caddy container (so{env.*}placeholders resolve), overwrites/home/caddy/Caddyfilein place and gracefully reloads Caddy, - finally runs the HTTPS redirect tests (
test-caddy.yml) against the live servers.
An invalid Caddyfile fails the job before anything on disk changes. /home/caddy/Caddyfile is a single-file bind mount, so it must be overwritten in place, never replaced with mv.
The redirect tests only run as part of the deploy (or via Run workflow), since they test the deployed servers rather than the checked-out Caddyfile.
- Tailscale ACL: a
tagOwnersentry fortag:ci, anacls/grantsrule allowingtag:ci→ server tag on port 22, and ansshrule (action: accept) allowingtag:cito log in asroot. Without the network rule the servers never show up in the runner's peer list and the workflow hangs onping. - A Tailscale OAuth client with the
auth_keyswrite scope, taggedtag:ci. - Repository secrets
TS_OAUTH_CLIENT_IDandTS_OAUTH_SECRET.
The same steps can be run by hand from a machine on the tailnet:
hash=$(git ls-remote https://github.com/intro-skipper/manifest.git refs/heads/main | cut -f1)
[[ "$hash" =~ ^[0-9a-f]{40}$ ]] || { echo "could not resolve manifest@main"; exit 1; }
for h in fra ams germany; do
sed "s/commit_hash \"[0-9a-f]*\"/commit_hash \"$hash\"/" docker/Caddyfile | ssh root@$h 'cat > /home/caddy/Caddyfile.new'
ssh root@$h bash -s < docker/deploy-remote.sh
doneWe switched to jsDelivr in oder to serve manifest and logo requests in without hitting GitHub API limits. Caddyfile Updater always serves the current version by updating the Caddyfile and reloading Caddy without any downtime.
Almost all webhosters have Apache mod_rewrite enabled. PHP always has restrictions and is not needed anyway.
Caution
Some countries block Cloudflare
Instead of spending money for a webhoster you can connect a domain to cloudflare and use domain redirect rules.
Rules -> Redirect Rules
All rules are Custom filter expressions
| Field | Operator | Value | URL redirect Type | URL | Status code |
|---|---|---|---|---|---|
| User Agent | strict wildcard | Jellyfin-Server/12.* | Static | https://raw.githubusercontent.com/intro-skipper/manifest/refs/heads/main/12/manifest.json | 302 |
| User Agent | strict wildcard | Jellyfin-Server/10.11.* | Static | https://raw.githubusercontent.com/intro-skipper/manifest/refs/heads/main/10.11/manifest.json | 302 |
| User Agent | strict wildcard | Jellyfin-Server/10.10.* | Static | https://raw.githubusercontent.com/intro-skipper/manifest/refs/heads/main/10.10/manifest.json | 302 |
| User Agent | strict wildcard | Jellyfin-Server/10.9.* | Static | https://raw.githubusercontent.com/intro-skipper/manifest/refs/heads/main/10.9/manifest.json | 302 |
| User Agent | strict wildcard | Jellyfin-Server/10.8.* | Static | https://raw.githubusercontent.com/intro-skipper/manifest/refs/heads/main/10.8/manifest.json | 302 |
Inspired by DigitalOcean Docs Enable Reserved IPv6
Our script generates and implements a permanent network plan.
curl -fsSL https://raw.githubusercontent.com/intro-skipper/server_configs/refs/heads/main/set_reserved_ipv6.sh | bash