Why one machine for four libraries
Leaving Plex, Google Photos, Spotify or Audible almost always leads to the same layout: one service per media type, each on its own small server or on the living-room NAS. The result is four updates to follow, four certificates and four backups to plan. A single VPS brings this down to one entry point, one Docker network and one backup plan.
The hard part is not installation, which our guides on Immich, Jellyfin, Navidrome and Audiobookshelf already cover. The hard part is sizing: Immich and video transcoding overwhelm a small server, while Navidrome and Audiobookshelf go almost unnoticed. This article answers a single question: which machine, for which library.
What you gain by putting it all together
- One entry point — one reverse proxy, one subdomain per service, certificates obtained and renewed automatically.
- Shared memory — Navidrome and Audiobookshelf use little, and the memory one leaves free helps the other's peaks.
- A single backup plan — four configuration folders, one PostgreSQL dump, one script.
- Vertical scaling — when the library grows, you increase RAM, vCPU and disk on the same VPS without reinstalling the stack.
- Full root access — you choose the distribution, tune the kernel and install whatever the stack requires.
The memory and CPU budget, service by service
Only the Immich and Jellyfin figures come from official documentation; the other lines are starting settings, to be measured on your own machine. Take your readings once the stack is running with docker stats --no-stream, then adjust.
Immich is the dominant item. Its documentation gives 6 GB of RAM as a minimum and 8 GB recommended, 2 cores minimum and 4 recommended. It also states that on a 4 GB machine Immich can run with machine learning disabled, and that the PostgreSQL database needs at least 2 GB if you set Docker resource limits. Since version 3, the machine learning image on amd64 requires the x86-64-v2 instruction-set level: check that the plan you choose exposes it.
On the CPU side, it is the initial indexing that saturates: face detection and semantic search run on every available core for hours, then calm returns. The other services stay almost invisible as long as you do not transcode.
Reservation budget per service
Scroll the table
| Service | RAM to reserve | What makes it grow |
|---|---|---|
| Caddy (reverse proxy) | Starting setting: 100 MB, to measure | Almost nothing: the number of subdomains barely changes it |
| Navidrome | Starting setting: 250 MB, to measure | Scans of a large library and clients that request audio transcoding |
| Audiobookshelf | Starting setting: 250 MB, to measure | Automatically downloaded podcasts and metadata scans |
| Jellyfin on direct play | Starting setting: 1 GB, to measure (Jellyfin documents 4 GB as possibly enough for a headless Linux server, 8 GB recommended on an average deployment) | Library scans and images generated on the initial pass |
| Jellyfin with software transcoding | No official per-stream figure: measure with a test stream | Every stream converted in parallel, especially HEVC or burned-in subtitles |
| Immich (server, ML, PostgreSQL, Valkey) | 6 GB minimum, 8 GB recommended | Initial indexing, face recognition, semantic search |
| Full stack, single machine | At least 8 GB (Immich's recommended value), 16 GB for margin: an estimate to check | Immich jobs running while a film is being transcoded |
The disk budget: calculate in GB before choosing
Disk is the first ceiling of a multimedia VPS, and it can be calculated. For video, the formula is: bitrate in Mb/s multiplied by duration in seconds, divided by 8. A two-hour film at 8 Mb/s weighs about 7 GB, at 4 Mb/s about 3.6 GB. For music, a 45-minute album in MP3 at 320 kb/s is about 110 MB; in FLAC, count 300 to 500 MB instead. A 10-hour audiobook at 64 kb/s weighs close to 290 MB.
For photos, start from 3 MB per shot: 10,000 photos therefore take about 30 GB, phone videos on top. Immich's documentation adds that thumbnails and transcoded videos grow the photo library by 10 to 20% on average.
Then separate what must be fast from what must be large. Immich's PostgreSQL database typically weighs 1 to 3 GB, must live on a local SSD and never on a network share. The same goes for the configuration of Jellyfin and Audiobookshelf: Audiobookshelf explicitly advises against placing its configuration folder on a network share, at the risk of corrupting its SQLite database. The media themselves can go on a separate, larger volume.
Do not transcode: the rule that protects a small VPS
Jellyfin distinguishes four playback modes, from lightest to heaviest: direct play, remux, direct stream and transcode. Its documentation describes direct play as delivering the file without modification, with almost no additional load on the server; video transcoding is the most expensive mode.
Without a graphics card, every video transcode goes through the CPU, and Jellyfin's documentation reminds that a GPU is always recommended for this. It also warns that HEVC, VP9 and AV1 can be very demanding in software decoding, and that converting HDR content to SDR (tone-mapping) always uses a software encoder without a GPU.
The practical rule comes down to three actions. Store your films in a format your devices play natively, typically H.264 in MP4 or MKV. Convert problem files once, upstream, rather than on every playback. And use Jellyfin's per-user setting to forbid audio and video transcoding for accounts that do not need it. To check that a stream costs nothing, start a playback and watch the dashboard: the playback type is shown there, and the CPU should stay at rest.
Assemble the stack: one network, one proxy, one subdomain per service
Point four subdomains to the VPS
Create four A records (jellyfin, music, books, photos) pointing to the VPS IPv4. Caddy obtains certificates only if the A or AAAA records point to the server and ports 80 and 443 are open from the outside.
Write the compose file for the three light services
In
/srv/stack/media/compose.yaml, declare Jellyfin, Navidrome and Audiobookshelf. No port is published: only Caddy talks to theedgenetwork. Media is mounted read-only for Jellyfin and Navidrome.services:jellyfin:image: jellyfin/jellyfinuser: 1000:1000volumes:- /srv/stack/jellyfin/config:/config- /srv/stack/jellyfin/cache:/cache- /srv/media/video:/media:rorestart: unless-stoppednetworks: [edge]navidrome:image: navidrome/navidrome:latestuser: 1000:1000volumes:- /srv/stack/navidrome/data:/data- /srv/media/music:/music:rorestart: unless-stoppednetworks: [edge]audiobookshelf:image: ghcr.io/advplyr/audiobookshelf:latestuser: 1000:1000volumes:- /srv/stack/audiobookshelf/config:/config- /srv/stack/audiobookshelf/metadata:/metadata- /srv/media/audiobooks:/audiobooksrestart: unless-stoppednetworks: [edge]Add Caddy to the same file
Complete the file with the proxy service, then the declarations for the external network and the certificates volume:
caddy:image: caddy:2ports:- "80:80"- "443:443"volumes:- ./Caddyfile:/etc/caddy/Caddyfile:ro- caddy_data:/datarestart: unless-stoppednetworks: [edge]networks:edge:external: truevolumes:caddy_data:Write the Caddyfile, one block per service
In
/srv/stack/media/Caddyfile, a domain name is enough to trigger automatic TLS, and Caddy redirects HTTP to HTTPS on its own:jellyfin.your-domain.com {reverse_proxy jellyfin:8096}music.your-domain.com {reverse_proxy navidrome:4533}books.your-domain.com {reverse_proxy audiobookshelf:80}photos.your-domain.com {reverse_proxy immich_server:2283}Install Immich with its official compose file
Download the two files of the latest release, and do not edit the compose file itself:
cd /srv/stack/immichwget -O docker-compose.yml https://github.com/immich-app/immich/releases/latest/download/docker-compose.ymlwget -O .env https://github.com/immich-app/immich/releases/latest/download/example.envIn
.env, set the three values below (the password only accepts letters and digits):UPLOAD_LOCATION=/srv/photos/libraryDB_DATA_LOCATION=/srv/stack/immich/postgresDB_PASSWORD=<mot-de-passe-A-Za-z0-9>Start and check every entry point
Start both stacks, wait for the certificates to be issued, then test HTTP headers and TLS. You should get a 2xx or 3xx code over HTTPS, never a certificate error:
cd /srv/stack/media && docker compose up -dcd /srv/stack/immich && docker compose up -dcurl -I https://jellyfin.your-domain.comcurl -I https://photos.your-domain.comMeasure, then tune Immich
During indexing, read
docker stats --no-streamand the CPU load. If memory nears the ceiling, disable a machine learning feature (smart search, facial recognition) in the administration settings, or lower task concurrency without raising it above the number of cores.
Back up every service, restore before you need to
A stack of four applications has four ways to lose data. The classic trap is backing up photos without the database: Immich does not scan its library folder, it relies on its database to know what exists. The documentation distinguishes the original folders (library, upload, profile), which must be backed up, from regenerable folders (thumbs, encoded-video).
Immich also produces its own database dumps, by default every day at 2 a.m., keeping the last 14 in the backups folder of the upload location. They hold metadata only, never the photos. For consistency, stop the server container during the backup; otherwise, back up the database first, then the files.
Restoration, on a test machine, follows the official procedure (then inject the dump into psql). Careful: down -v erases all Immich data on the machine where you run it:
docker compose down -vdocker compose pulldocker compose createdocker start immich_postgres
Backup plan per service
- Immich, database — a compressed dump off the VPS:
docker exec -t immich_postgres pg_dump --clean --if-exists --dbname=immich --username=postgres | gzip > "/srv/backup/immich-db.sql.gz", adapting database name and user to your.env. - Immich, files —
library,uploadandprofilecopied withrsyncto remote storage;thumbsandencoded-videocan be regenerated. - Jellyfin — the
/srv/stack/jellyfin/configfolder (libraries, users, metadata), container stopped while archiving. The cache rebuilds itself. - Navidrome —
/srv/stack/navidrome/data(database, playlists, cache); the music itself is your source of truth and is backed up like any folder of files. - Audiobookshelf —
/configand/metadata: the SQLite database and listening progress live in the first, internal backups in the second. - Restoration gets tested — replay Immich's official procedure, detailed above, on a test machine before relying on it.
Troubleshooting: saturated memory, denied permissions, cut uploads
Failures in this stack fall into two families: memory, when Immich and a transcode fight over the same gigabytes, and permissions, when a non-root container cannot read or write a mounted folder. For the first, start by looking for a kernel trace with dmesg -T | grep -i "out of memory": if an Immich process appears there, the kernel killed it for lack of memory, and the container restarts in a loop. For the second, always compare the container user's ID with the owner of the mounted folders.
Errors encountered and their fix
- Immich restarts in a loop on a small plan — the kernel kills the machine learning container (look for
out of memoryindmesg). Disable smart search and facial recognition, or move to a plan with at least 8 GB. can't set healthcheck.start_interval as feature require Docker Engine v25 or later— your Docker is too old. Update it, or comment out thestart_intervalline in thedatabasesection of the compose file, as Immich's documentation indicates.- Video uploads cut after one minute — Immich's documentation notes a 499 code behind Traefik with the default 60-second read timeout, and recommends 600 seconds. Check your proxy's timeout.
- Navidrome:
unable to open database file— the/datafolder is not writable. Give ownership to the ID set by theuserdirective. - Navidrome starts but the library stays empty, with
permission deniedin the logs — the music folder is not readable: applychmod -R a+rXon/srv/media/music. - Jellyfin does not see the films — the container's user 1000 cannot read the mounted folder. Test with
docker exec jellyfin ls /mediaand fix the permissions;PUIDandPGIDdo not work with these images, only theuserdirective counts.
Which plan for which library
Scroll the table
| Library profile | Recommended plan | Margin and limit |
|---|---|---|
| Music, audiobooks and a few films on direct play, Immich without machine learning, one or two users | VPS Start: 2 vCPU, 4 GB, 50 GB SSD, at 99 DH/month | Tight memory: Immich stays without ML and transcoding is best avoided; disk limits you to a few films (≈ 7 GB for a two-hour film at 8 Mb/s) |
| Family photo library of about 10,000 shots (30 to 36 GB) with ML on, music and audiobooks, direct play | VPS Power: 4 vCPU, 8 GB, 100 GB SSD, at 299 DH/month | Reaches the 8 GB recommended for Immich alone; initial indexing slows the rest |
| Full four-service stack, two or three users, an occasional transcode | VPS Business: 8 vCPU, 16 GB, 200 GB SSD, at 499 DH/month | Comfortable; disk becomes the ceiling as soon as video exceeds a few hundred GB |
| Several streams transcoded at the same time | VPS Business at minimum, plus upstream file conversion | Without a GPU, adding vCPUs is not enough: convert once rather than transcode on every playback |
Measure before paying more
Start with the plan the table indicates, run Immich's indexing and read memory with docker stats --no-stream during a peak day. If the margin exceeds 30%, you have the right size; if the kernel kills a container, move up a plan. Vertical scaling of the VPS avoids reinstalling: RAM and disk grow, your /srv folders and compose files stay in place.
Official documentation and next steps
The values in this guide come from the vendors' documentation: Immich requirements, Immich backup and restore, Jellyfin hardware selection, Jellyfin transcoding, Navidrome with Docker and Caddy's automatic HTTPS.
To pick a proxy other than Caddy, read our Caddy, Traefik and Nginx Proxy Manager comparison. If you are leaving Plex, the process is detailed in our migration guide to Jellyfin and Navidrome.