aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.claude/settings.local.json7
-rw-r--r--.env.enc31
-rw-r--r--.gitignore1
-rw-r--r--.sops.yaml3
-rw-r--r--README.md12
-rw-r--r--caddy/Caddyfile40
-rw-r--r--caddy/Dockerfile8
-rw-r--r--docker-compose.yml143
8 files changed, 245 insertions, 0 deletions
diff --git a/.claude/settings.local.json b/.claude/settings.local.json
new file mode 100644
index 0000000..15b1e4b
--- /dev/null
+++ b/.claude/settings.local.json
@@ -0,0 +1,7 @@
+{
+ "permissions": {
+ "allow": [
+ "Bash(docker compose *)"
+ ]
+ }
+}
diff --git a/.env.enc b/.env.enc
new file mode 100644
index 0000000..a73ee65
--- /dev/null
+++ b/.env.enc
@@ -0,0 +1,31 @@
+#ENC[AES256_GCM,data:9AZe+JCrAJgApE+oQlBIacT8Zs7Qu2WQ,iv:3VEh1f3XXQtf79jdmItPcKt/skt7U8HpMZhWO4Y8Blg=,tag:69XcDdKug3qpK1nHzaS8cQ==,type:comment]
+PUID=ENC[AES256_GCM,data:prO7Hw==,iv:XqNA9vtb+ebZbU/6jpDvsOnuMEs1vf3T5e0DcgzBDAU=,tag:TtTzb1ladTDtE5nzytbzkQ==,type:str]
+PGID=ENC[AES256_GCM,data:xVM=,iv:LiQG+qpjyyJMS0Y/foBpdZ2dPeVDlDx1Ziy6Zolq8Vc=,tag:7RhZTWE+4YPeyogwus7mkg==,type:str]
+TZ=ENC[AES256_GCM,data:5BolCAxAFUYd5RagmjeE5Q==,iv:5hofxcioR0kG0rA+PIXFKdqs0r/lTaycDq/kL1DqHhs=,tag:06P8+pF4XqfsN5SaduEDKw==,type:str]
+#ENC[AES256_GCM,data:heKOmgUqCwdkGT191OGv/LXuc5Hf,iv:p69H+q/FTGbYYoK5nQC1KRofOJP8LQA1Ppq+nhlu2pw=,tag:oKNmxsp/y9Y3alhzdXSCvA==,type:comment]
+#ENC[AES256_GCM,data:1khiCdJvFziwoQyyO39sW4GQFTMHWSOH2f1EeXs0uMVNG+iw,iv:45Q7dGfpK8j8FEU+EvZURBpHBCYx6N0jP7R4gLpYVqA=,tag:j/vSTb4AA2MIKsVJoMmhnQ==,type:comment]
+TS_IP=ENC[AES256_GCM,data:F/GmBGZkcR2INUKUbc0w,iv:zFjweg6n6iwGSopp9bX2LrV6ZPIpCKaMNSW8pKOnILY=,tag:hwZjPVdCpDfMJcY2O7ncjQ==,type:str]
+DOMAIN=ENC[AES256_GCM,data:ktPl91SzSbYRyA==,iv:ikOGrPitikBLxISgh+s3RDIaNUkEC5FFTBfZ4LIG3Zk=,tag:/WkRSgOLY0oBbmNaTa5AJg==,type:str]
+#ENC[AES256_GCM,data:kxB6douZV+eb1roeg5GqLE9nitG+jfI=,iv:ti6sniUI1WyntrMhucbq9OP6Iy1fcnh/zQegwMPU4I8=,tag:1TMYsRX5ri2lBW/PcPNmiQ==,type:comment]
+#ENC[AES256_GCM,data:MGoxzGN8OekHRRh7m8sBmLsewc4rRyzrcESKct2gU91JP/+5/7Tt206jce12CBTkJmAUoxfRJpkyzI9mye+uerk6mZfu1h0=,iv:lyeoQgYY6QvGfO5P6KL2S8oKYtLxZlACsrVBdA2eG6E=,tag:8J9MvJvcowkzHM4zQ9A1TQ==,type:comment]
+#ENC[AES256_GCM,data:XUfVYYSqnQGQTvqU+uu6oBMPcURbd0PQBC8synZRS8pPIeXdneviZAMFxoCiJzc06gzgEsWi3MRK,iv:RIA1bNPIDhrZp0wJTUyQQog96fV6a5qSumTRg54WQgc=,tag:xb+Ssg7MTvn1v6ILVf10Rg==,type:comment]
+GANDI_BEARER_TOKEN=ENC[AES256_GCM,data:uacfd7uOcOehUZuMqno8YoITogu0pXGubCy8B+R3Uvy1ohqF/cjG0Q==,iv:ZDV2D1COWekXzzK+kkFLYjiMUvDR2Yrrb9RTRgSMmaE=,tag:JmIyFvD4EsW2WDAPrvg7lA==,type:str]
+#ENC[AES256_GCM,data:aRd0GgWFF+u6dEcCJO7Unjh1w7zbFt/4I2Uyd6JfFtUov3ou/GHNIeBuGMtDhw==,iv:5gY9yVtXiTYxvQpqVLEIkhsyVqbxHtXX818+R0+T/r4=,tag:eRkai1Nl3JzR0rl4YpJ/mA==,type:comment]
+MOVIES_PATH=ENC[AES256_GCM,data:Uk0kKuYDgVsRaPIlFKU3,iv:Ozq7frOcW2K3xl3QxDBOSpNDnpWdpSAfkH//kvv0g3k=,tag:JLeBTOL9kzNKjIEs5CmkkQ==,type:str]
+TV_PATH=ENC[AES256_GCM,data:6ODQmkjvgYQ8rwj/PaN7oho=,iv:ZdHb76tms5L75Lzku/oKVGQWdYUw98hGKzUbbNUGM9M=,tag:wHbQEsY9BYol0VEaMxE84Q==,type:str]
+DOWNLOADS_PATH=ENC[AES256_GCM,data:RSW8rtnra1rSeACy38InFaMV,iv:8BNz3LT5QzFi/Fjuj6olXDYbvOCF8Qe+i8BoPPw6WP0=,tag:u3fDDeasEKstaDkOkOzfBg==,type:str]
+#ENC[AES256_GCM,data:HRM0VGIYHFOTAcNt7G8hmVHMlBmIpS9PDgtsnciN,iv:T0D0dgiOZdfLJkdpPuzn2xboN390AvDe9m0fjEF82gY=,tag:WCxQ13sYfPmug6stmcLqPQ==,type:comment]
+VPN_SERVICE_PROVIDER=ENC[AES256_GCM,data:1LF8+ABS3g==,iv:h8pTJ9xWNFWPdyoolLL5Xb92wOMvPs+8Q9lKzi5gzeg=,tag:3xIDm/ishURdOTnGRSnc9A==,type:str]
+VPN_TYPE=ENC[AES256_GCM,data:BqFIBDTkIBsI,iv:l6wJL0mpTjw48scMLzOQeO9cp0jqYRACpGCcPuGXLqE=,tag:6omGgVBp6Myp6mJiowv9lA==,type:str]
+#ENC[AES256_GCM,data:4JuH3JrcTRlG/bG2iKpjedIYmvfBSDrdQofFwAv3OZl1wHvVX2mD9hgx/jEJn86RrSyYimEw6ADs2XCW2uHQxlY=,iv:MS6ysSwuaTm+6XeaFHTG26hUfyZ9I/dMdnq3NAxnkuo=,tag:UlMwM4LSubZwzzDFpf9xRg==,type:comment]
+WIREGUARD_PRIVATE_KEY=ENC[AES256_GCM,data:pAf2FED7DbNUm4XkXSeZg9Z7nYO78tjlfCSIxLaNS/6YVCwFotPrah4Y8Po=,iv:PsUNp1jaIXnGPJO+B0qtYxjTh34TJ3h+BnihH5DRRaM=,tag:/n/7Mp0RbXgERpMrd0o1NA==,type:str]
+#ENC[AES256_GCM,data:r4xdkP2UmmlMI+hhtLJErb+aChgZfHHFK9Z60ABtCXGCPjmXLG3ovd33Qj7xmUXbpZ6uf6n3dR2LNcE=,iv:xVuVJuplA5YDjKfKxQXAN1Cirr/sYp63Sry+EY3PLwE=,tag:ZzhP2QYtyhqDJkTqO2C6kA==,type:comment]
+WIREGUARD_ADDRESSES=ENC[AES256_GCM,data:gTBapWrxKpRdqo+KW7A=,iv:rvuHbtQSAkfIpoO2ReP4QQYWN0czvnAzVdxbkK2Up0M=,tag:GuLAiOL5f928FDPeTXtq3Q==,type:str]
+#ENC[AES256_GCM,data:rLczdt3MHicSSNrTF0OsrD3vWaB7i9Z+KJ8fpQ7V,iv:KG6jD3UMp5ExzMqftpu3JJf9+pQVYL3P+V4+gLa2hLA=,tag:Quw6Bwp9TGUbiFF5SazWpQ==,type:comment]
+SERVER_CITIES=ENC[AES256_GCM,data:rkQUnIyNpAFH,iv:hUX4zjtufG5PsXEb793CbLzoPQdCGScQvxvOVwCEd1Q=,tag:opoc99h2s/wj90iPzX8+Nw==,type:str]
+sops_age__list_0__map_enc=-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBRNFZ1WWJMZ0ZZbFdvZkp1\ndlp6SkQ4c3IyMjJtenZWNHZteFowRkJ3SW1rCmkvbmVuMGJPZU9ZMk50Q0pYdzdB\neDhnVU90TW5RbWdEVXR1L0hvczZiVU0KLS0tIEhaUjJ6Ty9wZWNDKzMxVnh6Y2J3\ndktwekt6NnRZdEJTbFhEOHNaTW5MM2sK1uE4qdx9jRwnupHLtTyNgGf0JZ6NveJ+\n44Q2fzqO7upn2FIqhYfbj1KG3Nku7gSH5xDb16X7gyMxfqy5bDR/cQ==\n-----END AGE ENCRYPTED FILE-----\n
+sops_age__list_0__map_recipient=age1sv5yc279d8j66fx36awxhdnsqjp3pv8x9yc0ak6xa9qy9egg6e8qegccg4
+sops_lastmodified=2026-07-19T18:54:05Z
+sops_mac=ENC[AES256_GCM,data:h/X6+jefhCqe+Unc0CWVEmi27E55eleu+PY9T+vP7zIkpkybGK3el3foRBuJA15tDKaj7bZSwKo1a+mpMxi+bWsvrTEjNY3kuMeefLJzIfKSmfP8NVsn4cYJ5Iy8Rt+cfKXfD9JMu33rJ2rvnGD6I3p1JQAikOfzPeNo8urK/5E=,iv:5D3MmNTrqLaNzaQEw6mSNR51wNfdFHiXGK03XVImXI8=,tag:3S1tHW/VveAWdAkefztrMA==,type:str]
+sops_unencrypted_suffix=_unencrypted
+sops_version=3.13.2
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..4c49bd7
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1 @@
+.env
diff --git a/.sops.yaml b/.sops.yaml
new file mode 100644
index 0000000..bb2fede
--- /dev/null
+++ b/.sops.yaml
@@ -0,0 +1,3 @@
+creation_rules:
+ - path_regex: \.env$
+ age: age1sv5yc279d8j66fx36awxhdnsqjp3pv8x9yc0ak6xa9qy9egg6e8qegccg4
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..cc24110
--- /dev/null
+++ b/README.md
@@ -0,0 +1,12 @@
+# goodall
+
+Docker Compose media stack (Jellyfin, Radarr, Sonarr, Prowlarr, Bazarr, qBittorrent) fronted by Caddy, reachable only over Tailscale.
+
+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:
+
+```
+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"
+```
+
+See [MIGRATION_STATUS.md](MIGRATION_STATUS.md) for current deployment status and remaining work.
diff --git a/caddy/Caddyfile b/caddy/Caddyfile
new file mode 100644
index 0000000..9d941d0
--- /dev/null
+++ b/caddy/Caddyfile
@@ -0,0 +1,40 @@
+{
+ email me@karanj.com
+}
+
+(tls_gandi) {
+ tls {
+ dns gandi {env.GANDI_BEARER_TOKEN}
+ propagation_timeout -1
+ }
+}
+
+jellyfin.{$DOMAIN} {
+ import tls_gandi
+ reverse_proxy jellyfin:8096
+}
+
+movies.{$DOMAIN} {
+ import tls_gandi
+ reverse_proxy radarr:7878
+}
+
+shows.{$DOMAIN} {
+ import tls_gandi
+ reverse_proxy sonarr:8989
+}
+
+index.{$DOMAIN} {
+ import tls_gandi
+ reverse_proxy prowlarr:9696
+}
+
+subs.{$DOMAIN} {
+ import tls_gandi
+ reverse_proxy bazarr:6767
+}
+
+dl.{$DOMAIN} {
+ import tls_gandi
+ reverse_proxy gluetun:8123
+}
diff --git a/caddy/Dockerfile b/caddy/Dockerfile
new file mode 100644
index 0000000..f1b6d2c
--- /dev/null
+++ b/caddy/Dockerfile
@@ -0,0 +1,8 @@
+FROM caddy:2-builder AS builder
+
+RUN xcaddy build \
+ --with github.com/caddy-dns/gandi
+
+FROM caddy:2
+
+COPY --from=builder /usr/bin/caddy /usr/bin/caddy
diff --git a/docker-compose.yml b/docker-compose.yml
new file mode 100644
index 0000000..f93b205
--- /dev/null
+++ b/docker-compose.yml
@@ -0,0 +1,143 @@
+services:
+ gluetun:
+ image: qmcgaw/gluetun:v3.40.0
+ container_name: gluetun
+ cap_add:
+ - NET_ADMIN
+ devices:
+ - /dev/net/tun:/dev/net/tun
+ environment:
+ - VPN_SERVICE_PROVIDER=${VPN_SERVICE_PROVIDER}
+ - VPN_TYPE=${VPN_TYPE}
+ - WIREGUARD_PRIVATE_KEY=${WIREGUARD_PRIVATE_KEY}
+ - WIREGUARD_ADDRESSES=${WIREGUARD_ADDRESSES}
+ - SERVER_CITIES=${SERVER_CITIES}
+ - FIREWALL_INPUT_PORTS=8123
+ - TZ=${TZ}
+ volumes:
+ - ./config/gluetun:/gluetun
+ networks:
+ - media
+ restart: unless-stopped
+
+ qbittorrent:
+ image: lscr.io/linuxserver/qbittorrent:latest
+ container_name: qbittorrent
+ network_mode: "service:gluetun"
+ depends_on:
+ - gluetun
+ environment:
+ - PUID=${PUID}
+ - PGID=${PGID}
+ - TZ=${TZ}
+ - WEBUI_PORT=8123
+ volumes:
+ - /volume2/docker/linuxserver_qbittorrent-1/config:/config
+ - ${DOWNLOADS_PATH}:/downloads
+ restart: unless-stopped
+
+ jellyfin:
+ image: lscr.io/linuxserver/jellyfin:latest
+ container_name: jellyfin
+ environment:
+ - PUID=${PUID}
+ - PGID=${PGID}
+ - TZ=${TZ}
+ volumes:
+ - /volume2/docker/jellyfin/config:/config
+ - ${MOVIES_PATH}:/movies
+ - ${TV_PATH}:/shows
+ devices:
+ - /dev/dri:/dev/dri
+ networks:
+ - media
+ restart: unless-stopped
+
+ radarr:
+ image: lscr.io/linuxserver/radarr:latest
+ container_name: radarr
+ environment:
+ - PUID=${PUID}
+ - PGID=${PGID}
+ - TZ=${TZ}
+ volumes:
+ - /volume2/docker/radarr:/config
+ - ${MOVIES_PATH}:/movies
+ - ${DOWNLOADS_PATH}:/downloads
+ networks:
+ - media
+ restart: unless-stopped
+
+ sonarr:
+ image: lscr.io/linuxserver/sonarr:latest
+ container_name: sonarr
+ environment:
+ - PUID=${PUID}
+ - PGID=${PGID}
+ - TZ=${TZ}
+ volumes:
+ - /volume2/docker/sonarr:/config
+ - ${TV_PATH}:/tv
+ - ${DOWNLOADS_PATH}:/downloads
+ networks:
+ - media
+ restart: unless-stopped
+
+ prowlarr:
+ image: lscr.io/linuxserver/prowlarr:latest
+ container_name: prowlarr
+ environment:
+ - PUID=${PUID}
+ - PGID=${PGID}
+ - TZ=${TZ}
+ volumes:
+ - /volume2/docker/prowlarr:/config
+ networks:
+ - media
+ restart: unless-stopped
+
+ bazarr:
+ image: lscr.io/linuxserver/bazarr:latest
+ container_name: bazarr
+ environment:
+ - PUID=${PUID}
+ - PGID=${PGID}
+ - TZ=${TZ}
+ volumes:
+ - /volume2/docker/bazarr:/config
+ - ${MOVIES_PATH}:/movies
+ - ${TV_PATH}:/tv
+ networks:
+ - media
+ restart: unless-stopped
+
+ caddy:
+ build: ./caddy
+ container_name: caddy
+ ports:
+ - "${TS_IP}:443:443"
+ environment:
+ - GANDI_BEARER_TOKEN=${GANDI_BEARER_TOKEN}
+ - DOMAIN=${DOMAIN}
+ volumes:
+ - ./caddy/Caddyfile:/etc/caddy/Caddyfile:ro
+ - caddy_data:/data
+ - caddy_config:/config
+ networks:
+ - media
+ depends_on:
+ - jellyfin
+ - radarr
+ - sonarr
+ - prowlarr
+ - bazarr
+ - gluetun
+ restart: unless-stopped
+
+networks:
+ media:
+ driver: bridge
+
+volumes:
+ caddy_data:
+ caddy_config: