Documentation

How all of this actually works.

The concepts underneath a self-hosted media server, then a reference entry for every module — what it does, what it needs, how it connects to the rest, and what usually goes wrong with it.

01 · Start here

Getting started

You do not need a giant server to begin. A NAS, mini PC, used desktop, laptop, Raspberry Pi or dedicated machine can all be useful depending on what you want to run.

Suggested order
Choose hardwareInstall DockerInstall ArcaneChoose appsConfigure storageDeploy

Each of those steps has its own page. The hardware guide covers what to run it on, the platform guides cover installing Docker on Windows, macOS, Linux and NAS devices, and the setup guide walks through Arcane and the generator.

Start with one thing. Jellyfin on its own is a complete, useful server. Every module you add is another thing to configure, update and eventually debug — three services you understand beat fifteen you do not.
02 · The foundation

Docker & Compose

Docker packages an application and its dependencies into a container. Docker Compose gives you a readable way to describe several containers, their networks, volumes and configuration together.

The goal here is not to hide Docker from you. The generator makes the first Compose file approachable, then lets you inspect and learn from the result.

Anatomy of a service

Nearly every entry in your Compose file uses the same handful of keys. Once these make sense, the whole file does.

services:
  jellyfin:                                  # service name — also its hostname
    image: jellyfin/jellyfin:latest          # what to run
    container_name: jellyfin                 # fixed name, easier to type
    ports:
      - "8096:8096"                          # host:container — only the left side is yours
    volumes:
      - ${CONFIG_ROOT}/jellyfin:/config     # host path : container path
      - ${MEDIA_ROOT}:/media:ro            # :ro means read-only
    environment:
      - TZ=${TZ}                          # settings passed in from .env
    restart: unless-stopped                  # comes back after a reboot
KeyWhat it does
imageWhich published image to run. The part after : is the tag — latest moves, a version number does not.
portsPublishes a container port on the host. "8097:8096" changes only where you reach it.
volumesMaps a host folder into the container. Without one, data disappears when the container is recreated.
environmentSettings handed to the app. Values come from .env so secrets stay out of this file.
depends_onStart ordering. It waits for the container to start, not for the app inside to be ready.
restartunless-stopped restarts on failure and after reboot, but respects you stopping it deliberately.
network_modeservice:gluetun makes this container share another's network entirely. Used for the VPN kill switch.

The commands you will actually use

docker compose up -d          # start everything in the background
docker compose ps             # what is running, and its health
docker compose logs -f sonarr # follow one service's logs
docker compose pull           # fetch newer images
docker compose up -d          # recreate anything that changed
docker compose restart sonarr # bounce one service
docker compose down           # stop and remove containers (volumes survive)
docker compose exec sonarr sh # open a shell inside a container
docker compose down does not delete your data as long as it lives in a volume or bind mount. It removes containers, which are disposable by design. Adding --volumes is the flag that destroys data — avoid it unless you mean it.
03 · The bigger picture

How the stack fits together

A modular server is a collection of small tools with specific jobs. Some are independent. Others work best when connected.

Example media flow
JellyseerrSonarr / RadarrDownload clientLibraryJellyfin

Someone requests a film in Jellyseerr. Radarr searches configured indexers through Jackett, hands a result to qBittorrent, waits for it to finish, then renames and moves it into your library. Jellyfin sees the new file. Bazarr fetches subtitles for it. Nobody had to touch a terminal.

Three kinds of relationship

TypeMeaningExample
RequiredWill not work without it. Added automatically, with an explanation.qBittorrent needs Gluetun
RecommendedWorks alone, but most of the value comes from the pairing.Bazarr with Sonarr and Radarr
OptionalNice additions that change nothing if absent.Kometa alongside Jellyfin
Dependency rule. The generator distinguishes between required, recommended and optional relationships. It adds required infrastructure automatically and marks it in amber so you can see what appeared and why — and removing a module removes anything that depended on it, so you cannot end up with a torrent client and no VPN.
04 · Keep it organized

Storage & folders

Storage is one of the most important decisions in a media server, and the one that is most painful to change later. Keep configuration separate from media, and give downloads and libraries a predictable structure.

The layout that avoids doubling your disk use

Sonarr and Radarr can hardlink — giving one file two names on disk for no extra space. That only works when the download and the library sit on the same filesystem and the container sees them through a single mount.

