Media Server
Media and photo management services.
| Service | Host | Purpose |
|---|---|---|
| Plex | kontti | Video streaming and media library |
| Kometa | kontti | Plex metadata and collection management |
| Immich | kontti | Photo and video backup and browsing |
| Music Assistant | Home Assistant | Smart home music integration |
Plex, Kometa, and Immich run as Podman containers on kontti, managed by system-level Quadlets and provisioned with Ansible. Music Assistant runs as a Home Assistant add-on.
Plex
Plex serves as the central media library and streaming server. It runs using the LinuxServer.io image with host networking and hardware-accelerated transcoding via the AMD GPU (VA-API).
[Container]
ContainerName=plex
Image=lscr.io/linuxserver/plex:latest
AutoUpdate=registry
Network=host
# Own user namespace: the container's UIDs are subordinate UIDs on the host
UserNS=auto
Volume=/var/lib/appdata/plex:/config:idmap,Z
Volume=/var/mnt/nfs-data:/data:Z
# AMD GPU hardware transcoding (VA-API)
AddDevice=/dev/dri:/dev/dri
# s6 starts as root and drops the app to PUID/PGID, so it keeps what that needs
DropCapability=all
AddCapability=CHOWN
AddCapability=DAC_OVERRIDE
AddCapability=FOWNER
AddCapability=SETGID
AddCapability=SETUID
AddCapability=KILL
NoNewPrivileges=true
The :Z on those volume mounts stamps a private SELinux category, so only Plex can read that content — which is also why the backup container needs a specific exemption to read it.
Media files are stored on NFS and shared with other services through the same mount point. Plex library metadata is also synced to a vector database for natural-language search over the catalogue — see AI → plex-sync.
UserNS=auto is the more recent addition, and Plex was the last of the s6-based images to get it. Those images run PID 1 as real host root by design — that's how they drop the application to PUID/PGID, and it can't be configured away — so a namespace of its own was the only way to get Plex off host UID 0. The capabilities above are still needed for the privilege drop; they now apply only inside that namespace. /config is mapped through :idmap while the NFS mount deliberately isn't — why that split exists is an estate-wide decision, not a Plex one.
What was specific to Plex is hardware transcoding, which is why it went last. It still works under the namespace: the render node is world-readable, opens read-write from inside, and a real VA-API encode succeeds there on the same GPU. Network=host turned out to be no obstacle either, since the network and user namespaces are independent — Plex still binds the host's ports.
Kometa
Kometa runs nightly and manages Plex collections, posters, and metadata overlays. Collections are defined in YAML and templated with Ansible so they stay in version control.
Immich
Immich handles photo and video backup from phones and cameras. It runs as a Podman pod with four containers:
| Container | Role |
|---|---|
immich-server | API and web UI |
immich-machine-learning | Face recognition and CLIP search |
immich-postgres | Database |
immich-redis | Cache and job queue (Valkey image) |
All four share a single Podman pod, so they sit in one network namespace and reach each other over localhost. Only the web port is published to the host:
graph TB
phone[Phones · cameras] -->|":2283"| server
subgraph pod[immich.pod · one network namespace]
server[immich-server<br/>API + web UI]
ml[immich-machine-learning<br/>CLIP + face recognition]
pg[(immich-postgres<br/>VectorChord)]
redis[(immich-redis<br/>Valkey)]
end
server -->|localhost| pg
server -->|localhost| redis
server -->|localhost| ml The machine learning container uses the OpenVINO runtime for GPU-accelerated inference on the AMD GPU, which noticeably speeds up face clustering and the CLIP-based smart search on large photo libraries.
[Container]
Image=ghcr.io/immich-app/immich-server:v3
Pod=immich.pod
Volume=/var/mnt/nfs-data/media/photos:/data:Z
AddDevice=/dev/dri
ShmSize=2G
Photos are stored on NFS alongside other media. The ShmSize=2G setting is needed for video processing — the default shared memory is too small for transcoding larger files.
Deployment choices
A few choices in the Immich deployment are worth calling out:
- One pod — as a recorded exception, not the house style. All four containers join a single Podman pod (
immich.pod), so they share one network namespace and reach each other overlocalhost(DB_HOSTNAME=localhost,REDIS_HOSTNAME=localhost), with only the web port2283published. It's worth being straight about why this looks different from Miniflux, which uses a shared network instead: the shared network is the standard here. A.podhas a first-deployment ordering problem — the pod service has to be active before the container services can register, which trips Quadlet's unit generation. Immich predates that rule and works; migrating it would mean switching both hostnames to container names, moving the published port off the pod ontoimmich-server.container, and tearing down a live pod holding the photo library. That hasn't earned its risk, so the deviation is written down rather than quietly maintained, and new multi-container services use the network pattern. - Vectors live in Postgres. The database is not stock Postgres but Immich's own image bundling the VectorChord and
pgvecto.rsextensions. Both the CLIP smart-search embeddings and the face-recognition vectors are stored and queried in the same database as the rest of the metadata, rather than in a separate vector store. - That database is the one container pinned to an exact tag, with auto-updates off. Its image names the extension versions explicitly (
postgres:14-vectorchord0.4.3-pgvectors0.2.0) and the unit carries noAutoUpdate=registryline at all — the deliberate opposite of the fleet's usual posture. Elsewhere an unattended update is recoverable; here it would move a vector extension underneath a live database holding every embedding in the library. Rebuilding those indexes is expensive and a corrupted one is worse, so this image moves only when a human decides it does. - GPU inference over CPU. The machine-learning container runs the
v3-openvinoimage with/dev/dripassed through, so CLIP embedding and face clustering execute on the AMD iGPU via OpenVINO. Immich also ships a CPU-only image; the GPU variant was chosen because it noticeably speeds up the initial bulk indexing of a large library.
Music Assistant
Music Assistant runs as a Home Assistant add-on and acts as a music server for Sonos speakers and other players throughout the home. It handles library management, playback queues, and streaming from various music sources.
My experience
Plex has worked well. My media is already in formats that don't require transcoding, and since I have a lifetime Plex Pass, I haven't been motivated to explore alternatives.
Immich has been a good replacement for Google Photos — I no longer need to worry about running out of cloud storage. I don't take many photos, so it hasn't seen heavy use.
AI was a big help when setting up Kometa. The collections and overlay labels on cover art make browsing Plex a more personal experience.
Music Assistant is still at the experimentation stage.