diff options
| -rw-r--r-- | .env.enc | 8 | ||||
| -rw-r--r-- | README.md | 6 | ||||
| -rw-r--r-- | caddy/Caddyfile | 7 | ||||
| -rw-r--r-- | docker-compose.yml | 80 |
4 files changed, 96 insertions, 5 deletions
@@ -5,7 +5,7 @@ TZ=ENC[AES256_GCM,data:fVGxWW9EwgEEsvoINN91tw==,iv:07SQemVbCe7lBDP93ksoYTgiipZsu #ENC[AES256_GCM,data:DHskclKhT/ruYtSSw3sd5ype62a/,iv:qxyrn5tSrXqvvdel5NRJZYFPVnYLznYnTEzRiJ7WEXw=,tag:Ip53lQRy6pcUa3rPyCbIeQ==,type:comment] #ENC[AES256_GCM,data:gwa8g32Juth7AbOkgFbMbDS5RiZlyv127ctgI5IytLN5egIa,iv:UacZcnKuF0zQfquqKsBd8tckKP7t3jbL4MIEYRIesJI=,tag:3Yw6LmEii5V3EqTy/RgQHQ==,type:comment] TS_IP=ENC[AES256_GCM,data:7yTEnV0qUlot4hFOtyCL,iv:B9D3copRDvuecEb+TV5V6c77JDhlSZ+Fhlw5ZGgkQbs=,tag:zN8lEQ31/Zxzu9IGss4i4g==,type:str] -DOMAIN=ENC[AES256_GCM,data:EmH4SUfcGewX4g==,iv:JZukqc4yI3Gf7Tb/AukB1oFjXJ0CzPLzNplCOEGxAT4=,tag:8nACxPumMsQi8QJxIlOQgA==,type:str] +DOMAIN=ENC[AES256_GCM,data:QdxXEsBlPWsUfO0fMjc=,iv:87CFbrDCXuZNumWq/udGVU9seiHVKLDnW2A9vuWVRkE=,tag:KiS9Of3W3SjGpKoobDIGlA==,type:str] #ENC[AES256_GCM,data:uyTjemwDVhTCMvM7ta3mIQIzFHfSj6I=,iv:mkngrcKbRbZvfF2lHq1nLGdhF5o0VIamDqbvTq1BgOY=,tag:fVibo8oQu3YGDWJOsOEEZg==,type:comment] #ENC[AES256_GCM,data:mgmFC0i6d5yHMYAJkKp/DqnYZlaS1Ps+f3WUpb4xUkquaIxeK85N+aROMRL6Gw1vJlFSckq24jsLO5rDpu4+KJER66qYv/s=,iv:OtJf7PFUrkYL+WfK8pYaaLT/OoarWMYuS27tw45Epqw=,tag:I3gobI8p24IVAw6WWHGQ2w==,type:comment] #ENC[AES256_GCM,data:WrhB5Hm7iJkYEab72zzcCIIpbIs/9byPMc+QyPUWPPx7085deubT8WfOt5nbexIi9VuMssm30ULX,iv:hFY7ONM0F/B2t9pBpco8P7IgrC2lcve5/Nl6+856FKA=,tag:bwP0u15xmOpZb9i9D5oO7A==,type:comment] @@ -31,7 +31,7 @@ BAZARR_API_KEY=ENC[AES256_GCM,data:gr1guUQ17CUxuwPzDGtXegFPVA4CgIdJvWXAdxXf8nE=, 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-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_lastmodified=2026-08-30T20:35:49Z +sops_mac=ENC[AES256_GCM,data:92Jy7rI/K0C0z6izo0vSAQYFO8dvHwGLdJ0uLCLkopMxu7wY0mtQBGuVEH+hSQiRnjzqDvC3qjK1aMiArysMmYd5bCargCAquYD+AsmHqH+0efT7qY4S+yvS+dZNFYb4oOIzXMq6u4fOSRaW4r/yMZUxwoRspyaM53cb0mYkS+8=,iv:FLsdwNUooqN6jOR32pzwjxQlgN/DTeQFSaS/9LqCAqw=,tag:apzKXT1jQVNjH1zx+PEt5g==,type:str] sops_unencrypted_suffix=_unencrypted -sops_version=3.13.2 +sops_version=3.13.3 @@ -20,6 +20,12 @@ All secrets and per-deployment config (VPN credentials, Gandi DNS token, LAN/Tai 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. +## Monitoring + +Every service has a real `HEALTHCHECK` (not just "is the process alive" — e.g. qbittorrent's also curls an external IP through gluetun's tunnel, since its WebUI keeps responding even when the VPN is dead). `autoheal` watches Docker's health status and runs `docker restart` on anything reporting `unhealthy`. `uptime-kuma` (at `status.karanj.com`) is the dashboard — monitors aren't config-as-code, so after a fresh deploy log in and add a "Docker Container" monitor per service plus an HTTP monitor per public hostname. + +**Caveat:** `qbittorrent` shares `gluetun`'s network namespace (`network_mode: "service:gluetun"`), and autoheal restarts containers individually — it has no notion of "restart this one's dependents too." Both carry equivalent connectivity healthchecks so they should flip `unhealthy` and get restarted within a cycle or two of each other, but it's not perfectly atomic. If qbittorrent ever looks stuck after a gluetun restart, `docker compose restart gluetun qbittorrent` (together) is the manual fallback — that's what actually fixed the Aug 2026 VPN outage. + ## 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. diff --git a/caddy/Caddyfile b/caddy/Caddyfile index 4f38033..4fe36c5 100644 --- a/caddy/Caddyfile +++ b/caddy/Caddyfile @@ -39,7 +39,12 @@ dl.{$DOMAIN} { reverse_proxy gluetun:8123 } -nas.{$DOMAIN} { +status.{$DOMAIN} { + import tls_gandi + reverse_proxy uptime-kuma:3001 +} + +nas.karanj.com { import tls_gandi reverse_proxy https://host.docker.internal:9443 { transport http { diff --git a/docker-compose.yml b/docker-compose.yml index 6277bdd..8fd2d8d 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -18,6 +18,8 @@ services: - ./config/gluetun:/gluetun networks: - media + labels: + - "autoheal=true" restart: unless-stopped qbittorrent: @@ -34,6 +36,14 @@ services: volumes: - /volume2/docker/linuxserver_qbittorrent-1/config:/config - ${DOWNLOADS_PATH}:/downloads + labels: + - "autoheal=true" + healthcheck: + test: ["CMD-SHELL", "curl -fsS --max-time 5 http://localhost:8123/ >/dev/null && curl -fsS --max-time 5 https://1.1.1.1 >/dev/null"] + interval: 30s + timeout: 10s + retries: 3 + start_period: 30s restart: unless-stopped jellyfin: @@ -53,6 +63,14 @@ services: - "${LAN_IP}:8096:8096" networks: - media + labels: + - "autoheal=true" + healthcheck: + test: ["CMD-SHELL", "curl -fsS http://localhost:8096/health"] + interval: 30s + timeout: 10s + retries: 3 + start_period: 30s restart: unless-stopped radarr: @@ -68,6 +86,14 @@ services: - ${DOWNLOADS_PATH}:/downloads networks: - media + labels: + - "autoheal=true" + healthcheck: + test: ["CMD-SHELL", "curl -fsS http://localhost:7878/ping"] + interval: 30s + timeout: 10s + retries: 3 + start_period: 30s restart: unless-stopped sonarr: @@ -83,6 +109,14 @@ services: - ${DOWNLOADS_PATH}:/downloads networks: - media + labels: + - "autoheal=true" + healthcheck: + test: ["CMD-SHELL", "curl -fsS http://localhost:8989/ping"] + interval: 30s + timeout: 10s + retries: 3 + start_period: 30s restart: unless-stopped prowlarr: @@ -96,6 +130,14 @@ services: - /volume2/docker/prowlarr:/config networks: - media + labels: + - "autoheal=true" + healthcheck: + test: ["CMD-SHELL", "curl -fsS http://localhost:9696/ping"] + interval: 30s + timeout: 10s + retries: 3 + start_period: 30s restart: unless-stopped bazarr: @@ -111,6 +153,14 @@ services: - ${TV_PATH}:/tv networks: - media + labels: + - "autoheal=true" + healthcheck: + test: ["CMD-SHELL", "curl -fsS http://localhost:6767/"] + interval: 30s + timeout: 10s + retries: 3 + start_period: 30s restart: unless-stopped caddy: @@ -136,6 +186,35 @@ services: - prowlarr - bazarr - gluetun + labels: + - "autoheal=true" + healthcheck: + test: ["CMD-SHELL", "curl -fsS http://localhost:2019/config/"] + interval: 30s + timeout: 10s + retries: 3 + start_period: 30s + restart: unless-stopped + + autoheal: + image: willfarrell/autoheal:latest + container_name: autoheal + environment: + - AUTOHEAL_CONTAINER_LABEL=autoheal + - AUTOHEAL_INTERVAL=10 + - TZ=${TZ} + volumes: + - /var/run/docker.sock:/var/run/docker.sock + restart: unless-stopped + + uptime-kuma: + image: louislam/uptime-kuma:1 + container_name: uptime-kuma + volumes: + - uptime_kuma_data:/app/data + - /var/run/docker.sock:/var/run/docker.sock:ro + networks: + - media restart: unless-stopped networks: @@ -145,3 +224,4 @@ networks: volumes: caddy_data: caddy_config: + uptime_kuma_data: |
