JarvisOS HomeLab status
No signal

Storage tiers

4 tiers · 9 datasets · 2/3 flows running · register proposed

Which bytes belong on which tier, what each tier costs when it fills, and which copies of them survive the house. Occupancy, headroom and the date a tier runs out are derived from what is placed on it — never declared — and the one rule that fails the build rather than raising a finding is that live secrets do not go anywhere that replicates.

Tiers with headroom 3/4 6 months of warning, at the declared growth
Placed today 5.56 TB of 8.4 TB · 5.18 TB of it reproducible
Backups proven off-site 0/2 1/2 restored from anywhere
Recurring cost €0 per month, across every tier · 3/21 sizes measured

Open gaps

Measured against what is placed today, not what is designed

The three roles

A byte is in exactly one of them at a time

Hot is local SSD: worktrees, live databases, service state — fast, small, and the only storage that works when everything else is unreachable. Bulk is the NAS pool: cheap per gigabyte and in one room, which is the whole of its weakness. Off-site is the encrypted subset that survives the house, and the only tier with a quota and a bill.

Tier Role Accepts Used today Reaches warn Secrets
VIN local SSD Hot — local SSD worktree, state, artifact, cache, secret 39.1% 14.8 GB 17.1 months allowed
Home1 local SSD Hot — local SSD worktree, state, artifact, cache, secret, backup 27.4% 126 GB 2 years allowed
NAS storage pool Bulk — NAS pool artifact, cache, backup, media, state 68.6% 5.36 TB 20.6 months refused
GWSCLI / S3 off-site Off-site — object store backup, state 65% 65 GB 4.5 months refused

Hot — local SSD

VIN local SSD Hot — local SSD Headroom secrets ok

vin · No signal

The edge node's own disk: worker worktrees, service state, the container images it serves from, and the credentials it uses. Small, fast, and the only storage that keeps working when the house is dark.

Used today
14.8 GB of 38 GB (39.1%) estimated
Used by design
38.8 GB (102.2%) once every planned placement is running
Reaches 75%
17.1 months at 819 MB/month
Reclaimable
6 GB is reproducible and could be deleted
Measured
8.9 GB on 2026-08-17 · +5.9 GB against this register
Cost
€0/month · included

Where the numbers come from: df -P / run on the node itself on 2026-08-17: 38 GB total, 8.9 GB used, 27 GB available. The baseline is that reading minus the declared paths that are actually present — the OS, the package cache and everything nobody declared. This is the only tier in the register with a reading rather than a guess, and where the reading and the arithmetic disagree the register raises a finding rather than adjusting the baseline until they agree.

On this tier Class Path Size Growth Read?
Container images artifact /var/lib/docker/overlay2 6 GB 512 MB/mo estimated
JarvisOS service state state /var/lib/jarvisos 115 MB 205 MB/mo 2026-08-17
Active task worktrees worktree /var/lib/jarvisos/workspaces 28 MB 102 MB/mo 2026-08-17
Host credential material secret /etc/jarvisos 1 MB 0 MB/mo 2026-08-17

At 75%

  1. Prune the worker's finished worktrees older than the retention window.
  2. docker image prune for layers no running container references.

At 90%

  1. Stop accepting new dispatched work: a worker that cannot write its journal must refuse rather than half-run a task.
  2. Escalate. There is nothing else on this node that is safe for automation to delete.

Deleted first, in order: worktrees (frees 28 MB) → container-images (frees 6 GB)

Integrity: A hot tier holds no backup of anything. Its integrity is the checksums of what it serves — the container digests and the databases' own pages — and its failure mode is the node, which the data register already covers.

Home1 local SSD Hot — local SSD Headroom secrets ok

home1 · No signal

The primary node's own disk: the Postgres data directory, Coolify's stack and build cache, container images and service state. Everything the estate writes to first.

