Introduction
Usually, Docker stacks on a system are all located within the same folder, e.g., /opt
. If these stacks are properly set up with a docker-compose.yml
file, a simple docker compose pull
followed by a docker compose up -d
will update the stack.
To have this done automatically for many stacks, I wrote the following script.
Code
Put the code below into a file, e.g., docker_update.sh
, and make it executable: chmod +x docker_update.sh
.
You can run the code in a folder that has subfolders with Docker Compose stacks, and it will run docker compose pull
in every folder where a Docker stack is detected. If a new image has been pulled, docker compose up -d
will be executed to update the stack. Some logging is also done and is available in the file docker_update.log
.
Ensure you have checked the script before running it in your environment!
Ensure your Docker stacks support automatic updates by, for example, defining proper versions of images.
Code of the script
#!/bin/bash
# Configuration
LOG_FILE="docker_update.log"
COMPOSE_FILES=("docker-compose.yml" "docker-compose.yaml" "compose.yml" "compose.yaml")
TIMEOUT=600 # Timeout in seconds for operations
# Function to log messages
log() {
local message="$1"
local timestamp=$(date "+%Y-%m-%d %H:%M:%S")
echo "[${timestamp}] ${message}" | tee -a "$LOG_FILE"
}
# Function to log error messages
log_error() {
local message="$1"
log "ERROR: $message"
echo "ERROR: $message"
}
# Add separator to log file when starting a new run
add_log_separator() {
echo "" >> "$LOG_FILE"
echo "=========================================" >> "$LOG_FILE"
echo "=== Docker Compose Update: $(date) ===" >> "$LOG_FILE"
echo "=========================================" >> "$LOG_FILE"
echo "" >> "$LOG_FILE"
}
# Check if required commands are available
check_requirements() {
for cmd in docker grep timeout; do
if ! command -v "$cmd" &>/dev/null; then
log_error "Required command '$cmd' not found"
exit 1
fi
done
if ! docker info &>/dev/null; then
log_error "Docker daemon is not running"
exit 1
fi
}
# Process a single directory
process_directory() {
local dir="$1"
local compose_file=""
local updated=false
local base_dir=$(pwd)
local temp_log=$(mktemp)
log "Processing directory: $dir"
# Find compose file
for file in "${COMPOSE_FILES[@]}"; do
if [ -f "$dir/$file" ]; then
compose_file="$file"
break
fi
done
if [ -z "$compose_file" ]; then
log "No Docker Compose files found in $dir"
return 0
fi
log "Found $compose_file in $dir"
# Change to directory
cd "$dir" || {
log_error "Could not enter directory $dir"
return 1
}
# Validate compose file
if ! timeout "$TIMEOUT" docker compose config -q &>/dev/null; then
log_error "Invalid compose configuration in $dir"
cd "$base_dir" || true
return 1
fi
# Run docker compose pull and capture output
log "Running 'docker compose pull' in $dir"
local pull_output=""
pull_output=$(timeout "$TIMEOUT" docker compose pull 2>&1)
local pull_status=$?
# Log the pull output
{
echo "--- Pull output for $dir ---"
echo "$pull_output"
echo "--- End pull output ---"
} >> "$LOG_FILE"
if [ $pull_status -ne 0 ]; then
log_error "Failed to pull images in $dir"
cd "$base_dir" || true
return 1
fi
# Check for image updates by parsing pull output
local changed=false
# Method 1: Look for keywords in pull output
if echo "$pull_output" | grep -qi -E '(Pulling from|Downloaded newer image|Image is up to date|Pull complete)'; then
# Method 2: Compare image digests before and after
local before_after_diff=""
docker compose images > "$temp_log.before" 2>/dev/null
# Run pull again to ensure we have latest tags
timeout "$TIMEOUT" docker compose pull > /dev/null 2>&1
docker compose images > "$temp_log.after" 2>/dev/null
# Compare before and after image digests
if ! diff -q "$temp_log.before" "$temp_log.after" > /dev/null 2>&1; then
changed=true
log "Image changes detected by diff comparison"
diff "$temp_log.before" "$temp_log.after" >> "$LOG_FILE" 2>&1
else
# Method 3: Force update if pull output contains specific pull indicators
if echo "$pull_output" | grep -qi -E '(Downloaded newer image|Pulling fs layer|Pull complete)'; then
changed=true
log "Image changes detected in pull output"
fi
fi
fi
# Always try to update if there's any indication of new images
if [ "$changed" = true ] || echo "$pull_output" | grep -qi -E '(Downloaded newer image|Pull complete)'; then
log "Changes detected, updating containers in $dir"
if timeout "$TIMEOUT" docker compose up -d 2>&1 | tee -a "$LOG_FILE"; then
log "Successfully updated containers in $dir"
updated=true
else
log_error "Failed to update containers in $dir"
cd "$base_dir" || true
rm -f "$temp_log.before" "$temp_log.after" "$temp_log"
return 1
fi
else
log "No changes detected for $dir, skipping update"
fi
cd "$base_dir" || true
rm -f "$temp_log.before" "$temp_log.after" "$temp_log"
if [ "$updated" = true ]; then
return 2 # Special return code for updated directories
else
return 0 # Success but no update
fi
}
# Main script
main() {
check_requirements
add_log_separator
log "Starting Docker Compose update process"
# Find all valid directories
local directories=()
# Include current directory if it has compose files
for file in "${COMPOSE_FILES[@]}"; do
if [ -f "$file" ]; then
directories+=(".")
break
fi
done
# Find subdirectories
while IFS= read -r -d $'\0' dir; do
dir=${dir#./} # Remove leading ./
if [ "$dir" != "." ] && [ -d "$dir" ]; then
for file in "${COMPOSE_FILES[@]}"; do
if [ -f "$dir/$file" ]; then
directories+=("$dir")
break
fi
done
fi
done < <(find . -maxdepth 1 -type d -not -path '*/\.*' -print0)
if [ ${#directories[@]} -eq 0 ]; then
log "No directories with Docker Compose files found"
return 0
fi
log "Found ${#directories[@]} directories with Docker Compose files to process"
for dir in "${directories[@]}"; do
log "- $dir"
done
# Process directories
local total_dirs=0 successful_dirs=0 updated_dirs=0 failed_dirs=0
for dir in "${directories[@]}"; do
((total_dirs++))
process_directory "$dir"
case $? in
0) ((successful_dirs++)) ;;
2) ((successful_dirs++)) && ((updated_dirs++)) ;;
*) ((failed_dirs++)) ;;
esac
done
# Generate summary
log "Summary: Processed $total_dirs directories"
log "Successfully processed: $successful_dirs"
log "Services updated: $updated_dirs"
log "Failed to process: $failed_dirs"
log "Docker Compose update process completed"
echo "Process completed. Check $LOG_FILE for details."
[ $failed_dirs -eq 0 ] || exit 1
}
# Execute main function
main