Skip to content

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).

plex.container
[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.

immich-server.container (excerpt)
[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 over localhost (DB_HOSTNAME=localhost, REDIS_HOSTNAME=localhost), with only the web port 2283 published. 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 .pod has 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 onto immich-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.rs extensions. 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 no AutoUpdate=registry line 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-openvino image with /dev/dri passed 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.