Used today
126 GB of 460 GB (27.4%) estimated
Used by design
126 GB (27.4%)
Reaches 75%
2 years at 9 GB/month
Reclaimable
102 GB is reproducible and could be deleted
Measured
never read from the tier itself
Cost
€0/month · owned-hardware

Where the numbers come from: Estimated from the node's build, not read. Nothing in this repository has an execution path to home1, so this number is a claim until `jarvisos-storage.mjs report` is run there.

On this tier Class Path Size Growth Read?
Build and package cache cache /var/lib/docker/buildkit 60 GB 5 GB/mo estimated
Container images artifact /var/lib/docker/overlay2 40 GB 2 GB/mo estimated
Postgres data directories state /var/lib/postgresql/16/main 12 GB 1.5 GB/mo estimated
Configuration and estate archive backup /var/lib/coolify/backups 3 GB 102 MB/mo estimated
Active task worktrees worktree /var/lib/jarvisos/workspaces 2 GB 205 MB/mo estimated
JarvisOS service state state /var/lib/jarvisos 1 GB 205 MB/mo estimated
Host credential material secret /etc/jarvisos 1 MB 0 MB/mo estimated

At 75%

  1. Prune the Coolify build cache and dangling image layers.
  2. Confirm the WAL archive is draining to the NAS: a stalled archive_command turns a full pg_wal into an outage of the primary.

At 90%

  1. A full disk under the Postgres primary is data loss, not a capacity problem. Stop deploys first, then prune caches, then escalate.
  2. Never delete out of pg_wal by hand. The archive is what removes those files, and removing them another way removes the recovery point with them.

Deleted first, in order: artifact-cache (frees 60 GB) → container-images (frees 40 GB) → worktrees (frees 2 GB)

Integrity: Postgres verifies its own pages and the copies that have to be verifiable live on the NAS. The exception is Coolify's pre-upgrade dump, which lands here on the machine it protects — the register accounts for it below rather than pretending it is somewhere else, and the fact that nothing verifies it is a finding this tier earns.

Bulk — NAS pool

NAS storage pool Bulk — NAS pool Headroom replicates

nas-backups · No signal

Bulk on-site storage: the on-site backups, the shared artifact and image cache, and the media library. Cheap per gigabyte, redundant against a dead drive, and in the same room as everything it protects.

Used today
5.36 TB of 7.81 TB (68.6%) estimated
Used by design
5.65 TB (72.4%) once every planned placement is running
Reaches 80%
20.6 months at 44.2 GB/month
Reclaimable
5.08 TB is reproducible and could be deleted
Measured
never read from the tier itself
Cost
€0/month · owned-hardware

Where the numbers come from: Estimated usable capacity after parity. Not read from the pool — `zpool list` runs on the NAS and nothing here has a path to it.

On this tier Class Path Size Growth Read?
Media library media /mnt/pool/media 5.08 TB 40 GB/mo estimated
Database backups and WAL archive backup /mnt/pool/backups 240 GB 4 GB/mo estimated
Configuration and estate archive backup /mnt/pool/backups/config 8 GB 205 MB/mo estimated

At 80%

  1. Prune the shared artifact cache to its retention window; it is rebuilt on the next build.
  2. Check the media dataset's growth rate before touching anything backup-shaped.

At 92%

  1. Never prune a backup dataset to make room. Delete reproducible bytes — cache, images, media — or add capacity.
  2. A pool that cannot accept a WAL segment stops protecting the primary, which is the failure this tier exists to prevent.

Deleted first, in order: artifact-cache → media (frees 5.08 TB)

Integrity: zpool scrub of the whole pool, every 30 days · last 2026-08-02 (pass)

Off-site — object store

GWSCLI / S3 off-site Off-site — object store Headroom replicates

gwscli-s3-offsite · No signal

The encrypted copy that survives losing the house. Deliberately the smallest tier: only what cannot be reconstructed goes here, because this is the one tier where a gigabyte has a price and a quota.