Works — one shared mountThe container sees /data, containing both downloads/ and media/. Imports are instant and free.
Breaks — separate mountsThe container sees /downloads and /tv as unrelated places, so every import copies the file.
/srv/
├── media-server/
│   └── config/            # app settings and databases — back this up
│       ├── jellyfin/
│       ├── sonarr/
│       └── qbittorrent/
└── data/                  # mounted into containers as a single /data
    ├── downloads/
    │   ├── complete/
    │   └── incomplete/
    └── media/
        ├── movies/
        ├── tv/
        └── music/
This is expensive to change later. Restructuring paths after a library is established means re-importing everything. If you are at the beginning, use the shared layout — it is the generator's default for exactly this reason.

Naming your files

Metadata matching relies on filenames. Letting Sonarr and Radarr handle renaming avoids most problems, since they use the format media servers expect.

Movies/The Thing (1982)/The Thing (1982).mkv
TV/Show Name/Season 01/Show Name - S01E01.mkv

The year is what separates a remake from the original. film.final.copy.mkv will match something, but rarely what you wanted.

05 · The most common problem

Permissions & ownership

More setups break on file ownership than on anything else. The idea is simple once seen: a container runs as a numeric user, and that number must be allowed to touch your folders.

PUID and PGID

Linux does not really care about usernames — it cares about numeric user and group IDs. Find yours:

id
# uid=1000(you) gid=1000(you) groups=1000(you),27(sudo)

Put those numbers in .env as PUID and PGID. The LinuxServer images used for Sonarr, Radarr, Lidarr, Bazarr, Jackett and qBittorrent read them at startup and run the app as that user, so files they create belong to you rather than to root.

# make the folders match
sudo chown -R 1000:1000 /srv/data /srv/media-server
SymptomUsual cause
Permission denied in logsPUID/PGID do not match the folder's owner
Files owned by root appearA container ignoring PUID, or one that ran before you set it
Downloads finish but never importThe *arr app can read the download but cannot write to the library
Container starts then exits immediatelyIt could not create its config directory

Checking what a container thinks it is

docker compose exec sonarr id
docker compose exec sonarr ls -la /data

If the second command shows folders owned by a different number than the first reports, that is your problem in two lines.

Docker Desktop behaves differently. On Windows and macOS, ownership is handled at the virtual machine boundary and your real user ID usually does not apply. Leave PUID and PGID at 1000 there; changing them rarely helps and often confuses matters.
Do not fix this with chmod 777. It appears to work, and it makes every file on that path writable by anything on the system — including anything that gets in through an exposed service. Set the ownership correctly instead; it takes the same amount of time.
06 · Getting around

Networking

Keep services on a private network by default. Expose only what you actually need, and use a reverse proxy, VPN or mesh VPN when remote access makes sense.

Containers do not share your idea of localhost

This single misunderstanding causes more support questions than everything else combined. Inside a container, localhost means that container. Pointing Sonarr at 127.0.0.1:8080 makes it look inside itself, find nothing, and report a connection failure.

Containers on the same Compose network reach each other by service name. Sonarr finds Jellyfin at jellyfin:8096, Bazarr finds Radarr at radarr:7878.

qBittorrent is the exception that catches everyone. Because it uses network_mode: service:gluetun, it has no network identity of its own — it lives inside Gluetun's network. Other containers must therefore ask for gluetun, not qbittorrent.
Connecting toHostPort
qBittorrent behind a VPNgluetun8080
qBittorrent with no VPNqbittorrent8080
Jackett behind a VPNgluetun9117
Sonarr, from Jellyseerrsonarr8989
Jellyfin, from Jellyseerrjellyfin8096
# prove a name resolves before blaming anything else
docker compose exec sonarr ping -c2 gluetun

Sensible defaults

  • Do not expose administration interfaces casually.
  • Use strong passwords and generated secrets.
  • Keep Docker and application images updated.
  • Understand which container is allowed to reach the internet and why.

When you do want access from outside the house, the access guide compares Tailscale, Cloudflare Tunnel and a reverse proxy, and explains which services should never be published at all.

07 · Build it

How the generator works

The generator turns a selection of modules into a configuration plan, validates it, then writes the files. It runs entirely in your browser — nothing is uploaded.

