#!/bin/bash # nas-docker-up — Apply Docker updates via docker compose up -d # Usage: nas-docker-up [stack_name] # No argument: updates all containers with a newer image available # With argument: updates only the specified stack # Non-interactive mode (HA): applies directly, no prompt # Terminal mode: confirmation per stack (unless a specific stack is given) set -euo pipefail if [ -t 1 ]; then INTERACTIVE=true; else INTERACTIVE=false; fi TARGET_STACK="${1:-}" # Colors (terminal mode only) if $INTERACTIVE; then RED='\033[0;31m' GREEN='\033[0;32m' YELLOW='\033[1;33m' CYAN='\033[0;36m' BOLD='\033[1m' RESET='\033[0m' else RED='' GREEN='' YELLOW='' CYAN='' BOLD='' RESET='' fi # --- Detect global OMV env file --- OMV_GLOBAL_ENV="" if [ -f "/etc/omv-compose.env" ]; then OMV_GLOBAL_ENV="/etc/omv-compose.env" elif [ -f "/srv/omv-compose.env" ]; then OMV_GLOBAL_ENV="/srv/omv-compose.env" fi # --- Collect containers to update --- containers_to_update=() images_to_update=() compose_dirs=() container_ids=() old_versions=() new_versions=() if $INTERACTIVE; then echo -e "${BOLD}--- Phase 1: Detecting updates ---${RESET}" fi while IFS=: read -r container_id container_name; do # Filter by stack if argument provided if [ -n "$TARGET_STACK" ] && [ "$container_name" != "$TARGET_STACK" ]; then stack_label=$(docker inspect --format='{{index .Config.Labels "com.docker.compose.project"}}' "$container_id" 2>/dev/null | xargs) if [ "$stack_label" != "$TARGET_STACK" ]; then continue fi fi compose_dir=$(docker inspect --format='{{index .Config.Labels "com.docker.compose.project.working_dir"}}' "$container_id" 2>/dev/null | xargs) if [ -z "$compose_dir" ] || [ ! -d "$compose_dir" ]; then continue fi image_name=$(docker inspect --format='{{.Config.Image}}' "$container_id") old_image_id=$(docker inspect --format='{{.Image}}' "$container_id") old_ver=$(docker inspect --format='{{index .Config.Labels "org.opencontainers.image.version"}}' "$container_id" 2>/dev/null || echo "") [ -z "$old_ver" ] && old_ver="unknown" if $INTERACTIVE; then echo -ne " Checking ${CYAN}${container_name}${RESET}... " fi if ! docker pull "$image_name" > /dev/null 2>&1; then if $INTERACTIVE; then echo -e "${YELLOW}⚠ Pull error (skipped)${RESET}"; fi continue fi new_image_id=$(docker inspect --format='{{.Id}}' "$image_name" 2>/dev/null) if [ "$old_image_id" != "$new_image_id" ]; then new_ver=$(docker inspect --format='{{index .Config.Labels "org.opencontainers.image.version"}}' "$image_name" 2>/dev/null || echo "") [ -z "$new_ver" ] && new_ver="available" if $INTERACTIVE; then echo -e "${YELLOW}UPDATE AVAILABLE${RESET}: ${YELLOW}${old_ver}${RESET} → ${GREEN}${new_ver}${RESET}" fi containers_to_update+=("$container_name") images_to_update+=("$image_name") compose_dirs+=("$compose_dir") container_ids+=("$container_id") old_versions+=("$old_ver") new_versions+=("$new_ver") else if $INTERACTIVE; then echo -e "${GREEN}✅ Up to date${RESET}"; fi fi done < <(docker ps --format "{{.ID}}:{{.Names}}") if [ ${#containers_to_update[@]} -eq 0 ]; then if $INTERACTIVE; then echo "" echo -e "${GREEN}✅ No containers to update.${RESET}" fi exit 0 fi if $INTERACTIVE; then echo "" echo -e "${BOLD}--- Phase 2: Available updates ---${RESET}" for i in "${!containers_to_update[@]}"; do echo -e " • ${CYAN}${containers_to_update[$i]}${RESET}: ${YELLOW}${old_versions[$i]}${RESET} → ${GREEN}${new_versions[$i]}${RESET}" done echo "" # If a specific stack was given, apply directly without asking if [ -z "$TARGET_STACK" ]; then UPDATE_ALL=false while true; do read -p "Update ALL containers? [y]es (All) / [n]o (Choose per container): " global_choice case "$global_choice" in [yY]*) UPDATE_ALL=true; break ;; [nN]*) UPDATE_ALL=false; break ;; *) echo "Please answer y or n." ;; esac done else UPDATE_ALL=true fi fi # --- Phase 3: Apply --- if $INTERACTIVE; then echo "" echo -e "${BOLD}--- Phase 3: Applying updates ---${RESET}" fi START_DIR=$(pwd) upgraded=0 failed=0 for i in "${!containers_to_update[@]}"; do c_name="${containers_to_update[$i]}" c_dir="${compose_dirs[$i]}" c_old_ver="${old_versions[$i]}" c_new_ver="${new_versions[$i]}" DO_UPDATE=false if ! $INTERACTIVE; then # HA mode: always apply DO_UPDATE=true elif [ "$UPDATE_ALL" = true ]; then DO_UPDATE=true else while true; do read -p " Update '${c_name}' (${c_old_ver} → ${c_new_ver})? [y/n]: " choice case "$choice" in [yY]*) DO_UPDATE=true; break ;; [nN]*) DO_UPDATE=false; break ;; *) echo "Please answer y or n." ;; esac done fi if [ "$DO_UPDATE" = true ]; then if $INTERACTIVE; then echo -e " 🚀 Updating ${CYAN}${c_name}${RESET} in ${c_dir}..." fi if cd "$c_dir" 2>/dev/null; then ENV_ARGS="" # Global OMV env if [ -n "$OMV_GLOBAL_ENV" ] && [ -f "$OMV_GLOBAL_ENV" ]; then ENV_ARGS="--env-file $OMV_GLOBAL_ENV" else # Fallback: look for global.env or .env in parent directory PARENT_DIR=$(dirname "$c_dir") if [ -f "$PARENT_DIR/global.env" ]; then ENV_ARGS="--env-file $PARENT_DIR/global.env" elif [ -f "$PARENT_DIR/.env" ]; then ENV_ARGS="--env-file $PARENT_DIR/.env" fi fi # Local .env (always added if present) if [ -f ".env" ]; then ENV_ARGS="$ENV_ARGS --env-file .env" fi if docker compose $ENV_ARGS up -d --remove-orphans 2>&1; then if $INTERACTIVE; then echo -e " ${GREEN}✅ ${c_name} updated successfully.${RESET}" fi upgraded=$((upgraded + 1)) else if $INTERACTIVE; then echo -e " ${RED}❌ Update failed for ${c_name}.${RESET}" fi failed=$((failed + 1)) fi else if $INTERACTIVE; then echo -e " ${RED}❌ Cannot access ${c_dir}.${RESET}" fi failed=$((failed + 1)) fi else if $INTERACTIVE; then echo -e " ⏭ Skipped: ${c_name}" fi fi cd "$START_DIR" done if $INTERACTIVE; then echo "" echo -e "${BOLD}--- Done ---${RESET}" echo -e " ${GREEN}✅ ${upgraded} updated${RESET} ${RED}❌ ${failed} failed${RESET}" else printf '{"upgraded":%d,"failed":%d}\n' "$upgraded" "$failed" fi