Used today
65 GB of 100 GB (65%) estimated
Used by design
77 GB (77%) once every planned placement is running
Reaches 75%
4.5 months at 2.2 GB/month under the 6-month policy
Reclaimable
0 MB is reproducible and could be deleted
Measured
never read from the tier itself
Cost
€0/month · free-tier · 100 GB quota

Where the numbers come from: The free quota, not a measurement. `restic stats` needs the repository password, which is deliberately on no host here, so the occupancy below is the sum of what is pushed rather than what the provider reports.

On this tier Class Path Size Growth Read?
Database backups and WAL archive backup restic:jarvisos/backups 62 GB 2 GB/mo estimated
Configuration and estate archive backup restic:jarvisos/config 3 GB 205 MB/mo estimated

At 75%

  1. Shorten the retention on the off-site copy before adding capacity: the on-site tier keeps the long tail, and off-site only has to cover losing the house.
  2. Confirm nothing reproducible has crept into a flow. Off-site is for what cannot be rebuilt.

At 90%

  1. A full off-site repository stops accepting the copy that survives the site while everything on-site still looks healthy. Treat it as a protection outage, not a storage one.
  2. Paying for capacity here is a legitimate answer; it is a decision with a monthly cost and belongs in the register, not in an incident.

No eviction order: nothing here is safe to delete without a decision.

Integrity: restic check --read-data-subset=5%, every 30 days · never run

How bytes move

2/3 running

A flow that walks paths can pick up a file nobody meant to send, so it carries exclusions. A flow whose output is produced — a dump, a base backup — writes exactly the files it means to, and exclusions would be theatre. Anything leaving the estate is encrypted before it goes, with the key somewhere that is not the node being backed up.

Flow Route Carries When Encryption Selection
Home1 to the NAS backup dataset t-local-home1t-nas-pool db-backups, config-archive WAL continuously, base backups every 6h, dumps nightly at 01:00 dataset-level plaintext hop produced, not swept
NAS to the off-site repository t-nas-poolt-offsite-gwscli db-backups, config-archive nightly at 02:30 client-side 12 exclusion(s)
VIN to the off-site repository planned t-local-vint-offsite-gwscli jarvisos-state, postgres-data every 15 minutes client-side 12 exclusion(s)

The datasets

What each kind of byte is, where it lives, how long it is kept and who has ever proved it comes back.

Active task worktrees worktree reproducible

owned by jarvisos-worker

Where dispatched work actually happens: one checkout per task, plus whatever the task builds while it runs. Every byte of it is either committed and pushed, or it was never wanted.

Tier Path Size Growth Domain
t-local-vin /var/lib/jarvisos/workspaces 28 MB 102 MB/mo fd-vps
t-local-home1 /var/lib/jarvisos/workspaces 2 GB est. 205 MB/mo fd-home-rack
Kept for
14 days · age — jarvisos-worker on task completion, and a weekly sweep for anything an interrupted run left behind
Recovered by
regenerate — The in-flight task restarts. Tasks are at-least-once by design, so this is the normal path rather than a recovery.
Dedupe / compression
none · none
Last restore test
never

JarvisOS service state state unique must survive the site

owned by jarvisos-control-plane

The console's durable state on each node: the incident journal, the remediation ledger, discovery snapshots and — on VIN — the local-first queue that keeps the worker working while the house is dark.

Tier Path Size Growth Domain
t-local-vin /var/lib/jarvisos 115 MB 205 MB/mo fd-vps
t-local-home1 /var/lib/jarvisos 1 GB est. 205 MB/mo fd-home-rack
t-offsite-gwscli planned jarvisos/state 2 GB est. 205 MB/mo fd-offsite replicates
Kept for
90 days · age — restic forget --keep-daily 7 --keep-weekly 8 --keep-monthly 3 on the off-site copy; the local journal is bounded by the data register instead
Recovered by
restore — Anything written since the last push is gone. For the journal that is history; for the VIN queue it is finished work that has not reached Postgres, which is why that copy is the one with the shortest interval.
Dedupe / compression
restic content-defined chunking unmeasured · zstd inside restic
Last restore test
never never from the off-site tier

