#!/bin/bash # Docker Container Updater # # Description: Automatically updates Docker containers and manages Docker images # Features: # - Self-updating capability # - Updates all Docker Compose projects in /root/docker # - Skips containers with .ignore file # - Removes obsolete Docker Compose version attributes # - Cleans up unused Docker images # Author: ivanch # Version: 2.0 set -euo pipefail # Exit on error, undefined vars, and pipe failures #============================================================================== # CONFIGURATION #============================================================================== # Color definitions for output formatting readonly NC='\033[0m' readonly RED='\033[1;31m' readonly GREEN='\033[1;32m' readonly LIGHT_GREEN='\033[1;32m' readonly LIGHT_BLUE='\033[1;34m' readonly LIGHT_GREY='\033[0;37m' readonly YELLOW='\033[1;33m' # Script configuration readonly SCRIPT_NAME="docker-updater.sh" readonly SERVER_BASE_URL="https://git.ivanch.me/ivanch/server-scripts/raw/branch/main" readonly DOCKER_FOLDER="/root/docker" readonly COMPOSE_FILES=("docker-compose.yml" "docker-compose.yaml" "compose.yaml" "compose.yml") # Auto-update configuration readonly AUTO_UPDATE_ENABLED=true #============================================================================== # UTILITY FUNCTIONS #============================================================================== # Print formatted log messages log_info() { echo -e "${LIGHT_GREY}[i] $1${NC}"; } log_success() { echo -e "${LIGHT_GREEN}[✓] $1${NC}"; } log_warning() { echo -e "${YELLOW}[!] $1${NC}"; } log_error() { echo -e "${RED}[x] $1${NC}" >&2; } log_step() { echo -e "${LIGHT_BLUE}[i] $1${NC}"; } log_container() { echo -e "${LIGHT_BLUE}[$1] $2${NC}"; } # Exit with error message die() { log_error "$1" exit 1 } # Check if a command exists command_exists() { command -v "$1" >/dev/null 2>&1 } # Check if Docker and Docker Compose are available check_docker_requirements() { log_info "Checking Docker requirements..." if ! command_exists docker; then die "Docker is not installed or not in PATH" fi if ! docker compose version >/dev/null 2>&1; then die "Docker Compose is not available" fi log_success "Docker requirements satisfied" } # Get SHA256 hash of a file get_file_hash() { local file="$1" sha256sum "$file" 2>/dev/null | awk '{print $1}' || echo "" } # Get SHA256 hash from URL content get_url_hash() { local url="$1" curl -s "$url" 2>/dev/null | sha256sum | awk '{print $1}' || echo "" } # Check if server file is accessible check_server_connectivity() { local url="$1" curl -s --head "$url" | head -n 1 | grep -E "HTTP/[12] [23].." >/dev/null 2>&1 } #============================================================================== # AUTO-UPDATE FUNCTIONALITY #============================================================================== # Perform self-update if newer version is available perform_self_update() { if [[ "$AUTO_UPDATE_ENABLED" != "true" ]]; then log_info "Auto-update is disabled" return 0 fi local server_url="$SERVER_BASE_URL/$SCRIPT_NAME" log_step "Checking for script updates..." # Check if server file is accessible if ! check_server_connectivity "$server_url"; then log_warning "Cannot connect to update server, continuing with current version" return 0 fi # Compare local and server file hashes local local_hash local server_hash local_hash=$(get_file_hash "$SCRIPT_NAME") server_hash=$(get_url_hash "$server_url") if [[ -z "$local_hash" || -z "$server_hash" ]]; then log_warning "Cannot determine file hashes, skipping update" return 0 fi if [[ "$local_hash" != "$server_hash" ]]; then log_info "Update available, downloading new version..." # Create backup of current script local backup_file="${SCRIPT_NAME}.backup.$(date +%s)" cp "$SCRIPT_NAME" "$backup_file" || die "Failed to create backup" # Download updated script if curl -s -o "$SCRIPT_NAME" "$server_url"; then chmod +x "$SCRIPT_NAME" || die "Failed to set executable permissions" log_success "Script updated successfully" log_step "Running updated script..." exec ./"$SCRIPT_NAME" "$@" else # Restore backup on failure mv "$backup_file" "$SCRIPT_NAME" die "Failed to download updated script" fi else log_success "Script is already up to date" fi } #============================================================================== # DOCKER COMPOSE MANAGEMENT #============================================================================== # Find the active Docker Compose file in current directory find_compose_file() { for compose_file in "${COMPOSE_FILES[@]}"; do if [[ -f "$compose_file" ]]; then echo "$compose_file" return 0 fi done return 1 } # Remove obsolete version attribute from Docker Compose files clean_compose_files() { local container_name="$1" for compose_file in "${COMPOSE_FILES[@]}"; do if [[ -f "$compose_file" ]]; then log_container "$container_name" "Cleaning obsolete version attribute from $compose_file" sed -i '/^version:/d' "$compose_file" || log_warning "Failed to clean $compose_file" fi done } # Check if container should be skipped should_skip_container() { [[ -f ".ignore" ]] } # Check if any containers are running in current directory has_running_containers() { local running_containers running_containers=$(docker compose ps -q 2>/dev/null || echo "") [[ -n "$running_containers" ]] } # Update a single Docker Compose project update_docker_project() { local project_dir="$1" local container_name container_name=$(basename "$project_dir") log_container "$container_name" "Checking for updates..." # Change to project directory cd "$project_dir" || { log_error "Cannot access directory: $project_dir" return 1 } # Check if container should be skipped if should_skip_container; then log_container "$container_name" "Skipping (found .ignore file)" return 0 fi # Verify compose file exists local compose_file if ! compose_file=$(find_compose_file); then log_container "$container_name" "No Docker Compose file found, skipping" return 0 fi # Clean compose files clean_compose_files "$container_name" # Check if containers are running if ! has_running_containers; then log_container "$container_name" "No running containers, skipping update" return 0 fi # Stop containers log_container "$container_name" "Stopping containers..." if ! docker compose down >/dev/null 2>&1; then log_error "Failed to stop containers in $container_name" return 1 fi # Pull updated images log_container "$container_name" "Pulling updated images..." if ! docker compose pull -q >/dev/null 2>&1; then log_warning "Failed to pull images for $container_name, attempting to restart anyway" fi # Start containers log_container "$container_name" "Starting containers..." if ! docker compose up -d >/dev/null 2>&1; then log_error "Failed to start containers in $container_name" return 1 fi log_container "$container_name" "Update completed successfully!" return 0 } # Update all Docker Compose projects update_all_docker_projects() { log_step "Starting Docker container updates..." # Check if Docker folder exists if [[ ! -d "$DOCKER_FOLDER" ]]; then die "Docker folder not found: $DOCKER_FOLDER" fi # Change to Docker folder cd "$DOCKER_FOLDER" || die "Cannot access Docker folder: $DOCKER_FOLDER" local updated_count=0 local failed_count=0 local skipped_count=0 # Process each subdirectory for project_dir in */; do if [[ -d "$project_dir" ]]; then local project_path="$DOCKER_FOLDER/$project_dir" if update_docker_project "$project_path"; then if should_skip_container; then ((skipped_count++)) else ((updated_count++)) fi else ((failed_count++)) fi # Return to Docker folder for next iteration cd "$DOCKER_FOLDER" || die "Cannot return to Docker folder" fi done # Report results log_success "Docker update summary:" log_info " Updated: $updated_count projects" log_info " Skipped: $skipped_count projects" if [[ $failed_count -gt 0 ]]; then log_warning " Failed: $failed_count projects" fi } #============================================================================== # DOCKER CLEANUP #============================================================================== # Clean up unused Docker resources cleanup_docker_resources() { log_step "Cleaning up unused Docker resources..." # Remove unused images log_info "Removing unused Docker images..." if docker image prune -af >/dev/null 2>&1; then log_success "Docker image cleanup completed" else log_warning "Docker image cleanup failed" fi } #============================================================================== # MAIN EXECUTION #============================================================================== main() { log_step "Starting Docker Container Updater" echo # Check requirements check_docker_requirements # Perform self-update if enabled perform_self_update "$@" # Update all Docker projects update_all_docker_projects # Clean up Docker resources cleanup_docker_resources echo log_success "Docker container update process completed!" } # Execute main function with all arguments main "$@"