Pick modulesResolve dependenciesStorageConnectionCredentialsSecretsValidateGenerate
Generated package
  • docker-compose.yml — your services
  • .env — real paths and secrets, never to be committed
  • .env.example — the same shape with placeholders, safe to share
  • .gitignore — already excluding .env
  • Caddyfile — reverse proxy routes, when Caddy is selected
  • README.md — your service addresses and first steps

Secrets come from crypto.getRandomValues, the same source your browser uses for HTTPS. Each service gets its own value; nothing is shared or reused. Anything you leave blank is written as CHANGEME rather than guessed, so a missing value is visible instead of silently wrong.

There is a command-line equivalent. install.sh does the same job in a terminal, with presets and a --dry-run flag. It emits the same images and configuration as the web generator.
08 · The toolbox

Module reference

Every module, in a consistent format: what it is for, how hard it is, what it needs, how it connects to the rest of the stack, and what usually goes wrong. Open one to see the detail.

Media serving
JellyfinEasyStreams your library to any device
Port8096
CPULow, high when transcoding
RAMLow–Medium
StorageYour whole library
Purpose

A free media server for films, TV, music and photos, with clients for browsers, phones, tablets and TV devices. No subscription and no account with anyone else.

How it fits

The end of the chain. Everything else in the media path exists to put well-named files where Jellyfin can find them.

Dependencies

None. Jellyfin is a complete server on its own, and a perfectly good place to start.

Storage

Config at ${CONFIG_ROOT}/jellyfin/config, a separate cache folder, and your media mounted read-only. Read-only is deliberate: Jellyfin has no reason to modify your files.

Configuration

Add libraries using the container path (/media/movies), not the host path. Set the content type correctly per library — films and TV need separate libraries for metadata to match.

Transcoding

Direct play costs almost nothing. Transcoding rebuilds the video in real time and is where weak hardware fails. On native Linux with an Intel chip, uncomment the device mapping and enable hardware acceleration under Dashboard → Playback:

devices:
  - /dev/dri:/dev/dri
Not available on Docker Desktop. On Windows and macOS the virtual machine cannot reach the GPU, so transcoding is CPU-only — realistically one stream.
Troubleshooting
  • Empty library — run docker compose exec jellyfin ls /media. Nothing listed means the mount is wrong, not Jellyfin.
  • Buffering — check the dashboard during playback. If it says Transcoding rather than Direct Play, that is your answer.
  • Wrong artwork — filenames. Use Name (Year) and let the *arr apps do the renaming.
Official sources
JellyseerrEasyLets your household request things
Port5055
CPULow
RAMLow
StorageSmall
Purpose

A polished request and discovery interface. Family members browse, click request, and the automation handles the rest — instead of messaging you.

How it fits

Sits in front of Sonarr and Radarr. It signs users in against Jellyfin, so people use the account they already have.

Dependencies

Recommends Jellyfin for sign-in, plus Sonarr and Radarr to fulfil requests. Without those it can still track requests, but nothing acts on them.

Configuration

Point it at jellyfin:8096, then add Sonarr at sonarr:8989 and Radarr at radarr:7878 with their API keys. Set per-user request quotas — five films a week stops an enthusiastic new user filling your disk in an afternoon.

Troubleshooting
  • Requests approved but nothing downloads — the *arr connection is the problem, not Jellyseerr. Check its Sonarr and Radarr settings and quality profiles.
  • Users cannot sign in — it authenticates against Jellyfin, so confirm they have a working Jellyfin account first.
Official sources
Automation
SonarrModerateAutomated TV series management
Port8989
CPULow
RAMLow
StorageConfig only
Purpose

Monitors TV series, searches indexers for missing or new episodes, hands them to a download client, then renames and files the result.

How it fits

The centre of the TV chain. It talks to an indexer (Jackett), a download client (qBittorrent) and writes into your library where Jellyfin picks it up.

Dependencies

Needs an indexer and a download client to do anything useful. Recommends Bazarr for subtitles and Jellyseerr for requests.

Storage

Config at ${CONFIG_ROOT}/sonarr. With the shared layout it sees one /data mount, with the library at /data/media/tv and downloads at /data/downloads.

Configuration
  1. Settings → Download Clients — add qBittorrent. Host is gluetun when it is behind the VPN, port 8080.
  2. Settings → Indexers — add Jackett's Torznab feeds with its API key.
  3. Settings → Media Management — set the root folder and enable renaming.
  4. Find your own API key under Settings → General, for Jellyseerr and Bazarr.
