diff --git a/README.md b/README.md index 44c8b7a..8acac97 100644 --- a/README.md +++ b/README.md @@ -16,6 +16,7 @@ docker/ Docker app configs (compose files, nginx confs) nextcloud/ Nextcloud + PostgreSQL gitea/ Gitea + CI runner jellyfin/ Media server + navidrome/ Music server (Subsonic API), reads the [music] SMB share garage/ S3-compatible storage minecraft/ MCSManager immich/ Immich photo server + PostgreSQL @@ -104,5 +105,6 @@ See [RUNBOOK.md](RUNBOOK.md) for full disaster recovery steps. | Immich DB (PostgreSQL) | `pg_dumpall` + USB | | Garage S3 data | 2-node replication + USB | | Gitea repos + DB | SQLite `.backup` + USB | +| Navidrome DB (ratings, playlists) | SQLite `.backup` + USB | | Secrets (.env files) | USB backup + Bitwarden | | SSL certificates | Re-issued from Let's Encrypt | diff --git a/RUNBOOK.md b/RUNBOOK.md index e0114c0..f8b0a13 100644 --- a/RUNBOOK.md +++ b/RUNBOOK.md @@ -128,6 +128,12 @@ for u in yireh isaac; do chmod 2770 "/opt/argento/smb/$u" done +# Shared music dir. Lives inside the media dataset (so it rides the media +# snapshot policy) but is exported as its own writable [music] share. +# setgid so files dropped in over SMB inherit sambagroup and stay readable +# by the Navidrome container. +install -d -o timothykim -g sambagroup -m 2775 /opt/argento/smb/media/Music + # SELinux contexts for samba — fresh ZFS datasets get unlabeled_t and samba # refuses to serve them. Persistent rules (semanage) survive full relabels; # restorecon applies them now. @@ -232,7 +238,8 @@ docker compose up -d # Issue certs (add any new domains that have been added since this was written) /opt/argento/scripts/issue-cert.sh \ nextcloud.timothykim.net git.timothykim.net jellyfin.timothykim.net \ - argento.timothykim.net garage.timothykim.net photos.thekims.family + argento.timothykim.net garage.timothykim.net photos.thekims.family \ + music.thekims.family # Restore SSL confs now that certs exist mv conf.d/.hold/*.conf conf.d/ @@ -258,7 +265,7 @@ Deploy each app (nginx is already running and will return 502 until they start): ```bash # Add any new apps that have been added since this was written -for app in nextcloud gitea jellyfin garage minecraft immich beszel-agent; do +for app in nextcloud gitea jellyfin navidrome garage minecraft immich beszel-agent; do cd /opt/argento/docker/$app docker compose pull docker compose build @@ -291,6 +298,7 @@ CNAME records pointing to the DDNS hostname. These don't need updating when the | `garage.timothykim.net` | CNAME | `argento.ddns.net` | | `minecraft.thekims.family` | CNAME | `argento.ddns.net` | | `photos.thekims.family` | CNAME | `argento.ddns.net` | +| `music.thekims.family` | CNAME | `argento.ddns.net` | ## 14. Verify @@ -301,6 +309,7 @@ curl -I https://jellyfin.timothykim.net curl -I https://argento.timothykim.net curl -I https://garage.timothykim.net curl -I https://photos.thekims.family +curl -I https://music.thekims.family curl -I http://minecraft.thekims.family # Verify SMART, ZFS, and disk space are healthy diff --git a/docker/navidrome/.gitignore b/docker/navidrome/.gitignore new file mode 100644 index 0000000..8fce603 --- /dev/null +++ b/docker/navidrome/.gitignore @@ -0,0 +1 @@ +data/ diff --git a/docker/nginx/conf.d/music.thekims.family.conf b/docker/nginx/conf.d/music.thekims.family.conf new file mode 100644 index 0000000..9378413 --- /dev/null +++ b/docker/nginx/conf.d/music.thekims.family.conf @@ -0,0 +1,43 @@ +server { + listen 80; + listen [::]:80; + server_name music.thekims.family; + + location /.well-known/acme-challenge/ { + root /var/www/certbot; + } + + location / { + return 301 https://$host$request_uri; + } +} + +server { + listen 443 ssl; + listen [::]:443 ssl; + http2 on; + server_name music.thekims.family; + + ssl_certificate /etc/letsencrypt/live/music.thekims.family/fullchain.pem; + ssl_certificate_key /etc/letsencrypt/live/music.thekims.family/privkey.pem; + + include /etc/nginx/conf.d/security-headers.inc; + + # Use a variable so nginx starts even if the upstream is down + set $upstream_navidrome http://navidrome:4533; + + location / { + proxy_pass $upstream_navidrome; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + + # Navidrome streams scan progress and now-playing state over + # Server-Sent Events on /api/events. Buffering holds those messages + # until the response ends (it never does), and HTTP/1.0 to the upstream + # can't carry the chunked response the stream needs. + proxy_http_version 1.1; + proxy_buffering off; + } +} diff --git a/scripts/backup.sh b/scripts/backup.sh index dbb50ab..e9cb630 100755 --- a/scripts/backup.sh +++ b/scripts/backup.sh @@ -40,6 +40,11 @@ for db in jellyfin library; do || FAILURES+=("jellyfin ${db} sqlite backup") done +# Dump Navidrome SQLite for consistency +sqlite3 /opt/argento/docker/navidrome/data/navidrome.db \ + ".backup /opt/argento/docker/navidrome/data/navidrome.db.bak" \ + || FAILURES+=("navidrome sqlite backup") + # Single rsync covers everything: configs, app data, media, DB dumps, # and the samba passdb (mirrored into /opt/argento/system/samba-private/ # by sync-system.sh, gitignored)