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
| Layer | Contents | Committed? |
|---|---|---|
code/docker/.env.example | Template with placeholder values | Yes |
code/docker/.env | Real secrets (tokens, passwords) | Never — .gitignore'd |
data/ | Databases, uploads, certs, logs | Never — 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:
| Action | Result |
|---|---|
rm -rf data/ | Blocked — data/ is immutable |
rm -rf data/immich | Blocked — can't modify entries in immutable data/ |
git clean -fdx | Blocked — can't remove data/ or any top-level subdirectory |
mv data/ somewhere/ | Blocked — can't rename immutable directory |
| Apps creating runtime files/dirs | Allowed — subdirectories are fully writable |
Docker writing to data/loki/, data/grafana/, etc. | Allowed |
Why only
data/and not subdirectories? Theuchgflag prevents adding/removing entries inside the flagged directory. Ifdata/loki/haduchg, Loki couldn't create runtime directories liketsdb-shipper-cache/. By protecting onlydata/, all subdirectories (immich/,loki/,postgres/, etc.) are shielded from deletion but remain writable for apps.
| Additional protection | Covers | How |
|---|---|---|
.gitignore + data/.gitignore | git clean -fd | Respects .gitignore — data/ is safe |
make clean | Only runs docker system prune | Explicitly never touches data/ |
make protect / make unprotect | Manual lock/unlock | Toggle immutable flag |
make init | Auto-locks on first setup | Runs protect-data.sh lock |
make protect # Lock data/ (done automatically by make init)
make unprotect # Unlock for maintenance
make data-status # Check protection statusNever 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
.envto Docker secrets for database passwords and API tokens. On macOS this adds complexity (secrets require Swarm mode or Compose v2.23+), so.envis 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
- 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.
- Runs an autonomous backup container (
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:
| Category | Contents | Size | Backup Policy |
|---|---|---|---|
| Tier 1: Sovereign State | PostgreSQL dumps (Immich, Nextcloud, Authentik), Traefik TLS certs, Obsidian vault, Loki logs (30d audit trail), configs | ~327 MB | Backed up to Cloudflare R2 (h5h-backups/) + rolling 2 local archives |
| Tier 2: Sovereign Media | Immich raw photos/videos (data/immich), Nextcloud user files (data/nextcloud/data) | 24 GB+ | Strictly excluded from daily/offsite backup tarballs |
| Tier 3: Ephemeral Media | Plex video libraries (data/plex), torrent scratch (data/torrent) | ~33 GB | Stateless / Zero backup |
Backup Execution Methods
Zero-Disk Streaming Backup (
make backup-stream):- Uses
rclone rcatto 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.
- Uses
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.
- Runs directly in Docker (
Autonomous OCI Nightly Cron (24/7/365):
- Runs
h5h_oci_backupat 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.
- Runs
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/.
- Standalone CLI runner with 15 GB pre-flight check and full media exclusions. Retains the 2 most recent archives in