Troubleshooting
  • Cannot connect to download client — almost always localhost where a container name belongs. See Networking.
  • Downloads complete but never import — Sonarr and qBittorrent disagree about paths. Compare docker compose exec sonarr ls /data/downloads with the same command against qbittorrent.
  • Imports copy instead of moving — separate mounts breaking hardlinks. See Storage.
Official sources
RadarrModerateAutomated film management
Port7878
CPULow
RAMLow
StorageConfig only
Purpose

Sonarr's counterpart for films. Same design, same interface, same configuration steps.

How it fits

Identical position in the chain: indexer in, download client out, library written for Jellyfin.

Configuration

As Sonarr, with the library at /data/media/movies. Quality profiles matter more here — a 4K remux is enormous and will need transcoding on most hardware, so a 1080p profile is usually the sensible default.

Troubleshooting

The same three issues as Sonarr, with the same causes. If one is misbehaving, check whether the other is too — it usually points at the shared download client rather than either app.

Official sources
LidarrModerateAutomated music collection management
Port8686
CPULow
RAMLow
StorageConfig only
Purpose

Monitors artists and albums, fetches missing releases, and organises your music library with metadata from MusicBrainz.

How it fits

Feeds the music library that Jellyfin and Kima both read. Kima can also drive Lidarr directly, requesting downloads from its own interface.

Configuration

Library at /data/media/music. Music metadata is messier than film and TV — expect more manual matching, particularly for compilations and classical.

Troubleshooting
  • Artists will not match — Lidarr relies on MusicBrainz. If an artist is missing or wrong there, it will be wrong here.
  • Kima cannot reach it — Kima needs the URL and API key from Settings → General, and it sets up a webhook back to itself.
Official sources
BazarrEasyFetches subtitles automatically
Port6767
CPULow
RAMLow
StorageConfig only
Purpose

Watches the libraries Sonarr and Radarr manage and downloads matching subtitles in the languages you choose.

Dependencies

Recommends Sonarr and Radarr. On its own it has nothing to work with — it does not manage media itself.

Configuration

Connect to sonarr:8989 and radarr:7878 with their API keys, pick your languages, then add subtitle providers. Its paths must match what the *arr apps use, or it will look in the wrong place.

Troubleshooting
  • Finds no subtitles — most providers need a free account, configured under Settings → Providers.
  • Subtitles download but do not appear — path mismatch with Sonarr or Radarr, or Jellyfin needs a library rescan.
  • Out of sync — Bazarr can adjust timing, but a subtitle matched to a different release will never line up. Fetch one matching your file's release group.
Official sources
Downloads
GluetunModerateVPN container with a kill switch
PortPublishes for others
CPULow
RAMLow
StorageTiny
Purpose

A VPN client in a container. Other containers can route all their traffic through it, and if the VPN drops they lose their network entirely rather than falling back to your real connection.

How it fits

A network boundary rather than an application. qBittorrent and Jackett share its network stack, which is why Gluetun publishes their ports and why other containers address them as gluetun.

Configuration
VPN_SERVICE_PROVIDER=mullvad     # lowercase, Gluetun's own spelling
VPN_TYPE=wireguard
WIREGUARD_PRIVATE_KEY=...        # the private key, not the public one
WIREGUARD_ADDRESSES=10.64.0.2/32
SERVER_COUNTRIES=                # blank = provider default
Verifying it
docker compose exec gluetun wget -qO- https://ipinfo.io/ip
curl -s https://ipinfo.io/ip     # must be different
Troubleshooting
  • Unhealthy or restarting — read the logs, they are unusually clear. Provider name spelling and wrong key type are the top two causes.
  • No servers found — a country with none available. Leave SERVER_COUNTRIES blank to test.
  • Missing TUN device — the container needs /dev/net/tun and NET_ADMIN. Both are generated; check they were not removed.
A VPN routes traffic. It does not make files safe, verify what you download, or make unlawful activity lawful.
Official sources
qBittorrentModerateDownload client, locked to the VPN
Port8080 via Gluetun
CPULow
RAMLow–Medium
StorageWorking space
Purpose

A BitTorrent client with a web interface, driven by the *arr apps rather than used directly most of the time.

Dependencies

Requires Gluetun. The generator will not produce a qBittorrent service without it, and removing Gluetun removes qBittorrent too.

How it fits

