Files
felhom.eu/documentation/audits/sizing-2026-10-13/SIZING.md
T
admin 37cb83c166
gates / gates (push) Successful in 7m17s
Estimator: Immich peak 3,290 -> 3,983 MB from the 4,000-photo import (R-1147, operator yes 2026-10-18); Immich alone on 8 GB now 'does not fit'
Rebuilt sizing-data.json; data cache-bust v5; SIZING.md table and the pinned test cases moved
with it. Before/after: audits/day-2026-10-18/P6-before-after.md.
2026-10-18 14:36:14 +02:00

200 lines
15 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# What a Felhom box needs — sizing, 2026-10-13
Measured on the N100 demo box (`demo-felhom`: Intel N100, 4 cores, 16 GB). Method, sources and limits: `METHOD.md`.
Data file for the future estimator: `sizing.json` (rebuilt by `tools/build_sizing.py`). Memory is MB (MiB),
docker-style `used`. CPU is % of the whole N100 (4 cores = 100 %).
## 1. The floor — the box with no apps
| Part | Memory | Disk |
|---|---|---|
| Whole host (Proxmox + agent + guest + platform), `MemTotal − MemAvailable` | **2,494 MB** (peak 2,793) | host root 31 GB used of 94 GB, of which 19 GB = 3 local guest dumps |
| — of which inside the guest: platform containers + Docker daemons | ≈ 470 MB | guest system 1.1 GB, Docker volume 1.6 GB (platform images 1.55 GB) |
| traefik (proxy) | 113 MB (anon 15) | — |
| filebrowser (file browser) | 75 MB (anon 18) | — |
| cloudflared (tunnel) | 38 MB (anon 16) | — |
| felhom-controller | 29 MB (anon 28) | — |
| dockerd + containerd + shims | ≈ 220 MB RSS | — |
| CPU at rest, platform only | 0.4 % of the N100 | — |
| Power at rest (CPU package, approximate) | 3.9 W | — |
Samba is not in the floor: it runs only when sharing is switched on. Evidence: `evidence/floor/`.
## 2. Each app
<!-- apps-table: generated by tools/render_table.py -->
| App | Typical MB | Peak MB | Catalog request / limit | CPU class | CPU rest % | Busiest minute % | Source |
|---|---|---|---|---|---|---|---|
| Immich (`immich`) | 1,557 | 3,983 (raised 2026-10-18) | 2,048 / 4,480 | heavy-in-bursts | 0.74 | 74.2 | onboarding ! |
| Grimmory (`grimmory`) | 451 | 1,095 | 600 / 1,408 | medium | 0.83 | 22.0 | onboarding ! |
| Karakeep (`karakeep`) | 372 | 1,050 | 700 / 2,816 | light | 0.6 | 2.9 | onboarding ! |
| Cal.com (`calcom`) | 800 | 950 | 200 / 1,792 | light | 0.14 | 1.8 | onboarding |
| Dawarich (`dawarich`) | 390 | 890 | 500 / 2,688 | light | 0.48 | 14.4 | onboarding |
| Tandoor Recipes (`tandoor`) | 868 | 868 | 512 / 1,280 | medium | 0.16 | 20.0 | onboarding |
| RomM (`romm`) | 528 | 769 | 300 / 1,280 | medium | 0.71 | 24.2 | telemetry |
| Paperless-ngx (`paperless-ngx`) | 645 | 755 | 500 / 1,664 | medium | 0.37 | 31.3 | telemetry |
| SparkyFitness (`sparkyfitness`) | 316 | 664 | 400 / 1,792 | light | 0.23 | 0.4 | onboarding ! |
| n8n (`n8n`) | 351 | 578 | 512 / 1,536 | light | 0.1 | 0.5 | onboarding ! |
| Nextcloud (`nextcloud`) | 314 | 525 | 256 / 1,664 | light | 0.4 | 12.8 | onboarding ! |
| Komga (`komga`) | 442 | 496 | 200 / 768 | light | 0.15 | 0.5 | onboarding |
| Docmost (`docmost`) | 415 | 495 | 200 / 896 | light | 0.38 | 0.8 | telemetry |
| OnlyOffice (`onlyoffice`) | 364 | 488 | 512 / 2,048 | medium | 0.04 | 31.8 | measured |
| Rallly (`rallly`) | 360 | 479 | 384 / 1,024 | light | 0.19 | 2.9 | onboarding |
| AdventureLog (`adventurelog`) | 361 | 464 | 100 / 896 | medium | 0.45 | 19.3 | telemetry |
| Jellyfin (`jellyfin`) | 191 | 448 | 512 / 2,048 | heavy-in-bursts | 0.06 | 51.3 | measured |
| Grafana (`grafana`) | 269 | 435 | 100 / 512 | light | 0.31 | 1.0 | onboarding ! |
| Home Assistant (`home-assistant`) | 352 | 434 | 256 / 1,024 | light | 0.2 | 2.4 | onboarding |
| Mealie (`mealie`) | 273 | 431 | 200 / 1,000 | light | 0.15 | 1.4 | onboarding ! |
| Calibre-Web Automated (`calibre-web`) | 191 | 413 | 200 / 768 | medium | 0.04 | 26.0 | telemetry ! |
| Papra (`papra`) | 266 | 405 | 256 / 768 | light | 0.07 | 0.2 | onboarding |
| Outline (`outline`) | 282 | 393 | 200 / 1,152 | light | 0.39 | 1.4 | onboarding |
| Gramps Web (`gramps-web`) | 326 | 342 | 384 / 1,024 | light | 0.04 | 0.0 | onboarding |
| MeTube (`metube`) | 62 | 329 | 128 / 768 | light | 0.03 | 0.1 | onboarding ! |
| qBittorrent (`qbittorrent`) | 163 | 318 | 192 / 1,024 | light | 0.04 | 0.1 | onboarding ! |
| wger (`wger`) | 246 | 267 | 256 / 416 | — | – | – | onboarding — hidden |
| Plant-it (`plant-it`) | 50 | 256 | 50 / 256 | — | – | – | unmeasured — abandoned |
| Kimai (`kimai`) | 163 | 250 | 100 / 640 | light | 0.16 | 0.8 | telemetry ! |
| Claper (`claper`) | 248 | 248 | 100 / 640 | light | 0.3 | 0.5 | onboarding |
| Plex (`plex`) | 232 | 234 | 512 / 2,048 | light | 0.22 | 3.4 | measured |
| Uptime Kuma (`uptime-kuma`) | 118 | 227 | 50 / 256 | light | 0.2 | 0.2 | onboarding ! |
| BookStack (`bookstack`) | 159 | 225 | 150 / 768 | light | 0.18 | 0.9 | telemetry ! |
| Zipline (`zipline`) | 196 | 217 | 100 / 768 | light | 0.2 | 0.3 | onboarding |
| Wishlist (`wishlist`) | 118 | 184 | 120 / 512 | light | 0.07 | 0.1 | onboarding ! |
| Jellyseerr (`seerr`) | 166 | 168 | 100 / 384 | light | 0.04 | 0.1 | measured |
| ActualBudget (`actualbudget`) | 44 | 167 | 50 / 256 | light | 0.06 | 0.1 | measured ! |
| Ghost (`ghost`) | 149 | 149 | 150 / 512 | light | 0.08 | 0.2 | onboarding |
| Wanderer (`wanderer`) | 143 | 147 | 350 / 1,024 | light | 0.2 | 0.3 | measured |
| Emby (`emby`) | 142 | 146 | 512 / 2,048 | light | 0.18 | 1.9 | onboarding ! |
| Gitea (`gitea`) | 76 | 136 | 100 / 512 | light | 0.04 | 0.1 | onboarding ! |
| Homebox (`homebox`) | 17 | 118 | 50 / 256 | light | 0.06 | 0.1 | measured ! |
| Termix (`termix`) | 102 | 117 | 30 / 128 | light | 0.04 | 0.1 | measured |
| Grocy (`grocy`) | 33 | 114 | 60 / 384 | light | 0.04 | 0.3 | onboarding ! |
| LubeLogger (`lubelogger`) | 56 | 113 | 80 / 512 | light | 0.08 | 0.3 | onboarding ! |
| Vikunja (`vikunja`) | 20 | 104 | 50 / 256 | light | 0.0 | 0.0 | measured ! |
| Radarr (`radarr`) | 96 | 100 | 150 / 512 | light | 0.09 | 0.3 | measured |
| Vaultwarden (`vaultwarden`) | 44 | 99 | 50 / 256 | light | 0.03 | 0.0 | onboarding ! |
| Crafty Controller (`crafty-controller`) | 79 | 84 | 256 / 2,048 | light | 0.09 | 0.2 | onboarding ! |
| Homepage (`homepage`) | 82 | 83 | 50 / 256 | light | 0.03 | 0.0 | measured |
| Gokapi (`gokapi`) | 7 | 77 | 30 / 128 | light | 0.03 | 0.0 | measured ! |
| Sonarr (`sonarr`) | 77 | 77 | 150 / 512 | light | 0.07 | 0.2 | onboarding |
| Recipe Importer (`recipe-importer`) | 63 | 72 | 30 / 128 | light | 0.09 | 0.1 | measured |
| OpenGist (`opengist`) | 27 | 68 | 30 / 128 | light | 0.03 | 0.8 | telemetry |
| SABnzbd (`sabnzbd`) | 68 | 68 | 192 / 1,024 | light | 0.04 | 0.1 | onboarding |
| PrivateBin (`privatebin`) | 40 | 60 | 30 / 128 | light | 0.04 | 0.0 | telemetry ! |
| Code-Server (`code-server`) | 59 | 59 | 200 / 1,024 | light | 0.03 | 0.0 | measured |
| Audiobookshelf (`audiobookshelf`) | 48 | 54 | 100 / 512 | light | 0.04 | 0.1 | onboarding |
| Radicale (`radicale`) | 27 | 27 | 40 / 128 | light | 0.03 | 0.0 | onboarding |
| Transmission (`transmission`) | 6 | 22 | 64 / 512 | light | 0.05 | 0.1 | onboarding ! |
| Navidrome (`navidrome`) | 14 | 18 | 50 / 256 | light | 0.04 | 0.1 | onboarding |
| BentoPDF (`bentopdf`) | 10 | 15 | 100 / 384 | light | 0.03 | 0.0 | telemetry ! |
| Glance (`glance`) | 6 | 7 | 20 / 128 | light | 0.03 | 0.0 | measured |
| NZBGet (`nzbget`) | 5 | 5 | 64 / 512 | light | 0.04 | 0.1 | onboarding |
<!-- /apps-table -->
`source`: telemetry = hub fleet data ≥ 7 days · onboarding = an earlier session's measured peak · measured = this bench ·
unmeasured = catalog value. `!` = two sources' peaks differ by more than 50 %; the higher is used (all values:
`evidence/apps-table.json`). `—` = not installable today (hidden / abandoned), no bench.
**The five largest peaks:** Immich 3,983 (3,290 until 2026-10-18) · Grimmory 1,095 · Karakeep 1,050 · Cal.com 950 · Dawarich 890 MB.
**Raised with the HP check (2026-10-14, operator ruling A, R-1009):** 23 figures of 64 apps took the second machine's higher
figure (HP t740, `audits/sizing-check-2026-10-14/hp-figures.json`); none went down. Each raised figure is named in
`sizing.json` → `raised_by_hp` (the figure, what it was, what it is). The table above already holds the raised figures.
**CPU bursts (heavy-in-bursts):** Immich — 74 % for the busiest minute, while it reads new photos; Jellyfin — 51 %,
while it converts a film (CPU only, no GPU passed through). **Medium:** Paperless-ngx 31 % (reading scans),
OnlyOffice 32 %, Calibre-Web 26 %, RomM 24 %, Grimmory 22 %, Tandoor 20 %, AdventureLog 19 % (first start).
Every other app stays under 15 % even at first start.
## 3. Catalog accuracy (§7 C — templates NOT changed; `evidence/catalog-check.json`)
**Near a memory limit (peak > 80 %) — risk: the container is stopped for using too much memory:**
| App | Finding | Source |
|---|---|---|
| docmost | container `docmost` 475 MB = 93 % of its 512 MB limit | telemetry + bench |
| romm | container `romm` 703 MB = 92 % of its 768 MB limit | telemetry + bench |
| termix | 117 MB = 91 % of its 128 MB limit | bench |
| uptime-kuma | 227 MB = 89 % of `mem_limit` 256 MB | telemetry (tester-1) |
| grafana | 435 MB = 85 % of its 512 MB limit | bench |
| immich | container `immich-server` 1,665 MB = 81 % of its 2,048 MB limit (kernel peak incl. cache 4,365 of 4,480 MB for the app) | bench, 200-photo import |
| tandoor | container `tandoor` 827 MB = 81 % of its 1,024 MB limit | bench |
No container was killed for memory during the bench (`oom_kill` = 0 in every container).
**Typical use above `mem_request` (the catalog's figure is too low), 23 apps:** calcom ×4.0 · adventurelog ×3.6 ·
termix ×3.4 · claper ×2.5 · grafana ×2.2 · komga ×2.2 · docmost ×2.1 · recipe-importer ×2.1 · uptime-kuma ×2.1 ·
zipline ×2.0 · romm ×1.8 · seerr ×1.7 · tandoor ×1.7 · homepage ×1.6 · kimai ×1.6 · mealie ×1.4 · outline ×1.4 ·
privatebin ×1.3 · home-assistant ×1.2 · nextcloud ×1.2 · bookstack ×1.1 · paperless-ngx ×1.1 · papra ×1.0.
## 4. Power
Wall figures: the whole N100 box at the socket (Tuya meter plug, tinytuya, 2 s; `audits/poweroff-2026-10-13/`,
measured the same evening). CPU figures: Intel RAPL, CPU package only (this run).
| State | Wall (plug) | CPU package (RAPL) |
|---|---|---|
| Platform at rest | **8.5 W** average, 7.2 W median (10 min) | 3.9 W (15 min) |
| Immich import, 200 photos + all jobs | **20.7 W** average, 24.0 W busiest minute, 25.2 W max (442 s) | 12.5 W average, 14.7 W busiest minute; 70 °C max (411 s) |
| Switched off (host powered off, plug on) | **0.0 W** — below the plug's 0.1 W step (18 min) | — |
| **One real night, the N100** (2026-10-13 22:12 → 06:00, its normal night: dumps, Tier 2, Tier 3, whole-guest backup, OS steps) | **66 Wh in 7.8 h** — 8.5 W average, 7.3 W median, 20.6 W max (the backup window: 19.8 W max); ≈ 0.2 kWh a day | — |
| **HP t740 (demo-hp)**, the second machine, at rest (guest 9201 with 11 apps + Tester 1's VM + a scratch guest) | **24–27 W** (median 24.1 W) | — |
| HP, three heavy apps busy at once (Immich import, Jellyfin transcode, Nextcloud upload) | 71 W average, 81.6 W max | — |
| HP, a start after a power cut (to all apps healthy, ~4 min) | 72–81 W peak, 2.7–3.4 Wh | — |
| HP, switched off with the plug on | 0–3.2 W | — |
Rough yearly figure at rest: 8.5 W × 8,760 h ≈ 75 kWh (the measured night agrees: 66 Wh in 7.8 h). The N100 night and the
HP rows: `audits/night-power-2026-10-13/` (2026-10-14). They are in this table only, not in `sizing.json`: changing that file
changes the checksum the website's data file carries, and the website was not to change that night.
## 5. Proposed minimum machine — a proposal; the operator decides
**Worked from:** floor + the household set (Immich, Jellyfin, Nextcloud, Home Assistant, Vaultwarden, Paperless-ngx).
| | Memory |
|---|---|
| Household set, typical (sum) | 3,103 MB |
| Household set, all at peak at once (sum — they rarely coincide; deliberately pessimistic) | 5,551 MB |
| Guest platform | ≈ 470 MB |
| Guest needs at peak | ≈ 5.9 GB |
| + 50 % headroom | ≈ 8.8 GB guest |
| + the host's share (the installer keeps 4 GB for Proxmox; it used ≈ 2.0 GB here) | **≈ 12.8 GB box** |
**Headroom 50 %, because:** the bench library is small (200 photos; tester-1's real Immich already peaked at 2.9 GB);
the nightly backup runs database dumps and restic next to the apps; an update pulls new images while the old ones run;
and the page cache is what makes a photo library feel fast.
| | Minimum | Recommended |
|---|---|---|
| CPU | x86-64, 4 cores, N100 class or newer (Intel or AMD) | N100 / N150 / N305 or better; Intel Quick Sync helps Jellyfin later (not used today) |
| Memory | **8 GB** | **16 GB** |
| System disk | 256 GB SSD | 512 GB SSD (NVMe or SATA) |
| Data | a separate drive for photos and films | same |
| Out | ARM (Raspberry Pi), Synology / QNAP and other NAS appliances, routers, cloud VPS, 32-bit machines | — |
**Disk, why:** the installer gives the guest a 32 GB system volume and, from the free space, a 70 / 130 / 250 GB data
volume; the host keeps three daily guest dumps (19 GB on this box). Image sizes per app were **not measured** in this run.
**What each size runs — three verdicts, the same rule as the estimator page** (`/gepigeny`, `website/assets/sizing.js`).
*Corrected 2026-10-13:* the lines here first said an 8 GB box runs Immich „on its own” while this section sizes a box at
peak × 1.5 — which Immich alone does not meet on 8 GB ((470 + 3,061) × 1.5 ≈ 5.2 GB > 4 GB), though its peak fits
(3.4 GB). Three verdicts say both honestly. The guest is the installer's rule on MemTotal = label − 612 MiB (the
16 GB N100 reports 15,772 MiB): **8 GB → 4.0 GB, 16 GB → 11.4 GB, 32 GB → 27.4 GB** (the first version said 11.6 / 27.6).
**Fits well:** peak × 1.5 ≤ guest · **fits, but tight:** peak ≤ guest < peak × 1.5 — works, but a large photo import or
a backup at the same time can slow it down · **does not fit:** guest < peak. Peak = 470 MB platform + the apps' peaks summed.
<!-- sizing-verdicts: generated from the rule; scripts/test_build_sizing_web.py checks this table (24 GB column added 2026-10-13, estimator v2) -->
| Selection | Peak | 8 GB | 16 GB | 24 GB | 32 GB |
|---|---|---|---|---|---|
| Household set | 6.6 GB | does not fit | fits well | fits well | fits well |
| Household set without Immich | 2.7 GB | fits, but tight | fits well | fits well | fits well |
| Immich alone | 4.3 GB | does not fit | fits well | fits well | fits well |
| All 63 apps | 22.5 GB | does not fit | does not fit | does not fit | fits, but tight |
<!-- /sizing-verdicts -->
- **8 GB:** the household set without Immich fits, but tight; Immich alone does not fit (since 2026-10-18); the whole household set does not fit.
- **16 GB:** the whole household set fits well.
- **32 GB:** the household set fits well; all 63 apps at once fit, but tight.
2026-10-14: raised with the HP check (operator ruling A); one verdict became stricter — household set without Immich on 8 GB, by 0.5 MB.
2026-10-18 (R-1147, operator yes): **Immich's peak raised 3,290 → 3,983 MB** from a 4,000-photo import on scratch guest 9202 (`audits/night-2026-10-18/P6/`: the four containers' `memory.current` summed — it includes file cache, so it sits above `used`; the heap alone peaked at 2,777 MB, and the server was OOM-killed twice at its old 2 GB limit). Typical unchanged (1,557; rest after the import 1,230 heap). One verdict became stricter: **Immich alone on 8 GB — does not fit** (4.3 GB > the 4.0 GB guest).