#!/usr/bin/env bash # Clusev restart-sentinel watcher — runs on the HOST (never in a container). # # Watches for the restart sentinel file the dashboard writes (via a shared bind # mount, see docker-compose.prod.yml: ./run -> storage/app/restart-signal) and, # when it appears, restarts the prod stack, then deletes the sentinel. # # SECURITY: the panel container gets NO Docker socket. It only writes a marker # file. This scoped host unit is the ONLY thing that talks to Docker — so a panel # compromise cannot drive arbitrary `docker` commands. # # Two ways to run it: # 1. systemd .path unit (preferred) — clusev-restart.path triggers # clusev-restart.service, which runs this script ONCE per sentinel. # Invoke as: watch.sh once # 2. Standalone poll/inotify loop (fallback, no systemd) — runs forever: # Invoke as: watch.sh loop # # Config via env (with sane defaults): # CLUSEV_DIR project dir holding docker-compose.prod.yml (default: script's repo root) # CLUSEV_SIGNAL absolute path of the sentinel file (default: $CLUSEV_DIR/run/restart.request) # CLUSEV_POLL poll interval seconds for the loop fallback (default: 5) set -euo pipefail # Resolve the repo root: docker/restart-sentinel/ -> ../../ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" DEFAULT_DIR="$(cd "${SCRIPT_DIR}/../.." && pwd)" CLUSEV_DIR="${CLUSEV_DIR:-$DEFAULT_DIR}" CLUSEV_SIGNAL="${CLUSEV_SIGNAL:-${CLUSEV_DIR}/run/restart.request}" CLUSEV_POLL="${CLUSEV_POLL:-5}" COMPOSE_FILE="${CLUSEV_DIR}/docker-compose.prod.yml" log() { printf '%s clusev-restart: %s\n' "$(date -Is)" "$*"; } # Restart the stack and consume the sentinel. `up -d` (not bare `restart`) so a # changed image/config is also applied and any down service is brought back. do_restart() { [ -f "$COMPOSE_FILE" ] || { log "compose file not found: $COMPOSE_FILE — skipping"; return 1; } log "restart requested — applying (docker compose up -d) in ${CLUSEV_DIR}" if docker compose -f "$COMPOSE_FILE" up -d; then log "stack restarted" else log "docker compose up -d FAILED — leaving sentinel so it retries" return 1 fi rm -f "$CLUSEV_SIGNAL" && log "sentinel cleared: $CLUSEV_SIGNAL" } # One shot: only act if the sentinel is actually present (the .path unit guarantees this, # but a manual invocation might not). run_once() { if [ -f "$CLUSEV_SIGNAL" ]; then do_restart else log "no sentinel at $CLUSEV_SIGNAL — nothing to do" fi } # Fallback loop for hosts without systemd: prefer inotifywait, else poll. run_loop() { log "watching $CLUSEV_SIGNAL (loop mode)" if command -v inotifywait >/dev/null 2>&1; then # Act once on startup in case the sentinel already exists, then block on events. [ -f "$CLUSEV_SIGNAL" ] && do_restart || true while true; do # Watch the directory (the file is created/moved into it) for create/move events. inotifywait -q -e create -e moved_to -e close_write "$(dirname "$CLUSEV_SIGNAL")" >/dev/null 2>&1 || true [ -f "$CLUSEV_SIGNAL" ] && do_restart || true done else log "inotifywait not found — polling every ${CLUSEV_POLL}s" while true; do [ -f "$CLUSEV_SIGNAL" ] && do_restart || true sleep "$CLUSEV_POLL" done fi } case "${1:-once}" in once) run_once ;; loop) run_loop ;; *) log "usage: $0 [once|loop]"; exit 64 ;; esac