Uses network_mode: service:gluetun, so it has no network of its own. This is what makes the kill switch real: no VPN, no connection.

First login

Recent versions generate a temporary password on first start rather than shipping a fixed default:

docker compose logs qbittorrent | grep -i password

Sign in as admin, then change it under Tools → Options → Web UI.

Configuration

Set the save path to /data/downloads with the shared layout, and keep incomplete downloads in a subfolder so the *arr apps do not try to import half a file.

Troubleshooting
  • Web interface unreachable — check Gluetun first. If it is unhealthy, qBittorrent is unreachable by design.
  • Stalled at zero — often no forwarded port, which limits you to peers who accept incoming connections. Some providers support it through Gluetun, many do not.
  • Paused after a disk-full event — free space, then resume manually.
Downloaded files can be malicious. Filenames, extensions and comments guarantee nothing. Only obtain content you are legally entitled to, and treat everything as untrusted until verified.
Official sources
JackettModerateTranslates indexers for the *arr apps
Port9117
CPULow
RAMLow
StorageConfig only
Purpose

Converts many different tracker search APIs into one standard format (Torznab) that Sonarr, Radarr and Lidarr understand.

How it fits

Sits between the *arr apps and whatever indexers you configure. Routed through Gluetun when a VPN is present.

Configuration

Add indexers in Jackett, copy each Torznab feed URL and Jackett's API key, then paste them into the *arr apps under Settings → Indexers. When behind the VPN, the *arr apps reach it at gluetun:9117.

Troubleshooting
  • Indexer test fails — many require an account, and some use protection that Jackett cannot pass automatically.
  • Works in Jackett but not in Sonarr — usually the host name. Use the container name, not localhost.
You remain responsible for which indexers you use and whether accessing them is lawful where you are.
Official sources
Photos & music
ImmichAdvancedSelf-hosted photo and video library
Port2283
CPUMedium
RAM4 GB+
StorageLarge, growing
Purpose

A replacement for cloud photo services: phone backup, timeline, albums, search, face recognition and sharing, all on your hardware.

How it fits

Independent of the media chain. It brings its own Postgres database, Redis cache and machine-learning service — four containers from one module.

Storage

Photos at ${PHOTOS_ROOT}, database under ${CONFIG_ROOT}/immich/postgres. Both matter: the files are your photos, the database is every album, face and piece of metadata.

Configuration

Create your account on first visit, install the mobile app and point it at your server, then enable background backup. Supports OIDC single sign-on — see the access guide.

Troubleshooting
  • Slow first import — normal. Thumbnails and face recognition for a large library take hours.
  • Will not connect to the database — most often a changed password. The existing database still expects the old one.
  • Machine learning failing — usually memory. It is the heaviest part of the stack.
Back up the database, not just the photos. The files alone are not your library — losing Postgres means keeping every image but losing all albums, faces and organisation.
Official sources
KimaModerateMusic streaming from your own library
Port3030
CPUMedium
RAMMedium
StorageYour music
Purpose

A streaming-service experience over your own collection: playlists, discovery, podcasts, audiobooks via Audiobookshelf, and audio analysis for mood and similarity.

How it fits

Reads the same music library Lidarr manages, and can drive Lidarr to request new material. Implements the OpenSubsonic API, so native clients such as Symfonium, Amperfy and DSub work against it.

Storage

Music mounted at /music, everything else in a named volume kima_data. The image runs its own Postgres and Redis internally, so there are no extra services to add — the upstream docs recommend a named volume rather than a bind mount for /data.

Configuration

SETTINGS_ENCRYPTION_KEY is required and the container will not start without it; the generator produces one. extra_hosts is included so Lidarr's webhook can reach it on Linux.

Kima's built-in downloader is not behind the VPN. It includes Soulseek support, and unlike qBittorrent it runs on the normal network because it also serves your library. If you enter Soulseek credentials, that peer-to-peer traffic uses your ordinary connection. Leaving the integration unconfigured avoids this entirely; everything else still works.
Troubleshooting
  • Will not start — check SETTINGS_ENCRYPTION_KEY is set.
  • Permission errors on /data — the docs recommend a named volume; bind mounts need the postgres and redis subdirectories created and owned correctly.
  • Analysis is slow — audio analysis is CPU-heavy. GPU acceleration is optional and documented upstream.
Official sources
Monitoring & archives
Uptime KumaEasyMonitoring and status pages
Port3001
CPUVery low
RAMVery low
StorageTiny
Purpose

