Files
Linux_post_install/bin/pos-docker-compose
T
he 5ef38dc46f fix: resolve all 23 MAINTENANCE audit tickets
- deps guards before -h|--help in docker-health/ps, network-scan,
  usb-server, media-mp3/mp4 (--dry-run pre-scan kept); system-firewall
  gains usage()/--help; autostart/usb-automount get flags.sh + template
- install.sh: normalize N-M range syntax in --steps
- bin/pos: INTERACTIVE_CMDS += docker-compose docker-vbox network-hotspot
- common.sh: canonical XDG-aware CONFIG_DIR + DIM color var; notify.sh
  stderr fallback; ent_plugin_* registry renames (runtime plugin API kept)
- docker-compose SCALE_DIR/CONFIG_ENV env seams; ffmpeg in PACKAGES;
  scrcpy.sh exec bit
- docs: health is console-only (--send/--markdown removed), POS.md file
  refs for config/tree/entertainment, DEV.md no-guard exception, docmap/
  filetable regenerated (make gen), hand-maintained line rows bumped
- add scripts/lint-conventions.sh gate + Makefile lint target; record
  all VERIFIED outcomes in MAINTENANCE.md; AGENT_TODO Done entry
  (2026-08-14)
- gates: make gen/check/lint all green (0 FAIL, 0 WARN); bash -n sweep
  clean; restricted-PATH dep tests + step-matrix dry-runs verified
2026-08-14 12:57:00 -04:00

367 lines
12 KiB
Bash
Executable File