What keeps credentials out: The journal records incident text and entity ids. Node identities are hashes and live in the secrets dataset, not here.

Holds these copies from the data register: journal-offsite-s3 · see what they protect →

Its own protection lives in the data register: jarvisos-vin-journal · recovery points →

Host credential material secret live secrets unique

owned by homelab-operator

The live credentials each node needs to be itself: the operator env file, node identity material, SSH host keys and the WireGuard private key. Named here so the rule about them is enforceable, never copied here.

Tier Path Size Growth Domain
t-local-vin /etc/jarvisos 1 MB 0 MB/mo fd-vps
t-local-home1 /etc/jarvisos 1 MB est. 0 MB/mo fd-home-rack
Kept for
no retention · none — Rotation, not retention. A superseded credential is revoked at its authority and deleted, never archived.
Recovered by
reissue — Every session is signed out, the tunnel needs a new peer key on both ends, and any node whose identity was re-issued must re-enrol before it can report. Minutes of work, not data loss.
Dedupe / compression
none · none
Last restore test
never

Postgres data directories state unique

owned by postgres-primary

The live data directory of the primary on home1 and of the streaming replica on VIN. This is the estate's system of record while it is being written to.

Tier Path Size Growth Domain
t-local-home1 /var/lib/postgresql/16/main 12 GB est. 1.5 GB/mo fd-home-rack
t-local-vin planned /var/lib/postgresql/16/main 12 GB est. 1.5 GB/mo fd-vps
t-offsite-gwscli planned restic:jarvisos/pg-vin 10 GB est. 512 MB/mo fd-offsite replicates
Kept for
30 days · gfs — Two different things, deliberately. The live directories are pruned by Postgres itself through checkpointing and the archive, and nothing else may delete from them. The off-site snapshots taken from the replica are pruned by `restic forget --prune` on the VIN unit.
Recovered by
restore — Stated by the data register, in the recovery point it derives from the copies.
Dedupe / compression
none · none
Last restore test
never never from the off-site tier

Holds these copies from the data register: pg-offsite-vin · see what they protect →

Its own protection lives in the data register: jarvisos-postgres · recovery points →

Database backups and WAL archive backup redacted unique must survive the site

owned by postgres-primary

Everything the data register calls a copy that lands on storage: the WAL archive, the base backups, the Supabase dump and — once running — the Coolify dump. The bytes a restore actually reads.

Tier Path Size Growth Domain
t-nas-pool /mnt/pool/backups 240 GB est. 4 GB/mo fd-home-rack replicates
t-offsite-gwscli restic:jarvisos/backups 62 GB est. 2 GB/mo fd-offsite replicates
Kept for
180 days · gfs — restic forget --prune off-site; a dated-directory sweep on the NAS
Recovered by
restore — Bounded by the copy interval, which the data register derives. What this register adds is the time to pull the bytes back over a link that was sized for uploading them slowly.
Dedupe / compression
restic content-defined chunking across snapshots unmeasured · zstd in restic, and zstd on the NAS dataset
Last restore test
2026-07-12 from t-nas-pool — pass, 22m never from the off-site tier

What keeps credentials out: The dumps are database content, and the connection strings that produce them are read from /etc/jarvisos/config.env at run time and never written into the archive. The flow that pushes this dataset off-site excludes every secret path pattern in the policy.

Holds these copies from the data register: pg-wal-nas, pg-base-nas, supabase-dump-nas, pg-offsite-home, supabase-dump-offsite · see what they protect →

Configuration and estate archive backup redacted unique must survive the site

owned by jarvisos-control-plane

