added stacks for backup

This commit is contained in:
samjage
2026-04-06 10:13:21 -04:00
parent 12892ec9db
commit c948d5cbde
22 changed files with 998 additions and 0 deletions
+150
View File
@@ -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&current=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 ""
+61
View File
@@ -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
}
+47
View File
@@ -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
+83
View File
@@ -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
}
+30
View File
@@ -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
+26
View File
@@ -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
+24
View File
@@ -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
+20
View File
@@ -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
+20
View File
@@ -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
+62
View File
@@ -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
+16
View File
@@ -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
Submodule
+1
Submodule stacks/matrix added at 46f1bded39
+23
View File
@@ -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
+29
View File
@@ -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
+158
View File
@@ -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 ""
+39
View File
@@ -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"
)
+114
View File
@@ -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 ""
+31
View File
@@ -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"
View File
+20
View File
@@ -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
+29
View File
@@ -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
+15
View File
@@ -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