What actually runs on a 2 GB VPS has a dull answer if you read system requirements and a useful one if you measure. We took one Flare VPS (1 vCPU, 2 GB RAM, 25 GB disk), installed Docker, and ran the app stacks from our own guides: each alone, then side by side, then with a swap file.
Every number here is idle. The apps were started, checked on their health endpoints and left alone. No logins, no imported data, no load tests. Real use costs more, so read these as a floor.
Using an AI coding agent? There's a ready-made prompt at the end of this guide. Copy that instead of this article.
The short answer
- Eight of nine stacks fit together on 2 GB without swap, adding about 1.1 GB to the base system and leaving about 425 MB available.
- The ninth, Plausible, only fits with swap. With a 2 GB swap file it ran, and about 950 MB of idle memory moved to disk.
- Immich does not belong on this plan. Alone, without swap, it drove available memory to 38 MB within about 35 seconds, and we stopped it first.
- Disk is the second limit. The nine sets of images total 12.7 GiB of a 25 GB disk, before your data.
What we measured, and what we left out
Nine stacks from the compose files in our published guides: Uptime Kuma, Vaultwarden, Navidrome, Audiobookshelf, Jellyfin, Forgejo, n8n (with Postgres), Home Assistant (with Mosquitto) and Plausible Community Edition (with Postgres and ClickHouse). Immich was tested separately with its official compose file, since it is a different weight class. There is no "Immich lite", so we did not invent one.
Three things were left out:
- Caddy. Every guide uses it for automatic HTTPS, and it cannot get a certificate without a real domain. Nothing else needed a domain or an account to start, so we ran the apps without it and measured Caddy once, alone, on a plain port-80 page: about 6 MiB (range -12 to +19, inside the noise) and 0.8 s to start. Add that to any figure below.
- Accounts and logins. Home Assistant answered with its onboarding redirect, Forgejo with its install page, Immich with its ping endpoint.
- Real data. The Jellyfin, Navidrome and Audiobookshelf libraries were empty. Scans, thumbnails and transcodes will use more.
Results: each stack alone
Each stack started from a clean state three times, with containers and volumes removed between runs. "First start" runs from docker compose up -d to the first successful HTTP answer, images already pulled. "Idle RAM" is the rise in the host's used figure in free -m after two minutes idle: median of three, range in brackets.
| Stack | Containers | Image size | Pull | First start | Idle RAM | Data on disk after start |
|---|---|---|---|---|---|---|
| Vaultwarden | 1 | 383 MiB | 12.5 s | 1.3 s | under 30 MiB | 1 MiB |
| Navidrome | 1 | 332 MiB | 12.7 s | 0.8 s | under 30 MiB | 1 MiB |
| Audiobookshelf | 1 | 499 MiB | 16.2 s | 2.5 s (2.4-4.7) | 43 MiB (28-54) | 0-18 MiB |
| Forgejo | 1 | 266 MiB | 10.3 s | 1.8 s (1.8-2.5) | 112 MiB (102-120) | 117 MiB |
| Uptime Kuma | 1 | 2,372 MiB | 74.1 s | 4.0 s (3.7-4.5) | 98 MiB (95-106) | 0 MiB |
| Jellyfin | 1 | 2,385 MiB | 82.8 s | 3.4 s (2.0-3.5) | 139 MiB (137-161) | 1 MiB |
| Home Assistant + Mosquitto | 2 | 3,348 MiB | 91.6 s | 8.9 s (8.5-8.9) | 346 MiB (342-359) | 45 MiB |
| n8n + Postgres | 2 | 2,133 MiB | 74.0 s | 21.0 s (20.5-21.4) | 389 MiB (365-406) | 117 MiB |
| Plausible + Postgres + ClickHouse | 3 | 1,337 MiB | 38.2 s | 29.8 s (29.8-29.9) | 610 MiB (603-613) | 53 MiB |
| Caddy (reference) | 1 | 89 MiB | 4.1 s | 0.8 s | 6 MiB (-12 to +19) | 0 MiB |
- Memory and image size do not track each other. Uptime Kuma and Jellyfin pull 2.3 GiB images and idle at 98 and 139 MiB. Plausible has the smallest images of the heavy group and idles highest.
- Start time is mostly the database. Single-container apps answer in one to four seconds. n8n and Plausible wait for their databases and then migrate: 21 and 30 seconds.
- No container restarted, and swap stayed at zero in all 33 runs, control and Caddy included.
The control. To learn what the method can resolve, we ran it on a plain nginx:alpine container three times. It came out at -19 MiB (range -41 to +2). A container cannot use negative memory, so that spread is noise: about plus or minus 40 MiB. That is why Vaultwarden and Navidrome say "under 30 MiB" (their cgroup counters later read 6 and 9 MiB). docker stats also disagreed with itself between runs, with ClickHouse at 163 MiB in one and 495 in another, because it counts file cache. We trust the host figure and say where it is soft.
Results: how many fit together
We added the stacks one at a time, smallest first, let each settle for a minute and recorded memory. The rules: stop at about 90 percent used, and skip anything projected past 95 percent.
| Added | Started in | Used after | Available after |
|---|---|---|---|
| (Docker only) | 441 MiB | 1,526 MiB | |
| Vaultwarden | 1.2 s | 452 MiB | 1,515 MiB |
| Navidrome | 1.2 s | 474 MiB | 1,493 MiB |
| Audiobookshelf | 2.7 s | 546 MiB | 1,421 MiB |
| Forgejo | 1.7 s | 646 MiB | 1,321 MiB |
| Uptime Kuma | 4.2 s | 732 MiB | 1,235 MiB |
| Jellyfin | 1.9 s | 853 MiB | 1,114 MiB |
| Home Assistant + Mosquitto | 8.8 s | 1,160 MiB | 807 MiB |
| n8n + Postgres | 21.5 s | 1,575 MiB | 392 MiB |
| Two minutes later | 1,542 MiB | 425 MiB | |
| Plausible | skipped, projected 110% |
Eight stacks, ten containers, all answering. The rise (1,542 minus 441 MiB) is 1,101 MiB, and the eight stacks measured alone sum to 1,113 MiB, so you can budget a box by adding the single numbers. Pressure was close to nil: the kernel's stall counter read 0.07 percent over a minute, with no swap writes and no kills.
Next in line was Plausible at about 610 MiB against 425 MiB available. We did not start it without swap. The projection said something would have to be killed, and we had no reason to find that out the hard way.
The effect of a 2 GB swap file
With the eight stacks still running we created a 2 GB swap file (fallocate, mkswap, swapon, default swappiness of 60) and started Plausible.
| Moment | Used | Available | Swap used |
|---|---|---|---|
| Eight stacks, swap on, nothing changed | 1,543 MiB | 424 MiB | 0 |
| Plausible started (took 31.8 s; 29.8 s alone) | 1,396 MiB | 571 MiB | 817 MiB |
| Four minutes later | 1,338 MiB | 629 MiB | 892 MiB |
| Eight minutes later | 1,299 MiB | 668 MiB | 955 MiB |
Nine stacks, thirteen containers, all answering. "Available" went up after adding a stack, which looks wrong until you see why: the kernel moved memory idle processes had not touched out to the swap file and kept active file cache in RAM. n8n's private memory fell from 329 to 72 MiB, Jellyfin's from 74 to 16, Uptime Kuma's from 96 to 9.
The cost shows up as pressure. While Plausible started, the stall counter peaked at 1.5 percent over a minute, then fell to 0.05 by minute four. In eight minutes about 1.4 GB was written to swap and 0.5 GB read back. With 953 MiB swapped out we requested each health endpoint three times: first answers took 0.7 to 117 ms, later ones under 10 ms. That is a ping, not a page render, so it shows only that nothing hung.
What swap is. A safety net that lets cold memory leave RAM so the active set fits. It is not more RAM. A workload that actively uses 2.5 GB will thrash, and on one vCPU everything slows together.
One mistake worth sharing. Afterwards we ran swapoff while all nine stacks were still up. It has to pull everything back into RAM, and RAM was full, so the out-of-memory killer ended the swapoff process and nothing else. Stopping the stacks recovered the box in seconds. Stop things before you remove swap from a loaded server.
Immich: the one that doesn't fit
Immich's documentation lists 6 GB of RAM as the minimum and 8 GB as recommended (docs.immich.app, requirements page). We tested it instead of taking that on trust.
Alone, without swap, the official compose stack answered its ping at 24 s. Seconds later its server container reached about 1 GiB at 95 percent CPU, host used hit 1,381 MiB and available memory fell to 38 MB. A watchdog stopped the stack before the kernel had to. Nothing was killed.
With the swap file the same start survived. It peaked at 1,759 MiB used with 208 MiB available and 548 MiB swapped (stall counter 9.1 percent over ten seconds), then settled at 1.3 to 1.5 GiB used with about 515 MiB swapped. That was an empty Immich: no photos, no recognition running. Our Immich guide recommends a larger plan for this reason.
What not to run on 2 GB
This is judgement from the numbers above, not extra measurement.
- Immich, and anything else with a machine-learning container. Alone, it took the whole box at idle.
- Plausible beside other apps. Alone it idles near 610 MiB, and the project recommends at least 2 GB of RAM for it. ClickHouse grows with traffic. Our Plausible guide recommends Orbit.
- More than two of Home Assistant, n8n and Plausible (346, 389 and 610 MiB), and only with swap on.
- Jellyfin as a transcoder. We did not measure transcoding, and one vCPU is thin. Direct play suits it better (Jellyfin guide).
- Compiling on the box. Not measured, but a build peaking at 1 GB leaves less than 1 GB for everything else.
What fits comfortably: Uptime Kuma, Vaultwarden, Navidrome, Audiobookshelf and Forgejo together use under 400 MiB at idle.
Disk is the other limit
The nine stacks' images total 12.7 GiB on a 25 GB disk where Ubuntu and Docker already hold about 2.3 GiB. That leaves about 8.5 GiB for data, backups and logs. The big ones are Home Assistant (3.3 GiB), Jellyfin and Uptime Kuma (2.3 GiB each) and n8n with Postgres (2.1 GiB). Prune old images after updates.
How we measured
A Flare VPS on Ubuntu 24.04.3, kernel 6.8.0-88-generic, Docker Engine 29.9.2 and Compose v5.6.1, on 2026-10-22. To repeat it:
- Install Docker with the apt-repository method from Docker's documentation.
- Use the guides' compose files minus Caddy, one folder (one Compose project) per stack. Vaultwarden gets
DOMAIN=http://localhostand no admin token. Plausible getsBASE_URL=http://localhostand a generated secret. - Per stack, from clean: remove containers and volumes, drop the page cache (
echo 3 > /proc/sys/vm/drop_caches), recordfree -m, rundocker compose up -d, and poll the container's health URL every half second until it answers 2xx or 3xx. That is "first start". - Wait 120 s, drop the cache, record
free -manddocker stats --no-streamthree times. Memory is the rise inused. - Repeat three times and report median and spread.
- Run a control (
nginx:alpine). Its result is your noise floor. - Combined run: add stacks smallest first, a minute of settling each, logging
free -m,/proc/pressure/memoryand the swap counters in/proc/vmstat. Run a watchdog that stops the last stack if available memory drops under 60 MB, and lowersshd's OOM score so your session survives.
Not measured: any load, real data, a second server, memory creep over days, and CPU beyond seeing it near zero at idle.
Do it with an AI agent
Don't paste the article at Claude Code, Cursor or another agent: it will skim the cautions and run the risky step first. Copy this instead to measure your own box, and run it from your own machine with your agent able to SSH out.
Prompt for an AI agent
Measure the idle memory, disk and start time of docker compose stacks on a fresh
Ubuntu 24.04 VPS, and report how many fit together. Idle measurements only.
FILL IN BEFORE YOU START:
- SERVER_IP = <VPS IP from the PrivateByte dashboard>
- STACKS = <list of compose folders I will provide, one folder each>
- I will provide the compose files. Do not invent or download others.
WHAT TO DO:
1. SSH to root@SERVER_IP. Report "free -m", "df -h /" and "swapon --show".
2. Install Docker Engine using Docker's apt repository method from
https://docs.docker.com/engine/install/ubuntu/ (not the convenience script).
Report "docker --version" and "docker compose version".
3. Lower the SSH daemon's OOM score so the session survives pressure:
for each sshd pid, write -900 to /proc/PID/oom_score_adj.
4. CONTROL FIRST: run an nginx:alpine container three times with the
procedure in step 5. Report the spread. That is the noise floor.
5. For each stack, alone, three times: remove containers and volumes; drop the
page cache; record "free -m"; time "docker compose up -d" until its health
URL answers 2xx or 3xx (poll every 0.5 s, use the container's Docker network
address); wait 120 s; drop the cache; record "free -m" and
"docker stats --no-stream" three times, 10 s apart. Idle RAM is the rise in
"used". Report median and range. Also record image size and disk used.
6. COMBINED: add the stacks smallest first, one at a time, wait 60 s after
each, and record "free -m" and /proc/pressure/memory. BEFORE each add,
estimate the new memory from the single-stack figures. If the projected
total is over 95 percent of RAM, STOP and tell me. Never go past 90 percent
used without asking.
7. Only if I say yes: create a 2 GB swap file (fallocate, chmod 600, mkswap,
swapon), then continue adding stacks, recording swap used and pressure.
RULES:
- Idle measurements only. No load tests, no benchmarks, no traffic generation.
- Never create accounts, log in, or enter passwords in any app. Check health
URLs and HTTP codes only.
- Never print or log secret values. If a stack needs a generated secret, create
it with openssl and check that it is non-empty.
- Do not use swapoff while stacks are running. Stop the stacks first.
- Do not trigger an out-of-memory kill on purpose. Add a watchdog that stops
the most recently added stack if "MemAvailable" falls under 60 MB.
- Nothing destructive outside the stack folders. If a folder already holds data
I did not ask you to remove, STOP and ask.
- Do not edit the compose files except to remove a Caddy service that needs a
domain, and tell me which ones you changed.
VERIFY, AND SHOW ME THE OUTPUT OF EACH:
- "docker ps" -> expected containers running, none restarting
- the health URL answer and HTTP code for each stack
- "free -m" before and after each step, and "cat /proc/pressure/memory"
- "dmesg | grep -ci 'out of memory'" before and after -> must not rise
- the control run's spread, stated next to the results it applies to
Do not tell me a step succeeded without showing the command output that proves
it. If a verification fails, stop and report the actual error. Do not retry
silently and do not improvise a workaround.Two rules carry the weight. The 90 percent limit and the watchdog exist because the failure on a small server is a killed app, not a slow one, and the measurement should stop first. The control comes before the results because without it a small number is an unlabelled guess.
Deploy your VPS
A password manager, a status page, a music server and a Git host sit comfortably on 2 GB. Add a heavier stack and you want the next size up, with a swap file as insurance.
The Flare plan is where these numbers were measured.
- Unmetered bandwidth, no overage
- Free DDoS protection
- Daily automated backups
- Browser console access
Common questions
Can I run several Docker apps on a 2 GB VPS? Yes. Eight stacks (ten containers) ran together at idle with about 425 MB of RAM to spare: Vaultwarden, Navidrome, Audiobookshelf, Forgejo, Uptime Kuma, Jellyfin, Home Assistant and n8n.
Should I add a swap file on a 2 GB server? It is cheap insurance. With a 2 GB swap file the ninth stack, Plausible, ran, and about 950 MiB of idle memory moved to disk. It is not extra RAM, and active workloads over 2 GB will slow down.
How much RAM does Immich need? Its documentation says 6 GB minimum, 8 GB recommended. On our 2 GB box it reached 38 MB available in under a minute without swap.
Do these numbers include a reverse proxy? No. Caddy added about 6 MiB when measured alone, within the noise.
Are these numbers for a busy server? No. Everything is idle, with empty libraries and no users. Treat them as minimums.