aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.env.enc5
-rw-r--r--.gitignore2
-rw-r--r--.sops.yaml4
-rw-r--r--README.md24
-rw-r--r--caddy/Caddyfile9
-rwxr-xr-xdeploy.sh33
-rw-r--r--docker-compose.yml6
7 files changed, 75 insertions, 8 deletions
diff --git a/.env.enc b/.env.enc
index d289189..627ea75 100644
--- a/.env.enc
+++ b/.env.enc
@@ -28,9 +28,10 @@ RADARR_API_KEY=ENC[AES256_GCM,data:DHtJK8BSfruEzCbPkSuio7/qiw137rB2UZzyVjzelAU=,
SONARR_API_KEY=ENC[AES256_GCM,data:pz18U25UTap7JFLAj0I7SYrR+Y2HFxUhNgFSfkZsW8o=,iv:ps47Vp3/QoqYtfAt+U9ZS1/87K9+o/xBa/SX0ZqdwiA=,tag:P2oZIYB7WEOS7BWX/m/YBQ==,type:str]
PROWLARR_API_KEY=ENC[AES256_GCM,data:b6GYF0USAHqlmezOQlQMO1VwTUjAn1GjgoRkgaSXd5o=,iv:bGgXTV1hqFOZOkPIzP63c1lsEixropX3T2BLqwmZYN4=,tag:tyYc+Rq+guWigoUghPbEgQ==,type:str]
BAZARR_API_KEY=ENC[AES256_GCM,data:gr1guUQ17CUxuwPzDGtXegFPVA4CgIdJvWXAdxXf8nE=,iv:zX4faC5KE88E5GfG29rvFYd5FGAPJt6l+7wT6WQ5c7c=,tag:Bv/ZbW3IUiMnJFSChRpRfQ==,type:str]
+LAN_IP=ENC[AES256_GCM,data:sbYMEnIuCIcTU5Ek,iv:d5NysqFShXysQR5PUlUESiY3Uw0ri6MKgK4wi3oAhX8=,tag:Z/6DNZevYFWE4cU7wP3pmw==,type:str]
sops_age__list_0__map_enc=-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSB2WFE5QzNXNlhxakx3T3Yv\nV1FqVXBsQ21DdDB4R05hUGpqM09Qd0FsTno0CjJlL1lYNDNHVDBsRnRJNkdNcnJ0\nZnVVRFFWazcxUmV0emNTMzVLajNMdDQKLS0tIHBBWGRuWXdIWHYrcjJTeXVzLzZE\nVnpTeUNrQ205Tm1WODRLQkdablFaajAKM7W7VXTND6e33myo4rP/kFWQ938gFX1Y\nTRXBBCFMxJwRAjp9XE2IFsxxDHaHJnj/6O3k6kEyQsGZUwbWKfz7Pg==\n-----END AGE ENCRYPTED FILE-----\n
sops_age__list_0__map_recipient=age1sv5yc279d8j66fx36awxhdnsqjp3pv8x9yc0ak6xa9qy9egg6e8qegccg4
-sops_lastmodified=2026-07-19T18:58:17Z
-sops_mac=ENC[AES256_GCM,data:3SmAp/szkWnkZOTfyapqG/1LiBn52I6XVQrnYe8F6FeT1fGwgmEkp8iXOMq7mdzoDxd0lGrN/rs6URQNok05DMtAVCyZkz1eXI7VN7kpza6FHwTI520IwV0oEyzJA9995X3E0+c1TuL9c7okdiEi0QLXcmt4XvzegNPfRCV0J2g=,iv:A2E/fnzn0zcB18OPhrBsCazxbTiKtmOmqdWZ1+pVA/M=,tag:eAOrpmF/XIu10VkQ3K6HdQ==,type:str]
+sops_lastmodified=2026-07-26T16:56:26Z
+sops_mac=ENC[AES256_GCM,data:1OcJFIhZmpD37vzwyC6H9LaPFmHeSy4ky6L9mn2W+bxeS+efR30VYvdjjqd/ejoExTgoEtt+Iq2nDoLdC+bv9ZUcBcTsQu6IDcYQ1lc/KyFN1CYkWIYPgOhT2ePnMfKQcayedx1zmVaQlumdAeM1XNadDul2hTneontZtyUyCzs=,iv:/cJGM+Qmx9GK+VinC6gBq8O9EP5+UL87uVwoE4O0zrI=,tag:6gfNlqYTT5jpXPajvKeUaA==,type:str]
sops_unencrypted_suffix=_unencrypted
sops_version=3.13.2
diff --git a/.gitignore b/.gitignore
index 4c49bd7..522236b 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1 +1,3 @@
.env
+.env.*
+!.env.enc
diff --git a/.sops.yaml b/.sops.yaml
index bb2fede..4056ae6 100644
--- a/.sops.yaml
+++ b/.sops.yaml
@@ -1,3 +1,7 @@
creation_rules:
- path_regex: \.env$
age: age1sv5yc279d8j66fx36awxhdnsqjp3pv8x9yc0ak6xa9qy9egg6e8qegccg4
+ - path_regex: \.env\.enc$
+ age: age1sv5yc279d8j66fx36awxhdnsqjp3pv8x9yc0ak6xa9qy9egg6e8qegccg4
+ input_type: dotenv
+ output_type: dotenv
diff --git a/README.md b/README.md
index cc24110..c0c887a 100644
--- a/README.md
+++ b/README.md
@@ -1,12 +1,26 @@
# goodall
-Docker Compose media stack (Jellyfin, Radarr, Sonarr, Prowlarr, Bazarr, qBittorrent) fronted by Caddy, reachable only over Tailscale.
+Docker Compose media stack (Jellyfin, Radarr, Sonarr, Prowlarr, Bazarr, qBittorrent) fronted by Caddy, reachable only over Tailscale. qBittorrent's traffic is routed entirely through Gluetun (WireGuard to Mullvad) via `network_mode: "service:gluetun"`.
-Deployed on the NAS at `/volume2/docker/goodall` (host `karan@100.121.200.111`, tailnet name `goodall.taildc6b0a.ts.net`). This local directory is the source of truth for edits; redeploy a changed file with:
+Deployed on the NAS at `/volume2/docker/goodall` (host `karan@100.121.200.111`, tailnet name `goodall.taildc6b0a.ts.net`). This local directory is the source of truth for edits; ship a full deploy with:
```
-ssh karan@100.121.200.111 "cat > /volume2/docker/goodall/<file>" < ./<file>
-ssh karan@100.121.200.111 "cd /volume2/docker/goodall && sudo docker compose up -d"
+./deploy.sh
```
-See [MIGRATION_STATUS.md](MIGRATION_STATUS.md) for current deployment status and remaining work.
+This clears the project directory on the NAS (preserving `config/` runtime state and `.env`), streams the repo over via tar+SSH (DSM's bundled rsync only supports its own daemon/module mode, not plain SSH+path, so this project uses tar instead), decrypts `.env.enc` and writes it directly to the NAS, then runs `docker compose pull && up -d` remotely.
+
+Note: this NAS's Docker Manager GUI doesn't show `goodall` as a project since it was deployed via SSH/CLI rather than the GUI's own wizard — cosmetic only, the stack runs fine either way.
+
+## Secrets
+
+All secrets and per-deployment config (VPN credentials, Gandi DNS token, LAN/Tailscale IPs, PUID/PGID/TZ, media paths, and the four app API keys) live encrypted in `.env.enc`, via sops. To view: `sops -d --input-type dotenv --output-type dotenv .env.enc`. To edit: `sops --input-type dotenv --output-type dotenv .env.enc` (opens `$EDITOR` on the decrypted content, re-encrypts on save). `deploy.sh` decrypts this on every run and writes it to the NAS directly — there's no plaintext `.env` to hand-maintain locally.
+
+## Updates
+
+All images (including Gluetun) track `:latest`. Nothing auto-updates — re-run `./deploy.sh` to pull and recreate everything. This is deliberate: `gluetun` and `qbittorrent` share a network namespace (`network_mode: "service:gluetun"`), so updates need to recreate both together, which a project-level `docker compose pull && up -d` does correctly and a per-container auto-updater would not.
+
+## Gotchas
+
+- **Gluetun blocks inbound container-to-container traffic by default** — `FIREWALL_INPUT_PORTS=8123` in `docker-compose.yml` is required, or qBittorrent's WebUI (proxied by Caddy for `dl.karanj.com`) returns a 502.
+- **Caddy's Gandi DNS-01 challenge needs `propagation_timeout -1`** in the `tls_gandi` snippet (`caddy/Caddyfile`) — Gandi's own nameserver propagation is inconsistent across resolvers, so Caddy's own pre-check needs to be skipped and left to Let's Encrypt to validate directly.
diff --git a/caddy/Caddyfile b/caddy/Caddyfile
index 9d941d0..4f38033 100644
--- a/caddy/Caddyfile
+++ b/caddy/Caddyfile
@@ -38,3 +38,12 @@ dl.{$DOMAIN} {
import tls_gandi
reverse_proxy gluetun:8123
}
+
+nas.{$DOMAIN} {
+ import tls_gandi
+ reverse_proxy https://host.docker.internal:9443 {
+ transport http {
+ tls_insecure_skip_verify
+ }
+ }
+}
diff --git a/deploy.sh b/deploy.sh
new file mode 100755
index 0000000..9c547cf
--- /dev/null
+++ b/deploy.sh
@@ -0,0 +1,33 @@
+#!/usr/bin/env bash
+set -euo pipefail
+
+NAS_HOST="karan@100.121.200.111"
+NAS_PATH="/volume2/docker/goodall"
+REPO_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
+cd "$REPO_DIR"
+
+echo "==> Checking SSH connectivity to ${NAS_HOST}"
+if ! ssh -o ConnectTimeout=5 -o BatchMode=yes "$NAS_HOST" true; then
+ echo "ERROR: cannot reach ${NAS_HOST} over SSH (tailscale down?)" >&2
+ exit 1
+fi
+
+echo "==> Clearing stale project files on ${NAS_HOST}:${NAS_PATH} (preserving config/ and .env)"
+ssh "$NAS_HOST" "mkdir -p ${NAS_PATH} && find ${NAS_PATH} -mindepth 1 -maxdepth 1 ! -name 'config' ! -name '.env' -exec rm -rf {} +"
+
+echo "==> Syncing project files to ${NAS_HOST}:${NAS_PATH}"
+COPYFILE_DISABLE=1 tar -cz \
+ --exclude='.git' \
+ --exclude='.claude' \
+ --exclude='config' \
+ --exclude='.env' \
+ . | ssh "$NAS_HOST" "tar -xzf - -C ${NAS_PATH}"
+
+echo "==> Decrypting secrets and writing remote .env"
+sops --input-type dotenv --output-type dotenv -d .env.enc \
+ | ssh "$NAS_HOST" "cat > ${NAS_PATH}/.env && chmod 600 ${NAS_PATH}/.env"
+
+echo "==> Pulling latest images and recreating containers"
+ssh "$NAS_HOST" "cd ${NAS_PATH} && docker compose pull && docker compose up -d"
+
+echo "==> Deploy complete."
diff --git a/docker-compose.yml b/docker-compose.yml
index f93b205..6277bdd 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -1,6 +1,6 @@
services:
gluetun:
- image: qmcgaw/gluetun:v3.40.0
+ image: qmcgaw/gluetun:latest
container_name: gluetun
cap_add:
- NET_ADMIN
@@ -49,6 +49,8 @@ services:
- ${TV_PATH}:/shows
devices:
- /dev/dri:/dev/dri
+ ports:
+ - "${LAN_IP}:8096:8096"
networks:
- media
restart: unless-stopped
@@ -123,6 +125,8 @@ services:
- ./caddy/Caddyfile:/etc/caddy/Caddyfile:ro
- caddy_data:/data
- caddy_config:/config
+ extra_hosts:
+ - "host.docker.internal:host-gateway"
networks:
- media
depends_on: