Files
felhom.eu/documentation/audits/sizing-2026-10-13/SIZING.md
T
admin b4dcf4b373
gates / gates (push) Successful in 6m35s
Power-off spike (Part A) on the N100: plugswitch, wall-power figures, the hub's view
plugswitch.py refuses 'off' unless 20 s of readings show the N100 is already off (standby + 1 W,
no ping); 7 tests, each check red-proofed. By hand: guest shutdown 12 s, host off, 0.0 W; the hub
stayed silent and 'ONLINE' for 18 min; plug off/on did not start the N100, its power button did
(dashboard in 42 s). Wall figures into SIZING.md 4 and sizing.json power (8.5 / 20.7 / 0.0 W).
2026-10-13 19:15:08 +02:00

186 lines
13 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,356 | 3,061 | 2,048 / 4,480 | heavy-in-bursts | 0.74 | 74.2 | onboarding ! |
| Grimmory (`grimmory`) | 346 | 1,095 | 600 / 1,408 | medium | 0.83 | 22.0 | onboarding ! |
| Karakeep (`karakeep`) | 361 | 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 |
| SparkyFitness (`sparkyfitness`) | 305 | 664 | 400 / 1,792 | light | 0.23 | 0.4 | onboarding ! |
| Paperless-ngx (`paperless-ngx`) | 568 | 659 | 500 / 1,664 | medium | 0.37 | 31.3 | telemetry |
| n8n (`n8n`) | 321 | 550 | 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 |
| Grafana (`grafana`) | 220 | 435 | 100 / 512 | light | 0.31 | 1.0 | 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 |
| Jellyfin (`jellyfin`) | 191 | 373 | 512 / 2,048 | heavy-in-bursts | 0.06 | 51.3 | measured |
| 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 ! |
| Home Assistant (`home-assistant`) | 318 | 328 | 256 / 1,024 | light | 0.2 | 2.4 | 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 |
| Claper (`claper`) | 248 | 248 | 100 / 640 | light | 0.3 | 0.5 | onboarding |
| Kimai (`kimai`) | 163 | 240 | 100 / 640 | light | 0.16 | 0.8 | telemetry ! |
| Plex (`plex`) | 232 | 234 | 512 / 2,048 | light | 0.22 | 3.4 | measured |
| Uptime Kuma (`uptime-kuma`) | 103 | 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`) | 115 | 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`) | 74 | 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`) | 30 | 114 | 60 / 384 | light | 0.04 | 0.3 | onboarding ! |
| LubeLogger (`lubelogger`) | 53 | 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`) | 78 | 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 |
| Code-Server (`code-server`) | 59 | 59 | 200 / 1,024 | light | 0.03 | 0.0 | measured |
| PrivateBin (`privatebin`) | 40 | 59 | 30 / 128 | light | 0.04 | 0.0 | telemetry ! |
| 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`) | 5 | 10 | 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,061 · Grimmory 1,095 · Karakeep 1,050 · Cal.com 950 · Dawarich 890 MB.
**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) | — |
Rough yearly figure at rest: 8.5 W × 8,760 h ≈ 75 kWh.
## 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) | 2,791 MB |
| Household set, all at peak at once (sum — they rarely coincide; deliberately pessimistic) | 5,045 MB |
| Guest platform | ≈ 470 MB |
| Guest needs at peak | ≈ 5.5 GB |
| + 50 % headroom | ≈ 8.3 GB guest |
| + the host's share (the installer keeps 4 GB for Proxmox; it used ≈ 2.0 GB here) | **≈ 12.3 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 | 5.4 GB | does not fit | fits well | fits well | fits well |
| Household set without Immich | 2.4 GB | fits well | fits well | fits well | fits well |
| Immich alone | 3.4 GB | fits, but tight | fits well | fits well | fits well |
| All 62 apps | 21.3 GB | does not fit | does not fit | does not fit | fits, but tight |
<!-- /sizing-verdicts -->
- **8 GB:** the household set without Immich fits well; Immich alone fits, but tight; the whole household set does not fit.
- **16 GB:** the whole household set fits well.
- **32 GB:** the household set fits well; all 62 apps at once fit, but tight.