Watches your services and tells you when one stops responding, via Telegram, Discord, email and many others. Can publish a status page.

Configuration

Add an HTTP monitor per service using container names — http://jellyfin:8096 — so it tests the internal path rather than your router.

Monitoring that lives on the machine it monitors cannot tell you the machine is down. This is the strongest argument for a cheap VPS: run Uptime Kuma there, pointed back at home.
Troubleshooting

Monitors failing while services work usually means the wrong hostname. Notifications need testing once when you add them — most people discover a broken alert channel at the worst moment.

Official sources
ArchiveBoxModerateSaves web pages before they vanish
Port8000
CPUMedium
RAMMedium
StorageGrows steadily
Purpose

Archives URLs, bookmarks and RSS feeds as local copies in several formats at once — HTML, PDF, screenshot, WARC and extracted text.

Configuration

The generated service keeps public access off (PUBLIC_INDEX, PUBLIC_SNAPSHOTS and PUBLIC_ADD_VIEW all False) and sets an admin password. Turn those on only deliberately.

Troubleshooting

Some sites block archiving or need authentication. Chrome-based captures are slower on ARM. Storage grows faster than expected — each snapshot keeps several formats.

Archiving does not override copyright. Keeping a personal copy is a different thing from redistributing it.
Official sources
Tube ArchivistAdvancedArchives YouTube channels
Port8001
CPUMedium
RAMMedium–High
StorageLarge
Purpose

Subscribes to channels, downloads on a schedule using yt-dlp, and gives you a searchable interface over the archive.

How it fits

Independent, but brings Elasticsearch and Redis with it — three containers from one module, and the heaviest module here after Immich.

Troubleshooting
  • Elasticsearch will not start — usually memory limits, or vm.max_map_count being too low on the host.
  • Fails on ARM — the Elasticsearch image has limited ARM support. This is the one module likely to disappoint on a Raspberry Pi or Apple Silicon.
  • Downloads failing — yt-dlp needs regular updating as YouTube changes.
You are responsible for complying with YouTube's terms and applicable copyright law.
Official sources
Connection
CaddyModerateReverse proxy with automatic HTTPS
Port80, 443
CPUVery low
RAMLow
StorageCertificates
Purpose

Sits in front of your services, terminates HTTPS, and routes requests by hostname. Obtains and renews certificates without being asked.

How it fits

The only container that should face the internet. Everything else stays on the internal network and is reached through it.

Requirements

A domain whose DNS already points at your machine, and ports 80 and 443 forwarded — those two only. Port 80 is needed for certificate issuance, not for serving your site.

Configuration
jellyfin.example.com {
    reverse_proxy jellyfin:8096
}

Targets use container names because Caddy shares the Docker network. The generator writes a Caddyfile containing only the services that are safe to publish.

Some services are deliberately excluded. Arcane, qBittorrent, Sonarr, Radarr, Lidarr, Bazarr and Jackett are never proxied — they hold the Docker socket, can write files anywhere, or authenticate weakly. Reach those over Tailscale or your LAN.
Troubleshooting
  • Certificate errors — DNS not resolving yet, or port 80 not reaching the server. Both show clearly in the logs.
  • Rate limited — repeated failures hit issuer limits. Use the staging endpoint while testing.
  • 502 errors — the target container name or port is wrong, or that container is down.
Backups

Keep the /data volume. It holds your certificates, and losing it means re-issuing them.

Official sources
TailscaleEasyPrivate mesh network, nothing published
PortNone published
CPUVery low
RAMVery low
StorageTiny
Purpose

Builds a private network between your own devices. Your phone behaves as though it is at home, with nothing exposed to the internet at all.

How it fits

An alternative to publishing services, and the recommended way to reach the administrative tools that should never be public. You can run both: Caddy for what the household uses, Tailscale for everything else.

Installing Tailscale on the host is usually simpler than running this container, and covers every service on the machine at once. The container exists for cases where you cannot install on the host.
Configuration

Needs an auth key from the Tailscale admin console under Settings → Keys. Requires NET_ADMIN, SYS_MODULE and /dev/net/tun.

Troubleshooting

If it never appears in your device list, the auth key is missing, expired, or already used. Keys can be set as reusable and given an expiry when you create them.

