Deployment guide

Size a VPS for Jellyfin, Navidrome and Immich

Deploy on a VPS Cloud →

Tutorial

Size a VPS for Jellyfin, Navidrome and Immich

Self-hosting13 min read9 steps

Putting movies, music, photos and audiobooks on a single server looks easy, until Immich starts indexing your faces and memory collapses. A sizing mistake costs you both ways: a plan that is too tight and restarts in a loop, or a plan that is too large and paid for every month. This guide puts numbers on each service, sets the rule that avoids video transcoding, budgets disk per library and assembles everything behind a single reverse proxy. It ends with a table that maps your library profile to a VPS plan.

Contents· Why one machine for four libraries1/14
  1. 01Why one machine for four libraries
  2. 02What you gain by putting it all together
  3. 03The memory and CPU budget, service by service
  4. 04Reservation budget per service
  5. 05The disk budget: calculate in GB before choosing
  6. 06Do not transcode: the rule that protects a small VPS
  7. 07Assemble the stack: one network, one proxy, one subdomain per service
  8. 08Back up every service, restore before you need to
  9. 09Backup plan per service
  10. 10Troubleshooting: saturated memory, denied permissions, cut uploads
  11. 11Errors encountered and their fix
  12. 12Which plan for which library
  13. 13Measure before paying more
  14. 14Official documentation and next steps

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

ServiceRAM to reserveWhat makes it grow
Caddy (reverse proxy)Starting setting: 100 MB, to measureAlmost nothing: the number of subdomains barely changes it
NavidromeStarting setting: 250 MB, to measureScans of a large library and clients that request audio transcoding
AudiobookshelfStarting setting: 250 MB, to measureAutomatically downloaded podcasts and metadata scans
Jellyfin on direct playStarting 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 transcodingNo official per-stream figure: measure with a test streamEvery stream converted in parallel, especially HEVC or burned-in subtitles
Immich (server, ML, PostgreSQL, Valkey)6 GB minimum, 8 GB recommendedInitial indexing, face recognition, semantic search
Full stack, single machineAt least 8 GB (Immich's recommended value), 16 GB for margin: an estimate to checkImmich 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

  1. Create the folders, permissions and shared network

    Separate media, photos and configurations, give ownership to user 1000 and create the network all containers will share:

    sudo mkdir -p /srv/media/video /srv/media/music /srv/media/audiobooks /srv/photos /srv/stack/media /srv/stack/immich
    sudo mkdir -p /srv/stack/jellyfin/config /srv/stack/jellyfin/cache /srv/stack/navidrome/data /srv/stack/audiobookshelf/config /srv/stack/audiobookshelf/metadata
    sudo chown -R 1000:1000 /srv/stack /srv/photos
    sudo chmod -R a+rX /srv/media
    docker network create edge

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

  3. 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 the edge network. Media is mounted read-only for Jellyfin and Navidrome.

    services:
    jellyfin:
    image: jellyfin/jellyfin
    user: 1000:1000
    volumes:
    - /srv/stack/jellyfin/config:/config
    - /srv/stack/jellyfin/cache:/cache
    - /srv/media/video:/media:ro
    restart: unless-stopped
    networks: [edge]
    navidrome:
    image: navidrome/navidrome:latest
    user: 1000:1000
    volumes:
    - /srv/stack/navidrome/data:/data
    - /srv/media/music:/music:ro
    restart: unless-stopped
    networks: [edge]
    audiobookshelf:
    image: ghcr.io/advplyr/audiobookshelf:latest
    user: 1000:1000
    volumes:
    - /srv/stack/audiobookshelf/config:/config
    - /srv/stack/audiobookshelf/metadata:/metadata
    - /srv/media/audiobooks:/audiobooks
    restart: unless-stopped
    networks: [edge]

  4. 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:2
    ports:
    - "80:80"
    - "443:443"
    volumes:
    - ./Caddyfile:/etc/caddy/Caddyfile:ro
    - caddy_data:/data
    restart: unless-stopped
    networks: [edge]
    networks:
    edge:
    external: true
    volumes:
    caddy_data:

  5. 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
    }

  6. 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/immich
    wget -O docker-compose.yml https://github.com/immich-app/immich/releases/latest/download/docker-compose.yml
    wget -O .env https://github.com/immich-app/immich/releases/latest/download/example.env

    In .env, set the three values below (the password only accepts letters and digits):

    UPLOAD_LOCATION=/srv/photos/library
    DB_DATA_LOCATION=/srv/stack/immich/postgres
    DB_PASSWORD=<mot-de-passe-A-Za-z0-9>

  7. Attach Immich to the shared network

    Create /srv/stack/immich/docker-compose.override.yml. Docker Compose merges it automatically with the official file, which stays intact and replaceable at every update:

    services:
    immich-server:
    networks: [default, edge]
    networks:
    edge:
    external: true

  8. 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 -d
    cd /srv/stack/immich && docker compose up -d

    curl -I https://jellyfin.your-domain.com
    curl -I https://photos.your-domain.com

  9. Measure, then tune Immich

    During indexing, read docker stats --no-stream and 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 -v
docker compose pull
docker compose create
docker 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, upload and profile copied with rsync to remote storage; thumbs and encoded-video can be regenerated.
  • Jellyfin — the /srv/stack/jellyfin/config folder (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 — /config and /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 memory in dmesg). 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 the start_interval line in the database section 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 /data folder is not writable. Give ownership to the ID set by the user directive.
  • Navidrome starts but the library stays empty, with permission denied in the logs — the music folder is not readable: apply chmod -R a+rX on /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 /media and fix the permissions; PUID and PGID do not work with these images, only the user directive counts.

Which plan for which library

Scroll the table

Library profileRecommended planMargin and limit
Music, audiobooks and a few films on direct play, Immich without machine learning, one or two usersVPS Start: 2 vCPU, 4 GB, 50 GB SSD, at 99 DH/monthTight 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 playVPS Power: 4 vCPU, 8 GB, 100 GB SSD, at 299 DH/monthReaches the 8 GB recommended for Immich alone; initial indexing slows the rest
Full four-service stack, two or three users, an occasional transcodeVPS Business: 8 vCPU, 16 GB, 200 GB SSD, at 499 DH/monthComfortable; disk becomes the ceiling as soon as video exceeds a few hundred GB
Several streams transcoded at the same timeVPS Business at minimum, plus upstream file conversionWithout 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.

A VPS that grows with your media library

With a ServOrbit.com VPS, you get root access, a dedicated IPv4, NVMe SSD and a choice of distribution. When the library grows, vertical scaling adds RAM and disk without reinstalling. The VPS administration option is available.

Need help?

Browse our help center and FAQ, or reach our team — callback, WhatsApp or email. Support in French, English and Arabic.

Message us on WhatsAppopens in a new tab