Compare commits

..

2 Commits

Author SHA1 Message Date
samjage c948d5cbde added stacks for backup 2026-04-06 10:13:21 -04:00
samjage 12892ec9db Updates 2026-04-06 00:55:58 -04:00
80 changed files with 998 additions and 0 deletions
Regular → Executable
View File
Regular → Executable
View File
Regular → Executable
View File
Regular → Executable
View File
View File
Regular → Executable
View File
View File
View File
View File
View File
View File
View File
View File
View File
View File
View File
View File
View File
View File
View File
View File
View File
View File
View File

Before

Width:  |  Height:  |  Size: 3.0 KiB

After

Width:  |  Height:  |  Size: 3.0 KiB

Regular → Executable
View File
Regular → Executable
View File
Regular → Executable
View File
View File
View File
View File
View File
Regular → Executable
View File
View File
View File
View File
View File
View File
View File
Regular → Executable
View File
Regular → Executable
View File
View File
Regular → Executable
View File
Regular → Executable
View File
Regular → Executable
View File
Regular → Executable
View File
+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
Regular → Executable
View File