The small, unglamorous bytes that make a rebuild possible: Coolify's dump, the node configuration under /etc, discovery snapshots and the estate registers as they were on the day. Losing it does not lose data — it loses the ability to put the estate back the way it was.

Tier Path Size Growth Domain
t-local-home1 /var/lib/coolify/backups 3 GB est. 102 MB/mo fd-home-rack
t-nas-pool /mnt/pool/backups/config 8 GB est. 205 MB/mo fd-home-rack replicates
t-offsite-gwscli restic:jarvisos/config 3 GB est. 205 MB/mo fd-offsite replicates
Kept for
360 days · gfs — restic forget --prune, in the same flow that writes it
Recovered by
restore — The credentials are not in it, so a restore is followed by a rotation. That is the trade this dataset makes on purpose.
Dedupe / compression
restic content-defined chunking unmeasured · zstd inside restic
Last restore test
never never from the off-site tier

What keeps credentials out: The collector copies /etc/jarvisos/*.json and the unit files, and excludes every pattern in policy.secretPatterns before the archive is written. The env file is named in the manifest and its contents are not in the archive.

Holds these copies from the data register: coolify-dump-local, coolify-dump-nas, coolify-dump-offsite · see what they protect →

Build and package cache cache redacted reproducible

owned by coolify

Docker build layers, the npm and pip stores, and whatever else a build reads twice. Pure speed: every byte is reconstructible by running the build again.

Tier Path Size Growth Domain
t-local-home1 /var/lib/docker/buildkit 60 GB est. 5 GB/mo fd-home-rack
t-nas-pool planned /mnt/pool/cache 300 GB est. 5 GB/mo fd-home-rack replicates
t-local-vin planned /var/lib/minio/jarvisos-artifacts 12 GB est. 1 GB/mo fd-vps
Kept for
30 days · age — docker buildx prune --filter until=720h, weekly
Recovered by
regenerate — One slow build per project. Nothing else.
Dedupe / compression
none · zstd on the NAS dataset
Last restore test
never

What keeps credentials out: Build arguments are the classic way a token ends up in a cached layer. Builds take secrets through Coolify's environment at run time rather than as build args, and this dataset is in no flow that leaves a node, so a leak here stays on the node that made it.

Container images artifact reproducible

owned by coolify

The image layers each node needs to start what it runs. Reproducible from the registry, but not instantly: a node with no images and no uplink starts nothing.

Tier Path Size Growth Domain
t-local-home1 /var/lib/docker/overlay2 40 GB est. 2 GB/mo fd-home-rack
t-local-vin /var/lib/docker/overlay2 6 GB est. 512 MB/mo fd-vps
Kept for
60 days · age — docker image prune --filter until=1440h, weekly, dangling and unreferenced layers only
Recovered by
regenerate — Bandwidth and minutes — unless the registry is unreachable, in which case a node that pruned its images cannot start what it was already running.
Dedupe / compression
overlay2 layer sharing between images unmeasured · none
Last restore test
never

Media library media reproducible

owned by homelab-operator

Bulk media. Large, replaceable, and deliberately not off-sited — the inventory already says so, and this register says what that costs in capacity.

Tier Path Size Growth Domain
t-nas-pool /mnt/pool/media 5.08 TB est. 40 GB/mo fd-home-rack replicates
Kept for
no retention · none — The operator, by hand, when the pool needs room.
Recovered by
regenerate — Time and effort, no data that matters. This is a policy decision recorded in the inventory, not an oversight.
Dedupe / compression
none · none
Last restore test
never

Accepted risks

Decisions on record, each with a review date

Notes

Estimated sizes, unmeasured claims and design not yet adopted

How to read this page

Source: config/storage.json · machine-readable at /api/v1/storage · narrative in docs/STORAGE-TIERS.md. Occupancy here is arithmetic over what the register places; scripts/jarvisos-storage.mjs report reads the disks on a node and prints the difference.