Skip to content

10 — System Reference

Quick-lookup reference for paths, ports, services, and identifiers. Bookmark this one — it's the "I just need the answer" doc.


Hardware

Component Spec
Model HP Z2 Mini G5 Workstation
CPU Intel Core i9-10900K (10 cores, 20 threads)
RAM 64GB DDR4-3200 SODIMM (2x32GB)
GPU NVIDIA Quadro T2000 (4GB GDDR5)
OS Drive Samsung MZVLB1T0HBLR-000H1 (1TB NVMe)
Data Drive SK hynix PC601 HFS001TD9TNG-L2A0A (1TB NVMe)
Backup Drive Samsung Portable SSD T5 (500GB USB)
Network Wi-Fi (ethernet planned September 2026)

Operating system

Item Value
Distribution Ubuntu Server 24.04 LTS
Hostname z2mini
Primary user gabriel
User groups sudo, docker

Storage layout

/dev/nvme1n1 (Samsung 1TB) — OS drive
├── /dev/nvme1n1p1 → /boot/efi  (1G, vfat)
└── /dev/nvme1n1p2 → /          (953G, ext4)

/dev/nvme0n1 (SK hynix 1TB) — Data drive
└── /dev/nvme0n1p1 → /data     (953G, ext4)

/dev/sdb (Samsung 990 PRO 1TB, ASM2462 USB-NVMe enclosure) — Backup drive
└── /dev/sdb1 → /mnt/backup    (916G, ext4)
                 UUID=8795cb2e-fe34-4543-b93b-dd45b642846f
                 (SMART needs `-d sntasmedia`; the /dev/sdX name is not stable — see 06-drive-monitoring.md)

Note: NVMe device names (nvme0n1 vs nvme1n1) can occasionally swap between reboots. Always verify with lsblk before scripting against device names — use UUIDs in /etc/fstab.


