Skip to content

Storage Architecture & Data Safety ​

Two-folder strategy ​

h5h/
├── code/          ← this git repo (clone on any new machine)
│   ├── apps/
│   │   └── web/           h5h.me — portfolio (Vercel)
│   ├── docker/
│   │   ├── docker-compose.yml
│   │   ├── .env.example
│   │   ├── traefik/       static + dynamic config
│   │   ├── homepage/      Homepage dashboard config (sh.h5h.me)
│   │   ├── prometheus/    metrics + alert rules
│   │   ├── grafana/       dashboards + provisioning
│   │   ├── loki/          log aggregation config
│   │   ├── promtail/      log shipping config
│   │   ├── cloudflared/
│   │   └── README.md
│   ├── scripts/           setup.sh, generate-secrets.sh, protect-data.sh, setup-plex.sh, setup-qbittorrent.sh
│   ├── Dockerfile         portfolio image (Docker Hub safe)
│   ├── Makefile
│   ├── package.json
│   └── vercel.json
│
└── data/          ← all media, databases, caches (gitignored)
    ├── immich/
    ├── nextcloud/
    ├── plex/
    ├── torrent/       qBittorrent config + downloads
    ├── postgres/
    ├── grafana/
    ├── prometheus/
    ├── traefik/       acme certs + access logs
    ├── tailscale/     VPN state
    ├── crowdsec/      intrusion detection data
    ├── loki/          log storage
    └── backups/

Migrating to a mini PC? git clone for code, copy data/ to the HDD, cd code && make init && make up.

Secrets & data safety ​

This is a public repo. Configuration files (Traefik rules, Prometheus scrape targets) are committed. Secrets are never committed.

What lives where ​

LayerContentsCommitted?
code/docker/.env.exampleTemplate with placeholder valuesYes
code/docker/.envReal secrets (tokens, passwords)Never — .gitignore'd
data/Databases, uploads, certs, logsNever — outside code/

data/ protection ​

data/ is protected against accidental deletion using macOS chflags uchg on the data/ directory only (not subdirectories). This single flag gives full protection while keeping apps functional:

ActionResult
rm -rf data/Blocked — data/ is immutable
rm -rf data/immichBlocked — can't modify entries in immutable data/
git clean -fdxBlocked — can't remove data/ or any top-level subdirectory
mv data/ somewhere/Blocked — can't rename immutable directory
Apps creating runtime files/dirsAllowed — subdirectories are fully writable
Docker writing to data/loki/, data/grafana/, etc.Allowed

Why only data/ and not subdirectories? The uchg flag prevents adding/removing entries inside the flagged directory. If data/loki/ had uchg, Loki couldn't create runtime directories like tsdb-shipper-cache/. By protecting only data/, all subdirectories (immich/, loki/, postgres/, etc.) are shielded from deletion but remain writable for apps.

Additional protectionCoversHow
.gitignore + data/.gitignoregit clean -fdRespects .gitignore — data/ is safe
make cleanOnly runs docker system pruneExplicitly never touches data/
make protect / make unprotectManual lock/unlockToggle immutable flag
make initAuto-locks on first setupRuns protect-data.sh lock
bash
make protect      # Lock data/ (done automatically by make init)
make unprotect    # Unlock for maintenance
make data-status  # Check protection status

Never commit checklist ​

Before every git push, verify none of these appear in your commit:

  • [ ] CF_TUNNEL_TOKEN — Cloudflare Tunnel token
  • [ ] CF_DNS_API_TOKEN — Cloudflare DNS API token
  • [ ] AUTHENTIK_SECRET_KEY, AUTHENTIK_BOOTSTRAP_PASSWORD, AUTHENTIK_DB_PASSWORD
  • [ ] GOOGLE_CLIENT_ID, GOOGLE_CLIENT_SECRET — Google OAuth credentials
  • [ ] AUTHENTIK_GRAFANA_CLIENT_SECRET — Grafana OIDC secret
  • [ ] IMMICH_DB_PASSWORD, NEXTCLOUD_DB_PASSWORD, NEXTCLOUD_ADMIN_PASSWORD
  • [ ] PLEX_CLAIM — Plex claim token
  • [ ] GF_SECURITY_ADMIN_PASSWORD — Grafana admin password
  • [ ] TS_AUTHKEY — Tailscale auth key
  • [ ] CROWDSEC_BOUNCER_KEY — CrowdSec bouncer API key
  • [ ] HOMEPAGE_VAR_* — Homepage widget API tokens
  • [ ] data/traefik/acme/ — Let's Encrypt private keys

Future improvement: Migrate from .env to Docker secrets for database passwords and API tokens. On macOS this adds complexity (secrets require Swarm mode or Compose v2.23+), so .env is fine for v1.

Backups ​

The homelab uses a distributed multi-tier backup model across the local MacBook Air and the OCI cloud node.

Two-Node Backup Model ​

  1. OCI Node (24/7 Cloud Worker):
    • Runs an autonomous backup container (h5h_oci_backup) every night at 03:00 UTC.
    • Dumps the Authentik PostgreSQL database and configurations into a compressed archive (~10 MB).
    • Automatically uploads the archive to Cloudflare R2 (h5h-backups/oci/) with a 14-day retention policy.
    • Operates 100% independently of the MacBook's power state.

Sovereign State vs Media Decoupling ​

To prevent host SSD lockups and strictly adhere to Cloudflare R2's 10 GB Always Free tier, media files are completely decoupled from backups:

CategoryContentsSizeBackup Policy
Tier 1: Sovereign StatePostgreSQL dumps (Immich, Nextcloud, Authentik), Traefik TLS certs, Obsidian vault, Loki logs (30d audit trail), configs~327 MBBacked up to Cloudflare R2 (h5h-backups/) + rolling 2 local archives
Tier 2: Sovereign MediaImmich raw photos/videos (data/immich), Nextcloud user files (data/nextcloud/data)24 GB+Strictly excluded from daily/offsite backup tarballs
Tier 3: Ephemeral MediaPlex video libraries (data/plex), torrent scratch (data/torrent)~33 GBStateless / Zero backup

Backup Execution Methods ​

  1. Zero-Disk Streaming Backup (make backup-stream):

    • Uses rclone rcat to stream a compressed tarball directly into Cloudflare R2 (s3://h5h-backups/mac/) over TLS.
    • Writes 0 bytes to the host SSD and 0 bytes to container overlay filesystems, completely eliminating disk exhaustion risk.
  2. Container-Native Task Runner & Backup (h5h_runner):

    • Runs directly in Docker (docker-compose.yml) tied to your OrbStack container lifecycle.
    • Executes automatically upon container startup, then follows daily cron (0 3 * * *).
    • Completely offline with zero battery/process usage when OrbStack is closed.
  3. Autonomous OCI Nightly Cron (24/7/365):

    • Runs h5h_oci_backup at 03:00 UTC independently of the MacBook.
    • Streams Authentik DB state (~10 MB) to Cloudflare R2 (s3://h5h-backups/oci/) with a 14-day rolling retention.
  4. Manual Fallback (code/scripts/backup.sh):

    • Standalone CLI runner with 15 GB pre-flight check and full media exclusions. Retains the 2 most recent archives in data/backups/.

MIT License