#!/usr/bin/env bash
set -euo pipefail
# POS: docker compose — Docker Compose service manager (ls/up/down/restart/logs/update/config)
# POS_SUBCMDS: ls installed up down restart logs update config
# POS_CONFIG: compose | compose.env | TS_AUTHKEY=secret:Tailscale auth key for the sidecar | TZ=:Service timezone (default Europe/Amsterdam) | DNS_SERVER=:Custom DNS server (default 9.9.9.9) | SERVICES_BASE=:Deployment root (default /srv)
source "$(dirname "$0")/../lib/common.sh" 2>/dev/null || source "$(dirname "$0")/common.sh"
SCALE_DIR="${SCALE_DIR:-/usr/local/share/linux_post_install/scale-tail/services}"
CONFIG_ENV="${CONFIG_ENV:-${HOME}/.config/linux_post_install/compose.env}"
usage() {
cat <<EOF
Usage: pos docker compose <command> [args]
Commands:
ls List all available ScaleTail service templates
installed List services already deployed on this machine
up <service> Deploy or start a service
down <service> Stop and remove a deployed service
restart <service> Restart a running service
logs <service> [-f] View logs (use -f to follow)
update Pull latest ScaleTail templates + refresh all deployed compose files
config Show current global configuration
config set KEY=VALUE Set a global default (e.g. TS_AUTHKEY, TZ, SERVICES_BASE)
config edit Open global config in your editor (\$EDITOR)
─── Config Strategy ──────────────────────────────────────────
Three layers — each overrides the one above:
1. Template defaults
/usr/local/share/linux_post_install/scale-tail/services/<name>/.env
2. Global config (your defaults — set once, applies to all services)
~/.config/linux_post_install/compose.env
Keys: TS_AUTHKEY | TZ | DNS_SERVER | SERVICES_BASE
3. Per-service config (created on first deploy, preserved forever)
/srv/<service>/.env (or \$SERVICES_BASE/<service>/.env)
On first "up", the service .env is generated from the template
and filled with values from your global config. After that, the
per-service .env is never overwritten — even by "update".
─── Paths ────────────────────────────────────────────────────
Templates: /usr/local/share/linux_post_install/scale-tail/services/
Deployments: \$SERVICES_BASE/<service>/ (default: /srv/<service>/)
Global conf: ~/.config/linux_post_install/compose.env
─── Quick Start ──────────────────────────────────────────────
1. Set your Tailscale auth key (required):
pos docker compose config set TS_AUTHKEY=tskey-auth-xxxxx
2. Deploy a service:
pos docker compose up jellyfin
3. Open https://jellyfin.tail-xxxxx.ts.net
Examples:
pos docker compose up jellyfin
pos docker compose down actual-budget
pos docker compose logs home-assistant -f
pos docker compose config set TZ=Asia/Tokyo
pos docker compose config set SERVICES_BASE=/data
EOF
exit 0
}
check_deps() {
command -v docker &>/dev/null || err "docker not found — run 'install.sh --apps' and install Docker first"
}
check_templates() {
[ -d "$SCALE_DIR" ] || err "ScaleTail templates not found at $SCALE_DIR — run 'install.sh' to set them up"
}
check_service() {
local svc="$1"
[ -d "$SCALE_DIR/$svc" ] || err "Unknown service '$svc' — run 'pos docker compose ls' to see available services"
}
load_global_config() {
mkdir -p "$(dirname "$CONFIG_ENV")"
[ -f "$CONFIG_ENV" ] && source "$CONFIG_ENV"
SERVICES_BASE="${SERVICES_BASE:-/srv}"
}
service_dir() {
local svc="$1"
echo "$SERVICES_BASE/$svc"
}
###############################################################################
# Commands
###############################################################################
cmd_ls() {
check_templates
local names=()
for svc in "$SCALE_DIR"/*/; do
names+=("$(basename "$svc")")
done
IFS=$'\n' names=($(sort <<<"${names[*]}")); unset IFS
echo "${CYAN}Available ScaleTail services:${RESET}"
echo "${BLUE}────────────────────────────────────────${RESET}"
local name
for name in "${names[@]}"; do
printf " ${GREEN}%s${RESET}\n" "$name"
done
echo "${BLUE}────────────────────────────────────────${RESET}"
echo " ${#names[@]} services total"
}
cmd_installed() {
load_global_config
local count=0
echo "${CYAN}Deployed services:${RESET}"
echo "${BLUE}────────────────────────────────────────${RESET}"
[ -d "$SERVICES_BASE" ] || { echo " (none — $SERVICES_BASE does not exist)"; echo "${BLUE}────────────────────────────────────────${RESET}"; echo " 0 services deployed"; return; }
for svc in "$SERVICES_BASE"/*/; do
[ -d "$svc" ] || continue
local name
name=$(basename "$svc")
local status=""
if [ -f "$svc/compose.yaml" ] || [ -f "$svc/compose.yml" ]; then
status=$(docker compose ls --format json 2>/dev/null | python3 -c "
import sys, json
try:
data = json.load(sys.stdin)
if not isinstance(data, list):
data = [data]
for e in data:
if e.get('Name') == '$name':
print(e.get('Status', 'unknown'))
break
except: pass
" 2>/dev/null || echo "unknown")
fi
printf " ${GREEN}%-28s${RESET} %s\n" "$name" "${status:-unknown}"
count=$((count + 1))
done
echo "${BLUE}────────────────────────────────────────${RESET}"
echo " $count services deployed"
}
cmd_up() {
local svc="$1"
[ -z "$svc" ] && usage
check_deps
check_templates
check_service "$svc"
load_global_config
local target
target=$(service_dir "$svc")
if [ ! -d "$target" ]; then
log "Deploying $svc to $target"
mkdir -p "$target/config" "$target/data"
if [ -f "$SCALE_DIR/$svc/compose.yaml" ]; then
cp "$SCALE_DIR/$svc/compose.yaml" "$target/"
fi
if [ -f "$SCALE_DIR/$svc/compose.yml" ]; then
cp "$SCALE_DIR/$svc/compose.yml" "$target/"
fi
else
mkdir -p "$target/config" "$target/data"
fi
local env_file="$target/.env"
if [ ! -f "$env_file" ]; then
if [ -f "$SCALE_DIR/$svc/.env" ]; then
cp "$SCALE_DIR/$svc/.env" "$env_file"
else
cat > "$env_file" <<-EOF
SERVICE=$svc
IMAGE_URL=
SERVICEPORT=
DNS_SERVER=${DNS_SERVER:-9.9.9.9}
TS_AUTHKEY=${TS_AUTHKEY:-}
TZ=${TZ:-Europe/Amsterdam}
EOF
fi
if [ -n "${TS_AUTHKEY:-}" ]; then
sed -i "s|^TS_AUTHKEY=.*|TS_AUTHKEY=$TS_AUTHKEY|" "$env_file" 2>/dev/null || true
fi
if [ -n "${TZ:-}" ]; then
sed -i "s|^TZ=.*|TZ=$TZ|" "$env_file" 2>/dev/null || true
fi
if [ -n "${DNS_SERVER:-}" ]; then
sed -i "s|^DNS_SERVER=.*|DNS_SERVER=$DNS_SERVER|" "$env_file" 2>/dev/null || true
fi
if grep -q "^TS_AUTHKEY=$" "$env_file" 2>/dev/null; then
warn "TS_AUTHKEY is not set"
local key
read -rp " Enter your Tailscale auth key (or press Enter to skip): " key
if [ -n "$key" ]; then
sed -i "s|^TS_AUTHKEY=.*|TS_AUTHKEY=$key|" "$env_file"
fi
fi
log ".env created at $env_file"
if confirm "Edit .env before starting?" Y; then
"${EDITOR:-nano}" "$env_file"
log "Continuing with startup..."
fi
fi
log "Starting $svc..."
(cd "$target" && docker compose up -d)
ok "$svc is running"
}
cmd_down() {
local svc="$1"
[ -z "$svc" ] && usage
check_deps
load_global_config
local target
target=$(service_dir "$svc")
[ -d "$target" ] || err "$svc is not deployed at $target"
log "Stopping $svc..."
(cd "$target" && docker compose down)
ok "$svc stopped"
}
cmd_restart() {
local svc="$1"
[ -z "$svc" ] && usage
check_deps
load_global_config
local target
target=$(service_dir "$svc")
[ -d "$target" ] || err "$svc is not deployed"
log "Restarting $svc..."
(cd "$target" && docker compose restart)
ok "$svc restarted"
}
cmd_logs() {
local svc="$1"
shift 2>/dev/null || true
[ -z "$svc" ] && usage
check_deps
load_global_config
local target
target=$(service_dir "$svc")
[ -d "$target" ] || err "$svc is not deployed"
(cd "$target" && exec docker compose logs "$@")
}
cmd_update() {
check_templates
log "Updating ScaleTail templates..."
sudo git -C "/usr/local/share/linux_post_install/scale-tail" pull
ok "Templates updated"
load_global_config
local refreshed=0
[ -d "$SERVICES_BASE" ] || { ok "$refreshed compose files refreshed (.env preserved)"; return; }
for svc in "$SERVICES_BASE"/*/; do
[ -d "$svc" ] || continue
local name
name=$(basename "$svc")
if [ -f "$SCALE_DIR/$name/compose.yaml" ]; then
cp "$SCALE_DIR/$name/compose.yaml" "$svc/compose.yaml"
refreshed=$((refreshed + 1))
fi
if [ -f "$SCALE_DIR/$name/compose.yml" ]; then
cp "$SCALE_DIR/$name/compose.yml" "$svc/compose.yml"
refreshed=$((refreshed + 1))
fi
done
ok "$refreshed compose files refreshed (.env preserved)"
}
cmd_config() {
local action="${1:-show}"
shift 2>/dev/null || true
case "$action" in
show)
load_global_config
echo "${CYAN}Global compose config${RESET}"
echo "${DIM}File: $CONFIG_ENV${RESET}"
echo "${BLUE}────────────────────────────────────────${RESET}"
if [ -f "$CONFIG_ENV" ]; then
cat "$CONFIG_ENV"
else
echo "(no global config set — using defaults)"
fi
echo "${BLUE}────────────────────────────────────────${RESET}"
echo "SERVICES_BASE=$SERVICES_BASE (deployment directory)"
echo "${DIM}Set with: pos docker compose config set SERVICES_BASE=/srv${RESET}"
;;
set)
local pair="${1:-}"
[ -z "$pair" ] && usage
local key="${pair%%=*}"
local val="${pair#*=}"
mkdir -p "$(dirname "$CONFIG_ENV")"
if [ -f "$CONFIG_ENV" ] && grep -q "^${key}=" "$CONFIG_ENV" 2>/dev/null; then
sed -i "s|^${key}=.*|${key}=${val}|" "$CONFIG_ENV"
else
echo "${key}=${val}" >> "$CONFIG_ENV"
fi
ok "Set ${key}=${val}"
;;
edit)
mkdir -p "$(dirname "$CONFIG_ENV")"
if [ ! -f "$CONFIG_ENV" ]; then
cat > "$CONFIG_ENV" <<-EOF
# Linux_post_install — global Docker Compose config
# Each key is optional; empty values prompt on first deploy.
TS_AUTHKEY=
TZ=Europe/Amsterdam
DNS_SERVER=9.9.9.9
SERVICES_BASE=/srv
EOF
log "Created default config at $CONFIG_ENV"
fi
log "Opening $CONFIG_ENV in ${EDITOR:-nano}..."
"${EDITOR:-nano}" "$CONFIG_ENV"
log "Config saved"
;;
*)
usage
;;
esac
}
###############################################################################
# CLI dispatch
###############################################################################
[ $# -eq 0 ] && usage
case "${1:-}" in
-h|--help) usage ;;
esac
cmd="${1:-}"
shift
case "$cmd" in
ls) cmd_ls ;;
installed) cmd_installed ;;
up) cmd_up "${1:-}" ;;
down) cmd_down "${1:-}" ;;
restart) cmd_restart "${1:-}" ;;
logs) cmd_logs "$@" ;;
update) cmd_update ;;
config) cmd_config "$@" ;;
*) usage ;;
esac