Reserved paths (in use, don't repurpose)

Path Used by
/data/files/ Samba [files] share — user data
/data/docker/ Docker service data (Immich, icloudpd, future services)
/data/docker/immich/library/ Immich photo + thumbnail library (bind-mounted into container)
/data/docker/immich/postgres/ Immich Postgres data dir (mode 700, uid 999) — NEVER filesystem-rsync
/data/docker/immich/model-cache/ Immich ML model weights
/data/docker/icloudpd/cookies/ icloudpd Apple session cookies (root-owned, ~2 month validity)
/data/docker/navidrome/data/ Navidrome SQLite DB, cache, thumbnails (uid 1000)
/data/music/ Music library — bind-mounted read-only into Navidrome; Samba [music] share
/data/docker/homepage/config/ Homepage YAML config (services, bookmarks, widgets, settings)
/data/docker/beszel/data/ Beszel hub SQLite DB + PocketBase assets — NEVER filesystem-rsync live
/data/docker/beszel/socket/ Beszel hub↔agent unix socket (transient — never back up)
/data/docker/beszel/agent-data/ Beszel agent fingerprint + buffer (rebuildable)
/data/docker/radicale/data/collections/ Radicale calendars + contacts as .ics/.vcf files — files-as-truth, owned 2999:2999, rsync-safe
/data/docker/radicale/config/ Radicale main config + htpasswd users file (mode 644 — bcrypt hashes)
/data/docker/vaultwarden/data/ Vaultwarden SQLite DB + rsa_key.pem (JWT signing key) + attachments/ + sends/ — owned 1000:1000; db.sqlite3 is WAL-mode, NEVER raw-rsync live (use .backup)
/data/docker/caddy/ Caddy reverse-proxy stack: Dockerfile + docker-compose.yml + Caddyfile + .env (mode 600 — CF_API_TOKEN) + data/ (issued certs + ACME account, auto-renewing) + config/ (autosave)
/data/docker/openproject/ OpenProject stack — upstream opf/openproject-deploy (stable/17) cloned verbatim. .env (mode 600 — SECRET_KEY_BASE + COLLABORATIVE_SERVER_SECRET + POSTGRES_PASSWORD) is the only file edited.
/data/docker/openproject/postgres/ OpenProject Postgres data dir (mode 700, uid 999) — NEVER filesystem-rsync. Captured via nightly pg_dump instead.
/data/docker/openproject/assets/ OpenProject uploaded attachments + exports (uid 1000) — files-as-truth, rsync-safe.
/data/docker/openproject/backups/ Nightly pg_dump output (openproject-db-YYYY-MM-DD.sql.gz); last 14 retained. Picked up by the per-service rsync.
/data/icloud-import/ Was transient iCloud → Immich migration staging; deleted 2026-05-05 after ingest verified
/mnt/backup/current/ Latest backup mirror
/mnt/backup/daily/ Daily snapshots (7 retained)
/mnt/backup/weekly/ Weekly snapshots (4 retained)
/mnt/backup/system-state/ System config archives
/mnt/backup/backup.log Backup log
/mnt/backup/RECOVERY-README.md Disaster recovery procedure
/home/gabriel/scripts/ User scripts and small one-off binaries (immich-go)
/home/gabriel/.immich-api-key Immich admin API key (mode 600, used by immich-go)
/usr/local/sbin/backup-system-state.sh Privileged backup wrapper

Network

Item Value
Tailnet elk-kanyu.ts.net
Z2 hostname (FQDN) z2mini.elk-kanyu.ts.net
Custom domain (DNS via Cloudflare) *.z2mini.gabrielgabrie.com100.67.235.68 (wildcard A, DNS-only); Hostinger remains the registrar
Tailscale plan Free Personal
Tailscale SSH Enabled
Tailscale operator gabriel (so tailscale serve/set don't need sudo)
Tailscale global resolvers 1.1.1.1, 8.8.8.8, "Override local DNS" ON (so tailnet devices resolve *.z2mini.gabrielgabrie.com — the housing router's DNS chokes on it)

Reserved ports

Port Service Interface
22 SSH (via Tailscale SSH) tailnet
80 Caddy — HTTP→HTTPS redirect 100.67.235.68 only
137-139 Samba NetBIOS all
443 Caddy — HTTPS reverse proxy for all web services (<svc>.z2mini.gabrielgabrie.com); auto-renewing Let's Encrypt certs via ACME DNS-01/Cloudflare 100.67.235.68 only
445 Samba SMB all
2283 Immich web UI + mobile API 127.0.0.1 only (fronted by Caddy → immich.z2mini.gabrielgabrie.com)
3000 Homepage launcher dashboard 127.0.0.1 only (fronted by Caddy → home.z2mini.gabrielgabrie.com)
4533 Navidrome web UI + Subsonic API 127.0.0.1 only (fronted by Caddy → navidrome.z2mini.gabrielgabrie.com)
5232 Radicale CalDAV/CardDAV (HTTP) 127.0.0.1 only (fronted by Caddy → radicale.z2mini.gabrielgabrie.com)
8080 Vaultwarden web vault + API (HTTP, container) 127.0.0.1 only (fronted by Caddy → vault.z2mini.gabrielgabrie.com)
8082 OpenProject internal proxy (publishes the stack) 127.0.0.1 only (fronted by Caddy → openproject.z2mini.gabrielgabrie.com)
8090 Beszel hub web UI + agent ingest 127.0.0.1 only (fronted by Caddy → beszel.z2mini.gabrielgabrie.com)
2019 Caddy admin API (config / reload) 127.0.0.1 only (host loopback — Caddy runs network_mode: host)

No services exposed to the public internet. Caddy (port 443, bound to the Tailscale interface IP only, network_mode: host) is the single HTTPS ingress — it terminates TLS with auto-renewing Let's Encrypt certs obtained via the ACME DNS-01 challenge through the Cloudflare API, and reverse-proxies to each app on 127.0.0.1:<port>. Its config is all under /data/docker/caddy/ (Caddyfile + .env token) — unlike the old tailscale serve listeners it replaced, which lived in tailscaled's state. See 17-caddy.md. The two old tailscale serve HTTPS front-ends (:443 Radicale, :8443 Vaultwarden) are retired.

Heads-up: every app binds 127.0.0.1:<port> only — so scripts on z2mini can use http://127.0.0.1:<port> or http://localhost:<port> directly (the old "must use the tailnet IP" gotcha is gone). https://<svc>.z2mini.gabrielgabrie.com also works from the box. What no longer works: http://z2mini:<port> and http://100.67.235.68:<port> — those bindings were removed when the apps moved behind Caddy. From other tailnet devices, the only way in is https://<svc>.z2mini.gabrielgabrie.com (which requires the device's DNS to resolve *.z2mini.gabrielgabrie.com — see the Tailscale global resolvers note above).


Services

systemd-managed

Service Purpose Status command
smbd Samba file sharing sudo systemctl status smbd
tailscaled Tailscale daemon sudo systemctl status tailscaled
smartd Drive health monitoring sudo systemctl status smartd
cron Scheduled jobs sudo systemctl status cron
systemd-networkd Network config networkctl status
wpa_supplicant Wi-Fi auth sudo systemctl status wpa_supplicant

Docker-managed

Container Stack Image Purpose
immich_server Immich ghcr.io/immich-app/immich-server Web UI + REST API + microservices
immich_machine_learning Immich ghcr.io/immich-app/immich-machine-learning:*-cuda Face recognition, smart search, OCR — GPU-accelerated on the Quadro T2000
immich_postgres Immich ghcr.io/immich-app/postgres (vectorchord) DB + vector embeddings
immich_redis Immich valkey/valkey:9 Job queue + cache
navidrome Navidrome deluan/navidrome Music server — web UI + Subsonic-compatible API + file watcher
homepage Homepage ghcr.io/gethomepage/homepage Launcher dashboard + service-API widgets
beszel Beszel henrygd/beszel System-metrics hub — web UI, SQLite DB, alerts
beszel-agent Beszel henrygd/beszel-agent System-metrics agent — collects CPU/RAM/disk/net/Docker stats from host (network_mode: host)
radicale Radicale tomsquest/docker-radicale CalDAV + CardDAV server (calendars + contacts), htpasswd auth, web UI for collection management
vaultwarden Vaultwarden vaultwarden/server Bitwarden-compatible password server — web vault, REST API, identity, icons, WebSocket sync. Runs as user: 1000:1000.
caddy Caddy caddy-cloudflare:2.11.2 (locally built: caddy + caddy-dns/cloudflare plugin) HTTPS reverse proxy / single ingress — TLS termination + ACME DNS-01 client. network_mode: host.
openproject-web-1 OpenProject openproject/openproject:17-slim Rails web app (Puma, 4–16 threads) — UI + REST API + Hocuspocus client
openproject-worker-1 OpenProject openproject/openproject:17-slim Background jobs — mail, exports, search index, notifications
openproject-cron-1 OpenProject openproject/openproject:17-slim Scheduled-jobs runner (daily/weekly maintenance)
openproject-seeder-1 OpenProject openproject/openproject:17-slim Runs db:migrate + db:seed on every up, then exits
openproject-db-1 OpenProject postgres:17 Primary database
openproject-cache-1 OpenProject memcached Rails cache store
openproject-proxy-1 OpenProject openproject/proxy (built locally) Internal proxy — publishes the stack on 127.0.0.1:8082
openproject-hocuspocus-1 OpenProject openproject/hocuspocus:17.4.0 WebSocket server for collaborative editing
openproject-autoheal-1 OpenProject willfarrell/autoheal:1.2.0 Restarts web if its healthcheck fails (Docker socket RW)

Each stack defined under /data/docker/<stack>/docker-compose.yml. Status: cd /data/docker/<stack> && docker compose ps. (Caddy is a locally built image — docker compose build rebuilds it; see 17-caddy.md.)

One-shot Docker tool

Tool Image Purpose
icloudpd icloudpd/icloudpd:1.32.2 One-shot bulk download from iCloud Photos. Defined in /data/docker/icloudpd/; invoked with docker compose run --rm, not up -d.

Samba shares

Share Path Mode Access
\\z2mini\files /data/files Read-write gabriel only
\\z2mini\music /data/music Read-write gabriel only
\\z2mini\backup /mnt/backup Read-only gabriel only

Both shares require Samba authentication — separate password from Linux login.


Key configuration files

Purpose File
Samba config /etc/samba/smb.conf
Samba config backup /etc/samba/smb.conf.backup
Filesystem mounts /etc/fstab
Hostname /etc/hostname
smartd config /etc/smartd.conf
msmtp config /etc/msmtprc
msmtp AppArmor override /etc/apparmor.d/local/usr.bin.msmtp
Backup log rotation /etc/logrotate.d/backup-files
msmtp log rotation /etc/logrotate.d/msmtp
Sudo grant for backup /etc/sudoers.d/gabriel-backup
Immich Compose /data/docker/immich/docker-compose.yml
Immich env (DB password etc.) /data/docker/immich/.env (mode 600)
icloudpd Compose /data/docker/icloudpd/docker-compose.yml
Navidrome Compose /data/docker/navidrome/docker-compose.yml
Navidrome env (version pin) /data/docker/navidrome/.env
Homepage Compose /data/docker/homepage/docker-compose.yml
Homepage env (widget API tokens) /data/docker/homepage/.env (mode 600)
Homepage YAML config /data/docker/homepage/config/*.yaml
Beszel Compose /data/docker/beszel/docker-compose.yml
Beszel env (version pin + agent KEY/TOKEN) /data/docker/beszel/.env (mode 600)
Radicale Compose /data/docker/radicale/docker-compose.yml
Radicale env (version pin) /data/docker/radicale/.env
Radicale main config (INI) /data/docker/radicale/config/config
Radicale htpasswd users /data/docker/radicale/config/users (mode 644 — bcrypt)
Vaultwarden Compose /data/docker/vaultwarden/docker-compose.yml
Vaultwarden env (version pin + DOMAIN; admin token / push keys if added) /data/docker/vaultwarden/.env (mode 600)
Caddy Dockerfile (stock caddy + caddy-dns/cloudflare) /data/docker/caddy/Dockerfile
Caddy Compose /data/docker/caddy/docker-compose.yml
Caddy env (Cloudflare API token) /data/docker/caddy/.env (mode 600 — CF_API_TOKEN)
Caddyfile (global options + one block per service) /data/docker/caddy/Caddyfile
OpenProject Compose (upstream verbatim) /data/docker/openproject/docker-compose.yml
OpenProject env (version pin + hostname + 3 secrets + bind-mount paths) /data/docker/openproject/.env (mode 600)

Scripts

Script Purpose Runs as Schedule
~/scripts/backup-files.sh Daily backup script gabriel (via cron) 3 AM daily
~/scripts/check-backup-mount.sh Hourly backup-drive mount check gabriel (via cron) Every hour
~/scripts/icloudpd-bulk-download.sh iCloud → staging bulk download (icloudpd watch loop) gabriel (in tmux) On demand during migration
~/scripts/immich-go Bulk-import staging into Immich via API gabriel On demand after icloudpd completes
~/scripts/tailscale-token-age-check.sh Weekly check of Homepage's Tailscale token age; emails on day 75 of the 90-day cycle gabriel (via cron) Mondays 09:00
/usr/local/sbin/backup-system-state.sh Privileged wrapper for system-state backup root (via sudo from backup-files.sh) Called by backup-files.sh

Logs

Log Location
Backup log /mnt/backup/backup.log
msmtp log /var/log/msmtp.log
System logs journalctl (e.g., sudo journalctl -u smbd)
Boot logs sudo journalctl -b

Cron jobs

0 3 * * * /home/gabriel/scripts/backup-files.sh
0 * * * * /home/gabriel/scripts/check-backup-mount.sh
0 9 * * 1 /home/gabriel/scripts/tailscale-token-age-check.sh

User crontab. View: crontab -l. Edit: crontab -e.

  • 3 AM daily: full backup (data + system state) to USB SSD
  • Hourly: check backup drive is mounted, alert via email if not
  • Mondays 09:00: warn via email when Homepage's Tailscale API token is older than 75 days (90-day cycle)

External accounts and credentials

Service Account Notes
Tailscale gabrielgabrie99@gmail.com Free Personal plan
Gmail (for SMTP relay) gabrielgabrie99@gmail.com App password in /etc/msmtprc

App passwords should be rotated periodically.


Expected performance

Path Expected speed
Local file ops on Z2 ~3000 MB/s (NVMe)
SMB over Wi-Fi (same LAN) ~50-100 MB/s
SMB over Tailscale (off-network) varies — depends on internet upload speed
Initial backup of full /data minutes to hours depending on data size
Incremental daily backup seconds to minutes

Decisions already made (don't revisit casually)

  • Linux server, not Windows
  • File sharing via SMB (not Nextcloud, not WebDAV)
  • Samba native install (not Docker)
  • Tailscale (not Headscale, not OpenVPN, not WireGuard direct)
  • ext4 filesystems (not ZFS, not Btrfs)
  • OS and data on separate drives (not RAID)
  • No public internet exposure (Tailscale only)
  • Local backups only (off-site at parents' planned, no cloud)
  • 7 daily / 4 weekly snapshot retention
  • Immich for photo storage (not PhotoPrism, Ente, or Photoview)
  • Immich Storage Template Engine ON (date-organized library on disk)
  • GPU acceleration enabled (Quadro T2000, NVIDIA driver 595, nvidia-container-toolkit; pre-signed kernel modules to keep Secure Boot on)
  • iCloud → server migration via icloudpd (not CopyTrans Cloudly)
  • immich-go for bulk import (not web UI drag-and-drop or iOS app)
  • Homepage for the launcher dashboard (not Glance, Dashy, Heimdall, Homer, Flame)
  • Beszel for system-metrics dashboard (not Netdata, Glances, Grafana+Prometheus)
  • Beszel hub + agent in same compose file communicating via unix socket (not TCP)
  • Beszel emails directly to smtp.gmail.com:587 (separate Gmail app password from msmtp's, for revocation independence)
  • Radicale for self-hosted CalDAV/CardDAV calendar + contacts (not Baïkal, not Nextcloud)
  • Vaultwarden for self-hosted passwords (not the official Bitwarden self-host image, not KeePassXC)
  • Vaultwarden runs as uid 1000 (gabriel), not the image-default root — so cap_drop: ALL is viable and data/ stays gabriel-owned
  • Vaultwarden ROCKET_PORT=8080; DOMAIN=https://vault.z2mini.gabrielgabrie.com
  • Vaultwarden registration closed after the single account was created (SIGNUPS_ALLOWED=false); admin panel and SMTP left disabled
  • TOTP and passkeys to be consolidated into the Vaultwarden vault (decided May 2026; re-enroll off Microsoft Authenticator gradually; school/work accounts that mandate the Microsoft app stay on it)
  • Caddy as the single HTTPS reverse-proxy ingress (not Traefik, nginx, NPM); replaced the per-service tailscale serve listeners (May 2026)
  • All web apps bound to 127.0.0.1:<port> only; Caddy (network_mode: host, listening on 100.67.235.68:443 + :80) is the one door in — <svc>.z2mini.gabrielgabrie.com
  • Caddy image built locally = stock caddy + the caddy-dns/cloudflare plugin (needed for ACME DNS-01 — no public inbound, so HTTP-01/TLS-ALPN-01 unusable)
  • DNS for gabrielgabrie.com moved to Cloudflare (Hostinger still the registrar); *.z2mini wildcard A → 100.67.235.68; the Cloudflare API token (scoped Zone.DNS:Edit) lives in /data/docker/caddy/.env
  • Tailscale operator set to gabriel; Tailscale global resolvers 1.1.1.1+8.8.8.8 with "Override local DNS" (so tailnet devices resolve *.z2mini.gabrielgabrie.com — the housing router's DNS rejects/stale-caches it)
  • OpenProject chosen as the project-management / PMP-learning tool (not Planka / Vikunja / Kanboard / WeKan / Taiga / Redmine); upstream opf/openproject-deploy stable/17 cloned verbatim into /data/docker/openproject/, customized only via .env. Bind-mounted postgres/ (uid 999 mode 700, never raw-rsync) + assets/ (uid 1000, files-as-truth) + backups/ (nightly pg_dump). Port 127.0.0.1:8082 fronted by Caddy at https://openproject.z2mini.gabrielgabrie.com. autoheal kept upstream-verbatim despite the Docker-socket-RW trade-off (single-user tailnet box; auto-restart on Rails OOM is useful).

These were considered carefully — see z2mini-context-for-ai.md for reasoning. Suggesting reversal requires a real reason.


What's intentionally NOT installed

  • Nextcloud (rejected — poor iOS Files integration)
  • Docker containers for Samba (rejected — runs better natively)
  • Traefik / nginx / Nginx Proxy Manager — Caddy chosen instead as the reverse proxy (smallest config, automatic HTTPS+renewal built in); see 17-caddy.md
  • TrueNAS / Unraid (rejected — designed for multi-drive RAID setups)
  • Snap packages (rejected — using Docker for future services instead)
  • PhotoPrism, Ente, Photoview (evaluated as Immich alternatives; Immich won on iPhone integration + ML)
  • CopyTrans Cloudly (evaluated for iCloud bulk download; rejected in favor of FOSS server-side icloudpd. Kept as Plan B insurance if icloudpd fails)
  • iCloud for Windows (evaluated for iCloud bulk download; rejected — needs 458 GB+ of free local laptop storage)
  • Official Bitwarden self-hosted ("unified" container) — rejected vs Vaultwarden (~1 GB+ RAM, bundled DB engine, a few features license-gated); identical clients either way
  • KeePassXC + .kdbx over the Samba share (evaluated as a Vaultwarden alternative — server-less, files-as-truth — but loses on iOS: third-party iOS apps, sync-conflict risk, clunkier autofill)
  • Standalone Bitwarden Authenticator app (TOTP-only; not needed — Vaultwarden's TOTP-in-vault is free)
  • Cloud-Mac rental for the iCloud Passwords export (MacinCloud trial attempted 2026-05-11; Apple's anti-fraud blocks iCloud sign-in from datacenter IPs — abandoned. Migration was eventually done on a trusted personal MacBook, 2026-05-22.)