Volunteer-built media solution that puts you in control — stream to any device from your own server, with no strings attached.
| Port | 8096 |
| Registry | ghcr.io/daemonless/jellyfin |
| Source | https://github.com/jellyfin/jellyfin |
| Website | https://jellyfin.org/ |
| Tag | Description | Best For |
|---|---|---|
latest |
FreeBSD Latest. Jellyfin plus the jellyfin-ffmpeg7 package (not in quarterly). | Most users — recommended. |
pkg |
FreeBSD Quarterly. Uses stable, tested packages. | Production stability. |
pkg-latest |
FreeBSD Latest. Rolling package updates. | Staying current. |
Before deploying, ensure your host environment is ready. See the Quick Start Guide for host setup instructions.
services:
jellyfin:
image: "ghcr.io/daemonless/jellyfin:latest"
container_name: jellyfin
environment:
- PUID=1000 # User ID for the application process
- PGID=1000 # Group ID for the application process
- TZ=UTC # Timezone for the container
- FFMPEG_PATH=/usr/local/bin/jellyfin-ffmpeg # Path to the FFmpeg binary. Options: /usr/local/bin/jellyfin-ffmpeg (default) or /usr/local/bin/ffmpeg
volumes:
- "/path/to/containers/jellyfin:/config"
- "/path/to/containers/jellyfin/cache:/cache" # optional
- "/path/to/tv:/tv" # optional
- "/path/to/movies:/movies" # optional
ports:
- "8096:8096"
annotations:
org.freebsd.jail.allow.mlock: "true"
# always (not unless-stopped) so FreeBSD's podman rc.d auto-starts it at boot
restart: alwaysSave as compose.yaml, then run podman-compose up -d.
.env:
# .env
DIRECTOR_PROJECT=jellyfin
PUID=1000
PGID=1000
TZ=UTC
FFMPEG_PATH=/usr/local/bin/jellyfin-ffmpeg
appjail-director.yml:
# appjail-director.yml
options:
- virtualnet: ':<random> default'
- nat:
services:
jellyfin:
name: jellyfin
options:
- container: 'boot args:--pull'
- expose: '8096:8096 proto:tcp'
oci:
user: root
environment:
- PUID: !ENV '${PUID}'
- PGID: !ENV '${PGID}'
- TZ: !ENV '${TZ}'
- FFMPEG_PATH: !ENV '${FFMPEG_PATH}'
volumes:
- jellyfin: /config
- jellyfin_cache: /cache
- tv: /tv
- movies: /movies
volumes:
jellyfin:
device: '/path/to/containers/jellyfin'
jellyfin_cache:
device: '/path/to/containers/jellyfin/cache'
tv:
device: 'tv'
movies:
device: 'movies'Makejail:
# Makejail
ARG tag=latest
OPTION overwrite=force
OPTION from=ghcr.io/daemonless/jellyfin:${tag}
SET allow.mlock=1
Save the files above, then run appjail-director up.
Note: Exposing ports in AppJail means that your service can be reached from remote hosts. If that is not your intention, do not expose the ports and communicate with the service using the IPv4 address assigned by the virtual network.
podman run -d --name jellyfin \
-p 8096:8096 \
--annotation 'org.freebsd.jail.allow.mlock=true' \
-e PUID=1000 \
-e PGID=1000 \
-e TZ=UTC \
-e FFMPEG_PATH=/usr/local/bin/jellyfin-ffmpeg \
-v /path/to/containers/jellyfin:/config \
-v /path/to/containers/jellyfin/cache:/cache # optional \
-v /path/to/tv:/tv # optional \
-v /path/to/movies:/movies # optional \
ghcr.io/daemonless/jellyfin:latestSave as run.sh, then run sh run.sh.
appjail oci run -Pd \
-o overwrite=force \
-o container="args:--pull" \
-o virtualnet=":<random> default" \
-o nat \
-o expose="8096:8096 proto:tcp" \
-e PUID=1000 \
-e PGID=1000 \
-e TZ=UTC \
-e FFMPEG_PATH=/usr/local/bin/jellyfin-ffmpeg \
-o fstab="/path/to/containers/jellyfin /config <pseudofs>" \
-o fstab="/path/to/containers/jellyfin/cache /cache <pseudofs>" \ # optional
-o fstab="/path/to/tv /tv <pseudofs>" \ # optional
-o fstab="/path/to/movies /movies <pseudofs>" \ # optional
ghcr.io/daemonless/jellyfin:latest jellyfinSave as run.sh, then run sh run.sh.
Note: Exposing ports in AppJail means that your service can be reached from remote hosts. If that is not your intention, do not expose the ports and communicate with the service using the IPv4 address assigned by the virtual network.
Warning
Bastille's OCI support is experimental. It requires buildah, shares the host network stack (inherit), and persists image-declared volumes under --data-path.
services:
jellyfin:
image: "ghcr.io/daemonless/jellyfin:latest"
container_name: jellyfin
network_mode: host # jail shares host networking
environment:
- PUID=1000
- PGID=1000
- TZ=UTC
- FFMPEG_PATH=/usr/local/bin/jellyfin-ffmpegSave as podman-compose.yml, then run bastille up. Or via CLI:
bastille create -O \
--env PUID=1000 \
--env PGID=1000 \
--env TZ=UTC \
--env FFMPEG_PATH=/usr/local/bin/jellyfin-ffmpeg \
--data-path /path/to/containers/jellyfin \
jellyfin ghcr.io/daemonless/jellyfin:latest inherit- name: Deploy jellyfin
containers.podman.podman_container:
name: jellyfin
image: "ghcr.io/daemonless/jellyfin:latest"
state: started
restart_policy: always
env:
PUID: "1000"
PGID: "1000"
TZ: "UTC"
FFMPEG_PATH: "/usr/local/bin/jellyfin-ffmpeg"
ports:
- "8096:8096"
volumes:
- "/path/to/containers/jellyfin:/config"
- "/path/to/containers/jellyfin/cache:/cache" # optional
- "/path/to/tv:/tv" # optional
- "/path/to/movies:/movies" # optional
annotation:
org.freebsd.jail.allow.mlock: "true"Save as jellyfin-deploy.yaml, then run ansible-playbook jellyfin-deploy.yaml.
Access at: http://localhost:8096
| Variable | Default | Description |
|---|---|---|
PUID |
1000 |
User ID for the application process |
PGID |
1000 |
Group ID for the application process |
TZ |
UTC |
Timezone for the container |
FFMPEG_PATH |
/usr/local/bin/jellyfin-ffmpeg |
Path to the FFmpeg binary. Options: /usr/local/bin/jellyfin-ffmpeg (default) or /usr/local/bin/ffmpeg |
| Path | Description |
|---|---|
/config |
Configuration directory |
/cache |
Cache directory (Optional) |
/tv |
TV Series library (Optional) |
/movies |
Movie library (Optional) |
| Port | Protocol | Description |
|---|---|---|
8096 |
TCP | Web UI |
This image includes both standard ffmpeg and jellyfin-ffmpeg. By default,
it uses jellyfin-ffmpeg (via the FFMPEG_PATH environment variable).
Why use jellyfin-ffmpeg?
While raw encoding performance is similar, jellyfin-ffmpeg includes critical
patches for HDR to SDR tone mapping (specifically the tonemapx filter).
Stock FFmpeg lacks a proper BT.2390 implementation, which results in "washed
out" or blown-out colors when serving HDR10 content to SDR displays.
Port Status
Note that jellyfin-ffmpeg is not currently in the official FreeBSD ports
tree, but it is provided in this image to ensure a high-quality transcoding
experience identical to the official Jellyfin Linux distributions.
Intel iGPU hardware transcoding is supported via VAAPI. The image includes
libva, libva-intel-media-driver, and gmmlib.
Host setup required — the GPU must be enabled and a devfs ruleset must expose the DRI devices into the jail:
-
Install the DRM kernel module and firmware:
pkg install drm-kmod libva-intel-media-driver gmmlib echo 'kld_list="i915kms"' >> /etc/rc.conf kldload i915kms -
Add a devfs ruleset to
/etc/devfs.rules:[devfsrules_jails_gpu=61182] add include $devfsrules_hide_all add include $devfsrules_unhide_basic add include $devfsrules_unhide_login add path 'bpf*' unhide add path 'dri' unhide add path 'dri/*' unhide mode 0666 add path 'drm*' unhide mode 0666 -
Configure the jail to use ruleset
61182and enableallow.mlock(already set in the example compose above). -
In Jellyfin: Dashboard → Playback → Transcoding → set Hardware acceleration to VAAPI, device
/dev/dri/renderD128.
Architectures: amd64
User: bsd (UID/GID via PUID/PGID, defaults to 1000:1000)
Base: FreeBSD 15.1
Need help? Join our Discord community.