Update Docker stacks easily
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.
#!/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