Official sources
Cloudflare TunnelModeratePublic access with no open ports
PortNone published
CPUVery low
RAMVery low
StorageNone
Purpose

Runs an outbound-only connection to Cloudflare, who then serve your services on your domain. Nothing is opened on your router, and it works behind CGNAT.

How it fits

An alternative to Caddy rather than a companion — both publish services externally. Routing is configured in Cloudflare's Zero Trust dashboard, so the Compose file contains no routes at all.

Configuration

Needs only a tunnel token from the dashboard. Public hostnames point at container names, such as jellyseerr:5055.

Cloudflare terminates TLS, so traffic is decrypted at their edge — not end-to-end. The free plan also caps requests at 100 MB, which breaks larger Immich uploads.
Media streaming is unsettled. The much-quoted Section 2.8 was removed in 2023, but a CDN-specific restriction replaced it and self-hosted video is not covered by the exemption for Cloudflare-hosted content. Neither clearly banned nor clearly allowed — read the current terms before putting Jellyfin on it.
Official sources
Authentication
Pocket IDAdvancedPasskey single sign-on
Port1411, proxy only
CPUVery low
RAMLow
StorageTiny
Purpose

A small OpenID Connect identity provider built entirely around passkeys. One login for apps that support OIDC, with no passwords stored anywhere.

Dependencies

Requires Caddy or another reverse proxy. Passkeys only work over HTTPS on a real domain — this is a browser rule, not a setting. It publishes no host port and is reachable only through the proxy.

What it can actually cover
AppOIDCReality
ImmichNativeWorks properly, mobile app included
ArcaneNativeSupported directly
JellyfinPluginCommunity plugin, web browser only
Everything elseNoneTheir own logins
The Jellyfin SSO plugin authenticates via the web UI or Quick Connect only. Swiftfin, Android TV, Roku and Kodi cannot use the redirect flow, so anyone watching on a TV still needs a normal Jellyfin password. The original plugin was archived in May 2026; a maintained fork continues it. Native OIDC is proposed upstream for a future major release. Setup steps →
Configuration

APP_URL must exactly match the address you visit — passkeys bind to that origin. TRUST_PROXY=true makes it honour Caddy's forwarded headers. Recovery if you lose access:

docker compose exec pocket-id \
  /app/pocket-id one-time-access-token admin
Register two passkeys on separate devices. One passkey on one phone means losing that phone locks you out of every connected app at once. This is the single most important step.
Official sources
Management
ArcaneEasyDocker management in a browser
Port3552
CPUVery low
RAMLow
StorageSmall
Purpose

A web interface for Docker: start, stop, update and inspect containers, manage Compose projects, and read logs without the command line.

How it fits

How you operate everything else. Deploy your generated stack as a project, then use it for day-to-day management and updates.

Configuration

Needs two generated secrets, ENCRYPTION_KEY and JWT_SECRET, plus APP_URL matching where you reach it. Mounts the Docker socket, and your stack directory as its projects folder.

Arcane holds the Docker socket, which is equivalent to root on the host. Anyone who reaches its interface can control the whole machine. Keep it on your LAN or behind Tailscale, never proxy it publicly, and never forward port 3552.
Troubleshooting
  • Cannot see containers — the socket mount is missing or unreadable.
  • Login problems after a restartJWT_SECRET changed. It must stay the same between restarts.
Official sources
09 · Reference

Port reference

Default host ports for every module. Change only the left side of a mapping if something conflicts — the right side is fixed inside the container.

ServicePortNotes
Caddy80, 443The only ports to forward on your router
Jellyfin8096
Jellyseerr5055
Sonarr8989
Radarr7878
Lidarr8686
Bazarr6767
qBittorrent8080Published by Gluetun, not itself
Jackett9117Published by Gluetun when a VPN is present
BitTorrent traffic6881TCP and UDP, via Gluetun
Immich2283
Kima3030
Uptime Kuma3001
ArchiveBox8000
Tube Archivist8001Maps to 8000 inside the container
Arcane3552Never expose publicly
Pocket ID1411Not published — reached through Caddy
Cloudflare TunnelNoneOutbound only — nothing to publish
On a Synology NAS, DSM reserves many ports. If a container will not start because a port is in use, shift the host side — "8097:8096" — which breaks nothing.

Environment variables

Everything the generated stack reads from .env. Secrets are generated for you; the rest you set once.

