From c948d5cbde532d1b668ff0061ccc95a95646104e Mon Sep 17 00:00:00 2001 From: samjage Date: Mon, 6 Apr 2026 10:13:21 -0400 Subject: [PATCH] added stacks for backup --- stacks/00-header | 150 ++++++++++++++++++++ stacks/backbone/caddy/Caddyfile | 61 +++++++++ stacks/backbone/docker-compose.yml | 47 +++++++ stacks/caddy/caddy/Caddyfile | 83 ++++++++++++ stacks/caddy/docker-compose.yml | 30 ++++ stacks/codeserver/docker-compose.yml | 26 ++++ stacks/docker-socket-proxy/compose.yaml | 24 ++++ stacks/dockge/docker-compose.yml | 20 +++ stacks/gitea/docker-compose.yml | 20 +++ stacks/home-automation/docker-compose.yml | 62 +++++++++ stacks/homebridge/docker-compose.yml | 16 +++ stacks/matrix | 1 + stacks/mealie/docker-compose.yml | 23 ++++ stacks/pi-hole/docker-compose.yml | 29 ++++ stacks/scripts/do-update.sh | 158 ++++++++++++++++++++++ stacks/scripts/image-sources.conf | 39 ++++++ stacks/scripts/update-check.sh | 114 ++++++++++++++++ stacks/scrypted/docker-compose.yml | 31 +++++ stacks/todo.md | 0 stacks/watchtower/docker-compose.yml | 20 +++ stacks/wireguard/docker-compose.yml | 29 ++++ stacks/wyze-bridge/docker-compose.yml | 15 ++ 22 files changed, 998 insertions(+) create mode 100644 stacks/00-header create mode 100755 stacks/backbone/caddy/Caddyfile create mode 100755 stacks/backbone/docker-compose.yml create mode 100755 stacks/caddy/caddy/Caddyfile create mode 100755 stacks/caddy/docker-compose.yml create mode 100755 stacks/codeserver/docker-compose.yml create mode 100644 stacks/docker-socket-proxy/compose.yaml create mode 100644 stacks/dockge/docker-compose.yml create mode 100755 stacks/gitea/docker-compose.yml create mode 100755 stacks/home-automation/docker-compose.yml create mode 100755 stacks/homebridge/docker-compose.yml create mode 160000 stacks/matrix create mode 100755 stacks/mealie/docker-compose.yml create mode 100755 stacks/pi-hole/docker-compose.yml create mode 100644 stacks/scripts/do-update.sh create mode 100644 stacks/scripts/image-sources.conf create mode 100644 stacks/scripts/update-check.sh create mode 100755 stacks/scrypted/docker-compose.yml create mode 100644 stacks/todo.md create mode 100644 stacks/watchtower/docker-compose.yml create mode 100755 stacks/wireguard/docker-compose.yml create mode 100755 stacks/wyze-bridge/docker-compose.yml diff --git a/stacks/00-header b/stacks/00-header new file mode 100644 index 0000000..c71412d --- /dev/null +++ b/stacks/00-header @@ -0,0 +1,150 @@ +#!/bin/bash + +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +BLUE='\033[0;34m' +PURPLE='\033[0;35m' +CYAN='\033[0;36m' +GRAY='\033[0;90m' +BOLD='\033[1m' +NC='\033[0m' + +COLS=${COLUMNS:-$(tput cols 2>/dev/null)} +[[ "$COLS" =~ ^[0-9]+$ ]] || COLS=80 +SEP="${GRAY}$(printf '━%.0s' $(seq 1 $COLS))${NC}" + +# ── Logo ───────────────────────────────────────────────────────────────────── +echo -e "${CYAN}${BOLD}" +echo '██ ██ ███ ██ ██ ██' +echo '██ ██ ████ ██ ██ ██ ' +echo '██ ██ ██ ██ ██ █████ ' +echo '██ ██ ██ ██ ██ ██ ██ ' +echo '███████ ██ ██ ████ ██ ██' +echo -e "${NC}" +echo -e "$SEP" + +# ── Date & Time ─────────────────────────────────────────────────────────────── +echo -e " ${PURPLE}${BOLD}$(date '+%A, %B %d %Y')${NC} ${GRAY}·${NC} ${PURPLE}$(date '+%I:%M %p')${NC}" +echo -e "$SEP" + +# ── Weather ─────────────────────────────────────────────────────────────────── +WEATHER=$(curl -s --max-time 4 "https://api.open-meteo.com/v1/forecast?latitude=39.84&longitude=-82.81¤t=temperature_2m,apparent_temperature,relative_humidity_2m,wind_speed_10m,weather_code&temperature_unit=fahrenheit&timezone=America/New_York" 2>/dev/null) +echo -e " ${YELLOW}${BOLD}☁ Canal Winchester${NC}" +if echo "$WEATHER" | python3 -c "import sys,json; json.load(sys.stdin)" &>/dev/null; then + TEMP=$(echo "$WEATHER" | python3 -c "import sys,json; d=json.load(sys.stdin)['current']; print(round(d['temperature_2m']))") + FEELS=$(echo "$WEATHER" | python3 -c "import sys,json; d=json.load(sys.stdin)['current']; print(round(d['apparent_temperature']))") + HUMID=$(echo "$WEATHER" | python3 -c "import sys,json; d=json.load(sys.stdin)['current']; print(round(d['relative_humidity_2m']))") + WIND=$(echo "$WEATHER" | python3 -c "import sys,json; d=json.load(sys.stdin)['current']; print(round(d['wind_speed_10m']))") + CODE=$(echo "$WEATHER" | python3 -c "import sys,json; d=json.load(sys.stdin)['current']; print(d['weather_code'])") + case $CODE in + 0) ICON="☀️ Clear";; + 1|2) ICON="⛅ Partly Cloudy";; + 3) ICON="☁️ Overcast";; + 45|48) ICON="🌫️ Fog";; + 51|53|55|61|63|65|80|81|82) ICON="🌧️ Rain";; + 71|73|75|85|86) ICON="❄️ Snow";; + 95|96|99) ICON="⛈️ Storms";; + *) ICON="🌥️ Cloudy";; + esac + echo -e " ${ICON} ${BOLD}${TEMP}°F${NC} ${GRAY}feels ${FEELS}°F · 💧${HUMID}% · 💨 ${WIND} km/h${NC}" +else + echo -e " ${GRAY}weather unavailable${NC}" +fi +echo -e "$SEP" + +# ── Docker ──────────────────────────────────────────────────────────────────── +echo -e " ${GREEN}${BOLD}🐳 Docker${NC}" +if command -v docker &> /dev/null; then + RUNNING=$(docker ps -q 2>/dev/null | wc -l) + TOTAL=$(docker ps -a -q 2>/dev/null | wc -l) + DOWN=$((TOTAL - RUNNING)) + RESTART=$(docker ps --filter "status=restarting" -q 2>/dev/null | wc -l) + echo "" + echo -e " ${GREEN}●${NC} ${RUNNING} running ${GRAY}·${NC} ${RED}●${NC} ${DOWN} stopped ${GRAY}·${NC} 🔄 ${RESTART} restarting ${GRAY}·${NC} 📦 ${TOTAL} total" + echo "" + + # Outdated images — parsed live from Watchtower logs + echo -e " ${CYAN}image updates${NC}" + if docker inspect watchtower &>/dev/null 2>&1; then + LAST_SCAN=$(docker logs watchtower --since 72h 2>&1 | grep "Session done" | tail -1 | grep -oP '\d{4}-\d{2}-\d{2}T\d{2}:\d{2}' || true) + OUTDATED_LIST=$(docker logs watchtower --since 36h 2>&1 | grep "Found new" | grep -oP '(?<=Found new )\S+(?= image)' || true) + if [ -n "$OUTDATED_LIST" ]; then + COUNT=$(echo "$OUTDATED_LIST" | wc -l) + echo -e " ${YELLOW}↑ ${COUNT} update(s) available${NC}" + while IFS= read -r img; do + CT=$(docker ps -a --format '{{.Names}}|{{.Image}}' | awk -F'|' -v i="$img" '$2==i {printf "%s ", $1}') + echo -e " ${GRAY} · ${img}${NC} ${CT}" + done <<< "$OUTDATED_LIST" + else + echo -e " ${GREEN}●${NC} all images current" + fi + [ -n "$LAST_SCAN" ] && echo -e " ${GRAY}last scan: ${LAST_SCAN}${NC}" + echo -e " ${GRAY}details: bash /opt/stacks/scripts/update-check.sh${NC}" + else + echo -e " ${GRAY}watchtower not running${NC}" + fi +else + echo -e " ${RED}docker not available${NC}" +fi +echo -e "$SEP" + +# ── System ──────────────────────────────────────────────────────────────────── +echo -e " ${BLUE}${BOLD}💻 System${NC}" +echo "" + +LOCAL_IP=$(hostname -I | awk '{print $1}') +EXT_IP=$(curl -s --max-time 3 ifconfig.me 2>/dev/null || echo "unavailable") + +CPU_CORES=$(nproc 2>/dev/null || echo 1) +CPU_PCT=$(top -bn1 | grep "Cpu(s)" | sed "s/.*, *\([0-9.]*\)%* id.*/\1/" | awk '{printf "%.0f", 100 - $1}') +CPU_FILLED=$((CPU_PCT * 10 / 100)) +CPU_EMPTY=$((10 - CPU_FILLED)) +CPU_ON="$(printf '█%.0s' $(seq 1 $CPU_FILLED) 2>/dev/null)" +CPU_OFF="$(printf '░%.0s' $(seq 1 $CPU_EMPTY) 2>/dev/null)" + +RAM_USED=$(free -m | awk 'NR==2 {printf "%.1f", $3/1024}') +RAM_TOTAL=$(free -m | awk 'NR==2 {printf "%.1f", $2/1024}') +RAM_PCT=$(free | awk 'NR==2 {printf "%.0f", $3/$2*100}') +RAM_FILLED=$((RAM_PCT * 10 / 100)) +RAM_EMPTY=$((10 - RAM_FILLED)) +BAR_ON="$(printf '█%.0s' $(seq 1 $RAM_FILLED) 2>/dev/null)" +BAR_OFF="$(printf '░%.0s' $(seq 1 $RAM_EMPTY) 2>/dev/null)" + +LOAD=$(awk '{print $1" "$2" "$3}' /proc/loadavg) +LOAD1=$(awk '{print $1}' /proc/loadavg) +LOAD_PCT=$(awk -v l="$LOAD1" -v c="$CPU_CORES" 'BEGIN {v=int((l/c)*100); if(v>100) v=100; print v}') +LOAD_FILLED=$((LOAD_PCT * 10 / 100)) +LOAD_EMPTY=$((10 - LOAD_FILLED)) +LOAD_ON="$(printf '█%.0s' $(seq 1 $LOAD_FILLED) 2>/dev/null)" +LOAD_OFF="$(printf '░%.0s' $(seq 1 $LOAD_EMPTY) 2>/dev/null)" + +TOP_NAME=$(ps aux --sort=-%cpu | awk 'NR==2 {split($11,a,"/"); print a[length(a)]}') +TOP_CPU=$(ps aux --sort=-%cpu | awk 'NR==2 {printf "%.1f%%", $3}') + +printf " ${GRAY}%-10s${NC} %s\n" "uptime" "$(uptime -p | sed 's/up //')" +printf " ${GRAY}%-10s${NC} %s\n" "local ip" "$LOCAL_IP" +printf " ${GRAY}%-10s${NC} %s\n" "ext ip" "$EXT_IP" +echo "" +printf " ${GRAY}%-10s${NC} ${GREEN}${CPU_ON}${GRAY}${CPU_OFF}${NC} ${CPU_PCT}%%\n" "cpu" +printf " ${GRAY}%-10s${NC} ${GREEN}${BAR_ON}${GRAY}${BAR_OFF}${NC} ${RAM_USED}/${RAM_TOTAL} GB ${RAM_PCT}%%\n" "ram" +printf " ${GRAY}%-10s${NC} ${GREEN}${LOAD_ON}${GRAY}${LOAD_OFF}${NC} ${LOAD} ${GRAY}/ ${CPU_CORES} cores${NC}\n" "load" +echo "" +printf " ${GRAY}%-10s${NC} ${YELLOW}%s${NC} ${GRAY}%s${NC}\n" "top proc" "$TOP_NAME" "$TOP_CPU" +printf " ${GRAY}%-10s${NC} %s\n" "users" "$(who | wc -l) logged in" +echo -e "$SEP" + +# ── Quote ───────────────────────────────────────────────────────────────────── +echo -e " ${PURPLE}${BOLD}✦ Quote${NC}" +echo "" +QDATA=$(curl -s --max-time 4 "https://zenquotes.io/api/today" 2>/dev/null) +if echo "$QDATA" | python3 -c "import sys,json; json.load(sys.stdin)" &>/dev/null; then + QUOTE=$(echo "$QDATA" | python3 -c "import sys,json; d=json.load(sys.stdin)[0]; print(d['q'])") + AUTHOR=$(echo "$QDATA" | python3 -c "import sys,json; d=json.load(sys.stdin)[0]; print(d['a'])") + echo -e " ${GRAY}\"${QUOTE}\"${NC}" | fold -s -w $((COLS - 4)) + echo -e " ${PURPLE}— ${AUTHOR}${NC}" +else + echo -e " ${GRAY}unavailable${NC}" +fi +echo -e "$SEP" +echo "" diff --git a/stacks/backbone/caddy/Caddyfile b/stacks/backbone/caddy/Caddyfile new file mode 100755 index 0000000..211fd02 --- /dev/null +++ b/stacks/backbone/caddy/Caddyfile @@ -0,0 +1,61 @@ +invokesmoke.dev { + header /.well-known/matrix/* Content-Type application/json + header /.well-known/matrix/* Access-Control-Allow-Origin * + respond /.well-known/matrix/server `{"m.server":"matrix.invokesmoke.dev:443"}` + respond /.well-known/matrix/client `{"m.homeserver":{"base_url":"https://matrix.invokesmoke.dev"}}` + handle { + root * /srv + file_server + } +} + +matrix.invokesmoke.dev { + handle /_version { + respond `{"sha":"{env.GIT_SHA}"}` + } + handle /_matrix/* { + reverse_proxy tuwunel:6167 + } + handle /_synapse/* { + reverse_proxy tuwunel:6167 + } + handle { + root * /srv + file_server + } +} + +matrix.invokesmoke.dev:8448 { + reverse_proxy tuwunel:6167 +} + +mealie.invokesmoke.dev { + reverse_proxy mealie:9000 +} + +dockge.invokesmoke.dev { + reverse_proxy dockge:5001 +} + +code.invokesmoke.dev { + reverse_proxy code-server:8443 +} + +pihole.invokesmoke.dev { + reverse_proxy pihole:80 +} + +gitea.invokesmoke.dev { + reverse_proxy gitea:3000 +} + +wetty.invokesmoke.dev { + reverse_proxy wetty:3000 { + header_up Upgrade {http.request.header.Upgrade} + header_up Connection {http.request.header.Connection} + } +} + +vpn.invokesmoke.dev { + reverse_proxy wireguard:51821 +} \ No newline at end of file diff --git a/stacks/backbone/docker-compose.yml b/stacks/backbone/docker-compose.yml new file mode 100755 index 0000000..47c6bd4 --- /dev/null +++ b/stacks/backbone/docker-compose.yml @@ -0,0 +1,47 @@ +services: + caddy: + container_name: caddy + image: caddy:2-alpine + restart: unless-stopped + ports: + - "80:80" + - "443:443" + - "8448:8448" + volumes: + - ./caddy/Caddyfile:/etc/caddy/Caddyfile:ro + - caddy-data:/data + - caddy-config:/config + - /opt/srv:/srv:ro + networks: + - backbone + + pihole: + container_name: pihole + image: pihole/pihole:latest + restart: unless-stopped + ports: + - "53:53/tcp" + - "53:53/udp" + - "8053:80/tcp" + environment: + - WEBPASSWORD=${PIHOLE_PASSWORD} + - ServerIP=192.168.1.40 + volumes: + - etc-pihole:/etc/pihole + - etc-dnsmasq.d:/etc/dnsmasq.d + networks: + - backbone + +volumes: + caddy-data: + external: true + name: backbone_caddy-data + caddy-config: + external: true + name: backbone_caddy-config + etc-pihole: + etc-dnsmasq.d: + +networks: + backbone: + name: backbone diff --git a/stacks/caddy/caddy/Caddyfile b/stacks/caddy/caddy/Caddyfile new file mode 100755 index 0000000..a0ee07e --- /dev/null +++ b/stacks/caddy/caddy/Caddyfile @@ -0,0 +1,83 @@ +invokesmoke.dev { + handle /.well-known/matrix/server { + header Content-Type application/json + header Access-Control-Allow-Origin * + respond `{"m.server":"matrix.invokesmoke.dev:443"}` + } + handle /.well-known/matrix/client { + header Content-Type application/json + header Access-Control-Allow-Origin * + respond `{"m.homeserver":{"base_url":"https://matrix.invokesmoke.dev"},"org.matrix.msc4143.rtc_foci":[{"type":"livekit","livekit_service_url":"https://livekit.invokesmoke.dev"}]}` + } + handle { + root * /srv + file_server + } +} + +matrix.invokesmoke.dev { + handle /_matrix/* { + reverse_proxy tuwunel:6167 { + header_up Upgrade {http.request.header.Upgrade} + header_up Connection {http.request.header.Connection} + } + } + handle /_synapse/* { + reverse_proxy tuwunel:6167 { + header_up Upgrade {http.request.header.Upgrade} + header_up Connection {http.request.header.Connection} + } + } + respond 404 +} + +matrix.invokesmoke.dev:8448 { + reverse_proxy tuwunel:6167 +} + +livekit.invokesmoke.dev { + handle /sfu/* { + reverse_proxy lk-jwt-service:8080 + } + handle /livekit/jwt* { + reverse_proxy lk-jwt-service:8080 + } + handle { + reverse_proxy host.docker.internal:7880 { + header_up Upgrade {http.request.header.Upgrade} + header_up Connection {http.request.header.Connection} + } + } +} + +mealie.invokesmoke.dev { + reverse_proxy mealie:9000 +} + +dockge.invokesmoke.dev { + reverse_proxy dockge:5001 +} + +code.invokesmoke.dev { + reverse_proxy code-server:8443 +} + +pihole.invokesmoke.dev { + reverse_proxy pihole:80 +} + +gitea.invokesmoke.dev { + reverse_proxy gitea:3000 +} + +wetty.invokesmoke.dev { + reverse_proxy wetty:3000 { + header_up Upgrade {http.request.header.Upgrade} + header_up Connection {http.request.header.Connection} + } +} + +vpn.invokesmoke.dev { + reverse_proxy wireguard:51821 +} + diff --git a/stacks/caddy/docker-compose.yml b/stacks/caddy/docker-compose.yml new file mode 100755 index 0000000..b5fcc34 --- /dev/null +++ b/stacks/caddy/docker-compose.yml @@ -0,0 +1,30 @@ +services: + caddy: + container_name: caddy + image: caddy:2-alpine + extra_hosts: + - "host.docker.internal:host-gateway" + restart: unless-stopped + ports: + - "80:80" + - "443:443" + - "8448:8448" + volumes: + - ./caddy/Caddyfile:/etc/caddy/Caddyfile:ro + - caddy-data:/data + - caddy-config:/config + - /opt/srv:/srv:ro + networks: + - backbone + +volumes: + caddy-data: + external: true + name: backbone_caddy-data + caddy-config: + external: true + name: backbone_caddy-config + +networks: + backbone: + external: true diff --git a/stacks/codeserver/docker-compose.yml b/stacks/codeserver/docker-compose.yml new file mode 100755 index 0000000..e994702 --- /dev/null +++ b/stacks/codeserver/docker-compose.yml @@ -0,0 +1,26 @@ +services: + code-server: + image: lscr.io/linuxserver/code-server:latest + container_name: code-server + hostname: code-server + environment: + - PUID=1000 + - PGID=998 + - TZ=America/New_York + - PASSWORD=${CODE_PASSWORD} + - SUDO_PASSWORD=${CODE_SUDO_PASSWORD} + - DOCKER_MODS=linuxserver/mods:universal-docker + volumes: + - /opt:/opt + - /home/sam:/home/sam + - /var/run/docker.sock:/var/run/docker.sock + - /opt/codeserver/config:/config + ports: + - "8443:8443" + restart: unless-stopped + networks: + - backbone + +networks: + backbone: + external: true diff --git a/stacks/docker-socket-proxy/compose.yaml b/stacks/docker-socket-proxy/compose.yaml new file mode 100644 index 0000000..12532a7 --- /dev/null +++ b/stacks/docker-socket-proxy/compose.yaml @@ -0,0 +1,24 @@ +services: + socket-proxy: + image: tecnativa/docker-socket-proxy + container_name: docker-socket + restart: unless-stopped + volumes: + - /var/run/docker.sock:/var/run/docker.sock:ro + ports: + - 2375:2375 + environment: + CONTAINERS: 1 + IMAGES: 1 + INFO: 1 + NETWORKS: 1 + VOLUMES: 1 + SERVICES: 1 + TASKS: 1 + POST: 1 + ALLOW_START: 1 + ALLOW_STOP: 1 + ALLOW_RESTARTS: 1 +networks: + backbone: + external: true diff --git a/stacks/dockge/docker-compose.yml b/stacks/dockge/docker-compose.yml new file mode 100644 index 0000000..06a1864 --- /dev/null +++ b/stacks/dockge/docker-compose.yml @@ -0,0 +1,20 @@ +services: + dockge: + image: louislam/dockge:1 + container_name: dockge + restart: unless-stopped + volumes: + - /var/run/docker.sock:/var/run/docker.sock + - dockge-data:/app/data + - /opt/stacks:/opt/stacks + environment: + DOCKGE_STACKS_DIR: /opt/stacks + networks: + - backbone + +volumes: + dockge-data: + +networks: + backbone: + external: true diff --git a/stacks/gitea/docker-compose.yml b/stacks/gitea/docker-compose.yml new file mode 100755 index 0000000..57d11b3 --- /dev/null +++ b/stacks/gitea/docker-compose.yml @@ -0,0 +1,20 @@ +services: + gitea: + image: gitea/gitea:latest + container_name: gitea + environment: + - USER_UID=1000 + - USER_GID=1000 + - GITEA__database__DB_TYPE=sqlite3 + volumes: + - /opt/gitea/data:/data + ports: + - "3001:3000" + - "222:22" + restart: unless-stopped + networks: + - backbone + +networks: + backbone: + external: true diff --git a/stacks/home-automation/docker-compose.yml b/stacks/home-automation/docker-compose.yml new file mode 100755 index 0000000..886d373 --- /dev/null +++ b/stacks/home-automation/docker-compose.yml @@ -0,0 +1,62 @@ +services: + homebridge: + container_name: homebridge + image: homebridge/homebridge:latest + restart: unless-stopped + network_mode: host + environment: + - HOMEBRIDGE_CONFIG_UI_PORT=8581 + - ENABLE_AVAHI=1 + volumes: + - homebridge-data:/homebridge + + scrypted: + container_name: scrypted + image: koush/scrypted:latest + restart: unless-stopped + network_mode: host + environment: + - SCRYPTED_WEBHOOK_UPDATE_AUTHORIZATION=SET_THIS_TO_SOME_RANDOM_TEXT_BROHAM + - SCRYPTED_WEBHOOK_UPDATE=http://localhost:10444/v1/update + - ENABLE_AUDIO=True + volumes: + - /.scrypted/volume:/server/volume + labels: + - "com.centurylinklabs.watchtower.scope=scrypted" + + wyze-bridge: + container_name: wyze-bridge + image: mrlt8/wyze-bridge:latest + restart: unless-stopped + network_mode: host + environment: + - WYZE_EMAIL=acwallace520@gmail.com + - WYZE_PASSWORD=Alexander!626 + - API_ID=b98f65a0-92b6-4772-baa7-cde4663143f8 + - API_KEY=vhEztGgO1gRNL5utan5Bw1V6Z0TgTyle4EhYPpkmZD5klwBJ2nHQHCcItHUi + - WB_AUTH=False + - ENABLE_AUDIO=True + - AUDIO_CODEC=AAC + - RTSP_FW=Force + + scrypted-watchtower: + container_name: scrypted-watchtower + image: containrrr/watchtower + restart: unless-stopped + environment: + - WATCHTOWER_SCOPE=scrypted + - WATCHTOWER_HTTP_API_PERIODIC_POLLS=true + - WATCHTOWER_HTTP_API_TOKEN=SET_THIS_TO_SOME_RANDOM_TEXT_BRO + - WATCHTOWER_HTTP_API_UPDATE=true + volumes: + - /var/run/docker.sock:/var/run/docker.sock + command: --interval 3600 --cleanup --scope scrypted + labels: + - "com.centurylinklabs.watchtower.scope=scrypted" + ports: + - "10444:8080" + +volumes: + homebridge-data: + external: true + name: homebridge_homebridge diff --git a/stacks/homebridge/docker-compose.yml b/stacks/homebridge/docker-compose.yml new file mode 100755 index 0000000..223ad00 --- /dev/null +++ b/stacks/homebridge/docker-compose.yml @@ -0,0 +1,16 @@ +services: + homebridge: + container_name: homebridge + image: homebridge/homebridge:latest + restart: unless-stopped + network_mode: host + environment: + - HOMEBRIDGE_CONFIG_UI_PORT=8581 + - ENABLE_AVAHI=1 + volumes: + - homebridge-data:/homebridge + +volumes: + homebridge-data: + external: true + name: homebridge_homebridge diff --git a/stacks/matrix b/stacks/matrix new file mode 160000 index 0000000..46f1bde --- /dev/null +++ b/stacks/matrix @@ -0,0 +1 @@ +Subproject commit 46f1bded3923c1c64b625f2c8f47a4dd6ad101f7 diff --git a/stacks/mealie/docker-compose.yml b/stacks/mealie/docker-compose.yml new file mode 100755 index 0000000..c216a25 --- /dev/null +++ b/stacks/mealie/docker-compose.yml @@ -0,0 +1,23 @@ +services: + mealie: + container_name: mealie + image: ghcr.io/mealie-recipes/mealie:latest + restart: unless-stopped + ports: + - "9925:9000" + environment: + BASE_URL: "https://mealie.invokesmoke.dev" + volumes: + - mealie-data:/app/data + networks: + - backbone + +volumes: + mealie-data: + external: true + name: "766bc8605e066d1eba7287428e4e4130442f8a9473d730ed4abfd5fdd47c9076" + +networks: + backbone: + external: true + name: backbone diff --git a/stacks/pi-hole/docker-compose.yml b/stacks/pi-hole/docker-compose.yml new file mode 100755 index 0000000..e3159b7 --- /dev/null +++ b/stacks/pi-hole/docker-compose.yml @@ -0,0 +1,29 @@ +services: + pihole: + container_name: pihole + image: pihole/pihole:latest + restart: unless-stopped + ports: + - "53:53/tcp" + - "53:53/udp" + - "8053:80/tcp" + environment: + - WEBPASSWORD=${PIHOLE_PASSWORD} + - ServerIP=192.168.1.40 + volumes: + - etc-pihole:/etc/pihole + - etc-dnsmasq.d:/etc/dnsmasq.d + networks: + - backbone + +volumes: + etc-pihole: + external: true + name: etc-pihole + etc-dnsmasq.d: + external: true + name: etc-dnsmasq.d + +networks: + backbone: + name: backbone diff --git a/stacks/scripts/do-update.sh b/stacks/scripts/do-update.sh new file mode 100644 index 0000000..32330a6 --- /dev/null +++ b/stacks/scripts/do-update.sh @@ -0,0 +1,158 @@ +#!/usr/bin/env bash +# ============================================================= +# do-update.sh +# Interactive picker to update containers flagged by Watchtower. +# Pulls new image, recreates container via its compose file, +# and prunes old images. +# +# Usage: +# bash /opt/stacks/scripts/do-update.sh +# ============================================================= + +set -euo pipefail + +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +CYAN='\033[0;36m' +GRAY='\033[0;90m' +BOLD='\033[1m' +NC='\033[0m' + +# ── Check Watchtower is running ─────────────────────────────── +if ! docker inspect watchtower &>/dev/null; then + echo -e "${RED}✗ Watchtower is not running.${NC}" + exit 1 +fi + +# ── Get outdated images from Watchtower logs ────────────────── +OUTDATED_IMAGES=() +while IFS= read -r line; do + img=$(echo "$line" | grep -oP '(?<=Found new )\S+(?= image)') + [ -n "$img" ] && OUTDATED_IMAGES+=("$img") +done < <(docker logs watchtower --since 36h 2>&1 | grep "Found new") + +if [ ${#OUTDATED_IMAGES[@]} -eq 0 ]; then + echo -e "${GREEN}✓ No updates available per last Watchtower scan.${NC}" + exit 0 +fi + +# ── Build list of containers to update ─────────────────────── +declare -a CONTAINER_NAMES +declare -a CONTAINER_IMAGES +declare -a CONTAINER_COMPOSE_DIRS +declare -a CONTAINER_SERVICES + +for image in "${OUTDATED_IMAGES[@]}"; do + while IFS='|' read -r cname cimage; do + COMPOSE_DIR=$(docker inspect "$cname" \ + --format '{{index .Config.Labels "com.docker.compose.project.working_dir"}}' 2>/dev/null || echo "") + SERVICE=$(docker inspect "$cname" \ + --format '{{index .Config.Labels "com.docker.compose.service"}}' 2>/dev/null || echo "") + CONTAINER_NAMES+=("$cname") + CONTAINER_IMAGES+=("$cimage") + CONTAINER_COMPOSE_DIRS+=("$COMPOSE_DIR") + CONTAINER_SERVICES+=("$SERVICE") + done < <(docker ps -a --format '{{.Names}}|{{.Image}}' | awk -F'|' -v img="$image" '$2==img') +done + +if [ ${#CONTAINER_NAMES[@]} -eq 0 ]; then + echo -e "${YELLOW}⚠ Outdated images found but no matching running containers.${NC}" + exit 0 +fi + +# ── Show menu ───────────────────────────────────────────────── +echo "" +echo -e "${BOLD}Available updates:${NC}" +echo "" +for i in "${!CONTAINER_NAMES[@]}"; do + idx=$((i + 1)) + name="${CONTAINER_NAMES[$i]}" + image="${CONTAINER_IMAGES[$i]}" + svc="${CONTAINER_SERVICES[$i]}" + dir="${CONTAINER_COMPOSE_DIRS[$i]}" + printf " ${CYAN}%2d)${NC} %-20s ${GRAY}%-40s${NC}\n" "$idx" "$name" "$image" + if [ -n "$dir" ]; then + printf " ${GRAY}compose: %s service: %s${NC}\n" "$dir" "$svc" + fi +done +echo "" +echo -e " ${GRAY}a)${NC} update all" +echo -e " ${GRAY}q)${NC} quit" +echo "" +read -rp " Enter numbers to update (e.g. 1 3 5): " SELECTION + +if [ "$SELECTION" = "q" ] || [ -z "$SELECTION" ]; then + echo -e "\n ${GRAY}Cancelled.${NC}" + exit 0 +fi + +# ── Build list of selected indices ──────────────────────────── +SELECTED=() +if [ "$SELECTION" = "a" ]; then + for i in "${!CONTAINER_NAMES[@]}"; do SELECTED+=("$i"); done +else + for num in $SELECTION; do + idx=$((num - 1)) + if [ "$idx" -ge 0 ] && [ "$idx" -lt "${#CONTAINER_NAMES[@]}" ]; then + SELECTED+=("$idx") + else + echo -e " ${YELLOW}⚠ Skipping invalid selection: ${num}${NC}" + fi + done +fi + +if [ ${#SELECTED[@]} -eq 0 ]; then + echo -e " ${YELLOW}No valid selections.${NC}" + exit 1 +fi + +# ── Confirm ─────────────────────────────────────────────────── +echo "" +echo -e "${BOLD}Will update:${NC}" +for i in "${SELECTED[@]}"; do + echo -e " ${GREEN}→${NC} ${CONTAINER_NAMES[$i]} ${GRAY}(${CONTAINER_IMAGES[$i]})${NC}" +done +echo "" +read -rp " Proceed? [y/N] " CONFIRM +[[ "$CONFIRM" =~ ^[Yy]$ ]] || { echo -e " ${GRAY}Aborted.${NC}"; exit 0; } + +# ── Update selected containers ──────────────────────────────── +PRUNED=0 +for i in "${SELECTED[@]}"; do + name="${CONTAINER_NAMES[$i]}" + image="${CONTAINER_IMAGES[$i]}" + dir="${CONTAINER_COMPOSE_DIRS[$i]}" + svc="${CONTAINER_SERVICES[$i]}" + + echo "" + echo -e "${CYAN}── Updating ${name} ──────────────────────────────────────${NC}" + + echo -e " ${GRAY}pulling ${image}...${NC}" + docker pull "$image" + + if [ -n "$dir" ] && [ -d "$dir" ]; then + echo -e " ${GRAY}recreating via compose...${NC}" + docker compose -f "$dir/docker-compose.yml" up -d --force-recreate "$svc" + else + echo -e " ${YELLOW}⚠ No compose dir found — recreating container manually${NC}" + docker stop "$name" 2>/dev/null || true + docker rm "$name" 2>/dev/null || true + echo -e " ${YELLOW} Container removed. Restart manually with your compose file.${NC}" + fi + + echo -e " ${GREEN}✓ ${name} updated${NC}" + PRUNED=1 +done + +# ── Prune old images ────────────────────────────────────────── +if [ "$PRUNED" -eq 1 ]; then + echo "" + echo -e "${GRAY}Pruning dangling images...${NC}" + docker image prune -f + echo -e "${GREEN}✓ Done.${NC}" +fi + +echo "" +echo -e "${GREEN}${BOLD}All selected containers updated.${NC}" +echo "" diff --git a/stacks/scripts/image-sources.conf b/stacks/scripts/image-sources.conf new file mode 100644 index 0000000..a1f3202 --- /dev/null +++ b/stacks/scripts/image-sources.conf @@ -0,0 +1,39 @@ +# image-sources.conf +# Maps Docker image names to their GitHub repos for changelog lookup. +# Format: ["image/name"]="github-owner/repo" +# Add entries here as you add new services. + +declare -A IMAGE_REPOS=( + # Matrix stack + ["jevolk/tuwunel"]="jevolk/tuwunel" + ["livekit/livekit-server"]="livekit/livekit" + ["ghcr.io/element-hq/lk-jwt-service"]="element-hq/lk-jwt-service" + ["coturn/coturn"]="coturn/coturn" + + # Code Server + ["lscr.io/linuxserver/code-server"]="linuxserver/docker-code-server" + + # Caddy + ["caddy"]="caddyserver/caddy" + + # Gitea + ["gitea/gitea"]="go-gitea/gitea" + + # Homebridge + ["homebridge/homebridge"]="homebridge/homebridge" + + # Mealie + ["ghcr.io/mealie-recipes/mealie"]="mealie-recipes/mealie" + + # Pi-hole + ["pihole/pihole"]="pi-hole/pi-hole" + + # Scrypted + ["koush/scrypted"]="koush/scrypted" + + # Wyze Bridge + ["mrlt8/wyze-bridge"]="mrlt8/docker-wyze-bridge" + + # Watchtower + ["containrrr/watchtower"]="containrrr/watchtower" +) diff --git a/stacks/scripts/update-check.sh b/stacks/scripts/update-check.sh new file mode 100644 index 0000000..3ecb358 --- /dev/null +++ b/stacks/scripts/update-check.sh @@ -0,0 +1,114 @@ +#!/usr/bin/env bash +# ============================================================= +# update-check.sh +# Reads Watchtower's recent logs to find outdated images, +# maps them to containers, and fetches GitHub release notes. +# +# Usage: +# bash /opt/stacks/scripts/update-check.sh +# ============================================================= + +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +source "$SCRIPT_DIR/image-sources.conf" + +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +CYAN='\033[0;36m' +GRAY='\033[0;90m' +BOLD='\033[1m' +NC='\033[0m' + +SEP="${GRAY}$(printf '─%.0s' $(seq 1 60))${NC}" + +# ── Check Watchtower is running ─────────────────────────────── +if ! docker inspect watchtower &>/dev/null; then + echo -e "${RED}✗ Watchtower is not running.${NC}" + echo -e " Start it: cd /opt/stacks/watchtower && docker compose up -d" + exit 1 +fi + +# ── Parse Watchtower logs for outdated images (last 36h) ───── +echo -e "${BOLD}Checking Watchtower logs for updates...${NC}" +echo "" + +OUTDATED_IMAGES=() +while IFS= read -r line; do + # Extract image name from: "Found new some/image:tag image (sha256:...)" + img=$(echo "$line" | grep -oP '(?<=Found new )\S+(?= image)') + [ -n "$img" ] && OUTDATED_IMAGES+=("$img") +done < <(docker logs watchtower --since 36h 2>&1 | grep "Found new") + +if [ ${#OUTDATED_IMAGES[@]} -eq 0 ]; then + echo -e "${GREEN}✓ All images are up to date${NC} ${GRAY}(per last Watchtower scan)${NC}" + echo "" + LAST=$(docker logs watchtower --since 72h 2>&1 | grep "Session done" | tail -1 | grep -oP '\d{4}-\d{2}-\d{2}T\d{2}:\d{2}') + [ -n "$LAST" ] && echo -e "${GRAY} Last scan: ${LAST}${NC}" + exit 0 +fi + +echo -e "${YELLOW}${BOLD}${#OUTDATED_IMAGES[@]} image update(s) available:${NC}" +echo "" + +# ── For each outdated image: find container + fetch notes ───── +gh_release_notes() { + local repo=$1 + local result + result=$(curl -sf "https://api.github.com/repos/${repo}/releases/latest" 2>/dev/null) || true + if [ -n "$result" ]; then + TAG=$(echo "$result" | python3 -c "import sys,json; d=json.load(sys.stdin); print(d.get('tag_name','?'))" 2>/dev/null) + BODY=$(echo "$result" | python3 -c " +import sys,json,re +d=json.load(sys.stdin) +body=d.get('body','No release notes available.') +# Strip markdown links/badges, collapse whitespace +body=re.sub(r'!\[.*?\]\(.*?\)','',body) +body=re.sub(r'\[([^\]]+)\]\([^\)]+\)',r'\1',body) +body=re.sub(r'','',body,flags=re.DOTALL) +lines=[l.strip() for l in body.split('\n') if l.strip()] +print('\n'.join(lines[:6])) +" 2>/dev/null || echo "No release notes available.") + echo "$TAG|$BODY" + fi +} + +for image in "${OUTDATED_IMAGES[@]}"; do + # Find container(s) using this image + CONTAINERS=$(docker ps -a --format '{{.Names}}|{{.Image}}' | awk -F'|' -v img="$image" '$2==img {print $1}' | tr '\n' ' ') + [ -z "$CONTAINERS" ] && CONTAINERS="${GRAY}(no running containers)${NC}" + + echo -e "$SEP" + echo -e " ${CYAN}${BOLD}${image}${NC}" + echo -e " ${GRAY}containers:${NC} ${CONTAINERS}" + + # Strip tag for image name lookup + base_image=$(echo "$image" | sed 's/:.*//') + + if [ -n "${IMAGE_REPOS[$base_image]+_}" ]; then + REPO="${IMAGE_REPOS[$base_image]}" + RELEASE=$(gh_release_notes "$REPO") + if [ -n "$RELEASE" ]; then + TAG=$(echo "$RELEASE" | cut -d'|' -f1) + NOTES=$(echo "$RELEASE" | cut -d'|' -f2-) + echo -e " ${GRAY}latest tag:${NC} ${GREEN}${TAG}${NC}" + echo -e " ${GRAY}notes:${NC}" + while IFS= read -r line; do + echo -e " ${GRAY}${line}${NC}" + done <<< "$NOTES" + echo -e " ${GRAY}full notes:${NC} https://github.com/${REPO}/releases/latest" + else + echo -e " ${GRAY}→ https://hub.docker.com/r/${base_image}/tags${NC}" + fi + else + echo -e " ${GRAY}no GitHub mapping — add to scripts/image-sources.conf${NC}" + echo -e " ${GRAY}→ https://hub.docker.com/r/${base_image}/tags${NC}" + fi + echo "" +done + +echo -e "$SEP" +echo "" +echo -e " Ready to update? Run: ${CYAN}bash /opt/stacks/scripts/do-update.sh${NC}" +echo "" diff --git a/stacks/scrypted/docker-compose.yml b/stacks/scrypted/docker-compose.yml new file mode 100755 index 0000000..6a613ad --- /dev/null +++ b/stacks/scrypted/docker-compose.yml @@ -0,0 +1,31 @@ +services: + scrypted: + container_name: scrypted + image: koush/scrypted:latest + restart: unless-stopped + network_mode: host + environment: + - SCRYPTED_WEBHOOK_UPDATE_AUTHORIZATION=23e46534ef408ec7c221da15c2cda92892ca5c4a637249075fbdf39358dbbaaa + - SCRYPTED_WEBHOOK_UPDATE=http://localhost:10444/v1/update + - ENABLE_AUDIO=True + volumes: + - /.scrypted/volume:/server/volume + labels: + - "com.centurylinklabs.watchtower.scope=scrypted" + + scrypted-watchtower: + container_name: scrypted-watchtower + image: containrrr/watchtower + restart: unless-stopped + environment: + - WATCHTOWER_SCOPE=scrypted + - WATCHTOWER_HTTP_API_PERIODIC_POLLS=true + - WATCHTOWER_HTTP_API_TOKEN=18fc4c286cb14ce86e8719b939fd23036dbb843c451c88344f2e2edd0160a4eb + - WATCHTOWER_HTTP_API_UPDATE=true + volumes: + - /var/run/docker.sock:/var/run/docker.sock + command: --interval 3600 --cleanup --scope scrypted + labels: + - "com.centurylinklabs.watchtower.scope=scrypted" + ports: + - "10444:8080" diff --git a/stacks/todo.md b/stacks/todo.md new file mode 100644 index 0000000..e69de29 diff --git a/stacks/watchtower/docker-compose.yml b/stacks/watchtower/docker-compose.yml new file mode 100644 index 0000000..e4ad16b --- /dev/null +++ b/stacks/watchtower/docker-compose.yml @@ -0,0 +1,20 @@ +services: + watchtower: + image: containrrr/watchtower:latest + container_name: watchtower + restart: unless-stopped + volumes: + - /var/run/docker.sock:/var/run/docker.sock + command: + - --monitor-only + - --schedule + - "0 0 3 * * *" + - --include-stopped + - --log-level=info + networks: + - backbone + +networks: + backbone: + external: true + name: backbone diff --git a/stacks/wireguard/docker-compose.yml b/stacks/wireguard/docker-compose.yml new file mode 100755 index 0000000..6f8f987 --- /dev/null +++ b/stacks/wireguard/docker-compose.yml @@ -0,0 +1,29 @@ +services: + wg-easy: + image: ghcr.io/wg-easy/wg-easy + container_name: wireguard + environment: + - LANG=en + - WG_HOST=104.230.231.144 + - PASSWORD_HASH=$$2a$$12$$BSBRXnlt8Bdcqb25qruo9.6HamIVlsmreo33ja0pHZUEghUnzPCIK + - WG_PORT=51820 + - WG_DEFAULT_DNS=192.168.1.40 + - WG_ALLOWED_IPS=192.168.1.0/24 + volumes: + - /opt/wireguard/data:/etc/wireguard + ports: + - "51820:51820/udp" + - "41821:51821/tcp" + restart: unless-stopped + cap_add: + - NET_ADMIN + - SYS_MODULE + sysctls: + - net.ipv4.ip_forward=1 + - net.ipv4.conf.all.src_valid_mark=1 + networks: + - backbone + +networks: + backbone: + external: true diff --git a/stacks/wyze-bridge/docker-compose.yml b/stacks/wyze-bridge/docker-compose.yml new file mode 100755 index 0000000..ea2f0cf --- /dev/null +++ b/stacks/wyze-bridge/docker-compose.yml @@ -0,0 +1,15 @@ +services: + wyze-bridge: + container_name: wyze-bridge + image: mrlt8/wyze-bridge:latest + restart: unless-stopped + network_mode: host + environment: + - WYZE_EMAIL=acwallace520@gmail.com + - WYZE_PASSWORD=Alexander!626 + - API_ID=b98f65a0-92b6-4772-baa7-cde4663143f8 + - API_KEY=vhEztGgO1gRNL5utan5Bw1V6Z0TgTyle4EhYPpkmZD5klwBJ2nHQHCcItHUi + - WB_AUTH=False + - ENABLE_AUDIO=True + - AUDIO_CODEC=AAC + - RTSP_FW=Force