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.
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.
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
| Key | What it does |
|---|---|
| image | Which published image to run. The part after : is the tag — latest moves, a version number does not. |
| ports | Publishes a container port on the host. "8097:8096" changes only where you reach it. |
| volumes | Maps a host folder into the container. Without one, data disappears when the container is recreated. |
| environment | Settings handed to the app. Values come from .env so secrets stay out of this file. |
| depends_on | Start ordering. It waits for the container to start, not for the app inside to be ready. |
| restart | unless-stopped restarts on failure and after reboot, but respects you stopping it deliberately. |
| network_mode | service: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.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.
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
| Type | Meaning | Example |
|---|---|---|
| Required | Will not work without it. Added automatically, with an explanation. | qBittorrent needs Gluetun |
| Recommended | Works alone, but most of the value comes from the pairing. | Bazarr with Sonarr and Radarr |
| Optional | Nice additions that change nothing if absent. | Kometa alongside Jellyfin |
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.
/data, containing both downloads/ and media/. Imports are instant and free./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/
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.
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
| Symptom | Usual cause |
|---|---|
| Permission denied in logs | PUID/PGID do not match the folder's owner |
| Files owned by root appear | A container ignoring PUID, or one that ran before you set it |
| Downloads finish but never import | The *arr app can read the download but cannot write to the library |
| Container starts then exits immediately | It 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.
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.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.
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 to | Host | Port |
|---|---|---|
| qBittorrent behind a VPN | gluetun | 8080 |
| qBittorrent with no VPN | qbittorrent | 8080 |
| Jackett behind a VPN | gluetun | 9117 |
| Sonarr, from Jellyseerr | sonarr | 8989 |
| Jellyfin, from Jellyseerr | jellyfin | 8096 |
# 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.
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.
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.envCaddyfile— reverse proxy routes, when Caddy is selectedREADME.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.
--dry-run flag. It emits the same images and configuration as the web generator.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.
JellyfinEasyStreams your library to any device
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
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
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
SonarrModerateAutomated TV series management
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
- Settings → Download Clients — add qBittorrent. Host is
gluetunwhen it is behind the VPN, port 8080. - Settings → Indexers — add Jackett's Torznab feeds with its API key.
- Settings → Media Management — set the root folder and enable renaming.
- Find your own API key under Settings → General, for Jellyseerr and Bazarr.
Troubleshooting
- Cannot connect to download client — almost always
localhostwhere a container name belongs. See Networking. - Downloads complete but never import — Sonarr and qBittorrent disagree about paths. Compare
docker compose exec sonarr ls /data/downloadswith the same command against qbittorrent. - Imports copy instead of moving — separate mounts breaking hardlinks. See Storage.
Official sources
RadarrModerateAutomated film management
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
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
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
GluetunModerateVPN container with a kill switch
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_COUNTRIESblank to test. - Missing TUN device — the container needs
/dev/net/tunandNET_ADMIN. Both are generated; check they were not removed.
Official sources
qBittorrentModerateDownload client, locked to the VPN
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.
Official sources
JackettModerateTranslates indexers for the *arr apps
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.
Official sources
ImmichAdvancedSelf-hosted photo and video library
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.
Official sources
KimaModerateMusic streaming from your own library
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.
Troubleshooting
- Will not start — check
SETTINGS_ENCRYPTION_KEYis 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
Uptime KumaEasyMonitoring and status pages
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.
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
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.
Official sources
Tube ArchivistAdvancedArchives YouTube channels
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_countbeing 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.
Official sources
CaddyModerateReverse proxy with automatic HTTPS
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.
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
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.
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
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.
Official sources
Pocket IDAdvancedPasskey single sign-on
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
| App | OIDC | Reality |
|---|---|---|
| Immich | Native | Works properly, mobile app included |
| Arcane | Native | Supported directly |
| Jellyfin | Plugin | Community plugin, web browser only |
| Everything else | None | Their own logins |
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
Official sources
ArcaneEasyDocker management in a browser
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.
Troubleshooting
- Cannot see containers — the socket mount is missing or unreadable.
- Login problems after a restart —
JWT_SECRETchanged. It must stay the same between restarts.
Official sources
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.
| Service | Port | Notes |
|---|---|---|
| Caddy | 80, 443 | The only ports to forward on your router |
| Jellyfin | 8096 | |
| Jellyseerr | 5055 | |
| Sonarr | 8989 | |
| Radarr | 7878 | |
| Lidarr | 8686 | |
| Bazarr | 6767 | |
| qBittorrent | 8080 | Published by Gluetun, not itself |
| Jackett | 9117 | Published by Gluetun when a VPN is present |
| BitTorrent traffic | 6881 | TCP and UDP, via Gluetun |
| Immich | 2283 | |
| Kima | 3030 | |
| Uptime Kuma | 3001 | |
| ArchiveBox | 8000 | |
| Tube Archivist | 8001 | Maps to 8000 inside the container |
| Arcane | 3552 | Never expose publicly |
| Pocket ID | 1411 | Not published — reached through Caddy |
| Cloudflare Tunnel | None | Outbound only — nothing to publish |
"8097:8096" — which breaks nothing.Environment variables
Everything the generated stack reads from .env. Secrets are generated for you; the rest you set once.
| Variable | Purpose |
|---|---|
| CONFIG_ROOT | Where application settings and databases live. The thing most worth backing up. |
| DATA_ROOT | Shared parent for downloads and media, in the recommended layout. |
| MEDIA_ROOT | Media library, in the split layout. |
| DOWNLOADS_ROOT | Download working space, in the split layout. |
| PHOTOS_ROOT | Immich's upload folder. |
| PUID / PGID | User and group IDs that should own created files. |
| TZ | Timezone, as an IANA name such as Europe/London. |
| BASE_DOMAIN | Your domain, when Caddy is in use. |
| VPN_SERVICE_PROVIDER | Gluetun's own lowercase spelling of your provider. |
| VPN_TYPE | wireguard or openvpn. |
| WIREGUARD_PRIVATE_KEY | The private key. Not the public one. |
| WIREGUARD_ADDRESSES | Supplied with your key, e.g. 10.64.0.2/32. |
| VPN_SERVER_COUNTRIES | Optional. Blank uses the provider default. |
| TS_AUTHKEY | Tailscale auth key from the admin console. |
| CLOUDFLARE_TUNNEL_TOKEN | Tunnel token from the Cloudflare Zero Trust dashboard. |
| ARCANE_APP_URL | Where you reach Arcane. Must match the address you actually use. |
| ARCANE_ENCRYPTION_KEY | Generated. Encrypts stored values. |
| ARCANE_JWT_SECRET | Generated. Must stay stable across restarts. |
| POCKETID_URL | Must match the address you visit exactly. |
| POCKETID_ENCRYPTION_KEY | Generated. |
| IMMICH_DB_PASSWORD | Generated. Do not change after first run. |
| KIMA_SESSION_SECRET | Generated. |
| KIMA_ENCRYPTION_KEY | Generated. Required — Kima will not start without it. |
| TA_PASSWORD / TA_ES_PASSWORD | Generated, for Tube Archivist and its Elasticsearch. |
| ARCHIVEBOX_PASSWORD | Generated 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.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.
What to back up
| Priority | What | Why |
|---|---|---|
| Essential | CONFIG_ROOT, .env, docker-compose.yml | Every setting and secret. Small, and painful to rebuild. |
| Essential | Immich photos and its database | Genuinely irreplaceable. The database holds all organisation. |
| Worthwhile | Caddy's /data volume | Your certificates. Avoids re-issuing and rate limits. |
| Optional | Media library | Large, 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
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.
Legal & security
This project is a guide and a configuration generator. It does not provide, host, index or endorse copyrighted material.
You are responsible for ensuring everything you download, store, stream or share is legally obtained and that you hold the necessary rights. These applications all have legitimate uses — managing media you own, public-domain and Creative Commons material, your own recordings, and openly distributed software. Copyright law differs between countries, and nothing here is legal advice.
Security basics
- Never commit
.env, and redact secrets before pasting logs anywhere public. - Keep administrative interfaces off the public internet — Arcane above all.
- Forward ports 80 and 443 only, never application ports.
- Set a password on every service that offers one, including on a home network.
- Keep Docker and your images updated.
- Back up config and anything irreplaceable, and test the restore.
Project ownership
Every application here is a separate open-source project owned by its own developers and licensed on its own terms. This project claims no ownership of any of them, and links to each project's official sources in the module reference above. Consult those before deploying.