VariablePurpose
CONFIG_ROOTWhere application settings and databases live. The thing most worth backing up.
DATA_ROOTShared parent for downloads and media, in the recommended layout.
MEDIA_ROOTMedia library, in the split layout.
DOWNLOADS_ROOTDownload working space, in the split layout.
PHOTOS_ROOTImmich's upload folder.
PUID / PGIDUser and group IDs that should own created files.
TZTimezone, as an IANA name such as Europe/London.
BASE_DOMAINYour domain, when Caddy is in use.
VPN_SERVICE_PROVIDERGluetun's own lowercase spelling of your provider.
VPN_TYPEwireguard or openvpn.
WIREGUARD_PRIVATE_KEYThe private key. Not the public one.
WIREGUARD_ADDRESSESSupplied with your key, e.g. 10.64.0.2/32.
VPN_SERVER_COUNTRIESOptional. Blank uses the provider default.
TS_AUTHKEYTailscale auth key from the admin console.
CLOUDFLARE_TUNNEL_TOKENTunnel token from the Cloudflare Zero Trust dashboard.
ARCANE_APP_URLWhere you reach Arcane. Must match the address you actually use.
ARCANE_ENCRYPTION_KEYGenerated. Encrypts stored values.
ARCANE_JWT_SECRETGenerated. Must stay stable across restarts.
POCKETID_URLMust match the address you visit exactly.
POCKETID_ENCRYPTION_KEYGenerated.
IMMICH_DB_PASSWORDGenerated. Do not change after first run.
KIMA_SESSION_SECRETGenerated.
KIMA_ENCRYPTION_KEYGenerated. Required — Kima will not start without it.
TA_PASSWORD / TA_ES_PASSWORDGenerated, for Tube Archivist and its Elasticsearch.
ARCHIVEBOX_PASSWORDGenerated admin password.
.env holds real secrets in plain text. It is written with restrictive permissions and listed in .gitignore. Never commit it, and redact it before pasting logs or configuration anywhere public.
10 · Keep it healthy

Updates & backups

A server you never maintain becomes a server you cannot trust. Neither of these takes long once you have a rhythm.

Updating

docker compose pull      # fetch newer images
docker compose up -d     # recreate anything that changed
docker image prune       # clear out the replaced ones

In Arcane it is pull, then redeploy. Monthly is a reasonable rhythm; anything reachable from outside your network deserves more urgency.

Read release notes before major version jumps, particularly for Immich, which occasionally requires migration steps. Automatic updaters like Watchtower are tempting, but an update that breaks something at 4am while you are asleep is worse than one you are present for.

What to back up

PriorityWhatWhy
EssentialCONFIG_ROOT, .env, docker-compose.ymlEvery setting and secret. Small, and painful to rebuild.
EssentialImmich photos and its databaseGenuinely irreplaceable. The database holds all organisation.
WorthwhileCaddy's /data volumeYour certificates. Avoids re-issuing and rate limits.
OptionalMedia libraryLarge, and usually re-acquirable.
# stop first so databases are copied in a consistent state
docker compose stop
tar czf backup-$(date +%F).tar.gz /srv/media-server
docker compose start
A backup you have never restored is not a backup. Test it once, deliberately, while nothing is wrong.

Glossary

Bind mount
A host folder mapped into a container, so you can see the files yourself. Used for config and media here.
Named volume
Storage Docker manages for you, not visible as an ordinary folder. Used where an application expects to own its own data, as Kima does.
Hardlink
A second name for the same file on disk, costing no extra space. What lets imports be instant instead of copying.
Kill switch
Here, the effect of network_mode: service:gluetun — if the VPN drops, the container loses its network rather than falling back to your real connection.
Torznab
The standard search format Jackett exposes so the *arr apps can talk to many different indexers the same way.
Direct play
The client can handle the file as-is, so the server just sends it. Costs almost no CPU.
Transcoding
Rebuilding video in real time for a client that cannot play the original. Expensive, and where weak hardware fails.
Reverse proxy
A server that receives requests and forwards them to the right internal service, handling HTTPS on the way.
OIDC
OpenID Connect. The standard that lets one login work across several applications.
Passkey
A key pair held by your device and unlocked biometrically. No password exists to leak or reuse.
PUID / PGID
The numeric user and group a container runs as. Must match the owner of your folders.
CGNAT
When your provider does not give you a real public address, making port forwarding impossible. Tailscale sidesteps it.