Last active
May 4, 2025 20:06
-
-
Save Eliastik/38e391183c137442403e4dc46d63ed26 to your computer and use it in GitHub Desktop.
This script checks for updates of the images for current installed Docker containers.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/bin/bash | |
# Filename: check-updates-docker.sh | |
# | |
# Author: Eliastik ( eliastiksofts.com/contact ) | |
# Version 1.4 (4 may 2025) - Eliastik | |
# | |
# Description: This script checks for updates of the images for current installed Docker containers. | |
# This script doesn't need root access as long as you are running the Docker daemon in rootless mode | |
# or you followed the "Manage Docker as a non-root user" in the Docker documentation: https://docs.docker.com/engine/install/linux-postinstall/ | |
# | |
# Changelog: | |
# Version 1.4 (4 may 2025): | |
# - Added support for API pagination to fetch all tags for images with many tags. | |
# - Introduced a pagination limit configurable via the -p (or --pagination) argument (default: 10). | |
# Version 1.3.2 (4 may 2025): | |
# - Fix endoflife.date API call due to API update | |
# Version 1.3.1 (17 nov 2024): | |
# - Display container names when updates are available | |
# - Fix issue with the GitHub Packages API not returning certain tags, which resulted in displaying an incorrect latest version | |
# Version 1.3 (13 july 2024): | |
# - Check outdated container version EOL with endoflife.date API | |
# Version 1.2.1 (25 march 2024): | |
# - Added support for Gitlab Registry | |
# - Allow to disable update checking for certain containers, by adding the label value "org.eliastik.checkUpdatesDocker.disabled" in the container configuration to "True" | |
# - Minor fixes | |
# Version 1.2 (24 march 2024): | |
# - Enable version checking for images with tags ending with for example "-alpine" or other names | |
# - Enable version detection for images from Github Packages and Google Container Registry | |
# Version 1.1 (15 april 2023): | |
# - By default, the command only checks for patch versions upgrades. To enable checking for major/minor version | |
# you have to run the command with -mm or -m argument | |
# - Added help for the command | |
# | |
# Version 1.0 (17 march 2023): | |
# - Initial version | |
verbose=false | |
ultra_verbose=false | |
enable_major_versions=false | |
enable_minor_versions=false | |
tag_matching_regexp="^(v|[0-9\.])+(-[a-zA-Z]*)?$" | |
pagination_limit=10 | |
# Parse arguments on command line | |
for argument in "$@" | |
do | |
if [[ "$argument" = "-v" ]] || [[ "$argument" = "--verbose" ]]; then | |
verbose=true | |
fi | |
if [[ "$argument" = "-vvv" ]] || [[ "$argumenet" = "--ultra-verbose" ]]; then | |
verbose=true | |
ultra_verbose=true | |
fi | |
if [[ "$argument" = "-mm" ]] || [[ "$argument" = "--major" ]]; then | |
enable_major_versions=true | |
enable_minor_versions=true | |
fi | |
if [[ "$argument" = "-m" ]] || [[ "$argument" = "--minor" ]]; then | |
enable_minor_versions=true | |
fi | |
if [[ "$argument" =~ ^--pagination=([0-9]+)$ ]] || [[ "$argument" =~ ^-p([0-9]+)$ ]]; then | |
pagination_limit="${BASH_REMATCH[1]}" | |
fi | |
if [[ "$argument" = "-h" ]] || [[ "$argument" = "--help" ]]; then | |
name=$(basename "$0") | |
echo "Check updates for your Docker containers - by Eliastik (eliastiksofts.com)" | |
echo | |
echo "Syntax: $name [-v] [-vvv] [-mm] [-m] [-pN]" | |
echo "options:" | |
echo "v (--verbose) Output more verboses messages when running the command" | |
echo "vvv (--ultra-verbose) Output debug messages" | |
echo "mm (--major) Enable major/minor versions checking" | |
echo "m (--minor) Enable minor versions checking" | |
echo "pN (--pagination=N) Limit the number of API pagination requests to N (default: 10, set 0 for unlimited)" | |
echo | |
echo "Note: by default the command only checks for patch versions upgrades" | |
exit 0 | |
fi | |
done | |
# Get all containers currently running on the system | |
containers=$(docker container list --format '{{.ID}}') | |
# For each container | |
for container in $containers; do | |
# Get full image name | |
image=$(docker inspect --format='{{.Config.Image}}' "$container") | |
# Get container name | |
container_name=$(docker inspect --format='{{.Name}}' "$container" | sed 's|^/||') | |
# Get the update checking disabling configuration value | |
check_disabled=$(docker inspect --format='{{ index .Config.Labels "org.eliastik.checkUpdatesDocker.disabled"}}' "$container") | |
# Get image name | |
image_name=$(echo "$image" | cut -d ':' -f1) | |
# Get image tag | |
image_tag=$(echo "$image" | cut -d ':' -f2) | |
# Get image flavour (for example "alpine") | |
image_flavour=$(echo "$image_tag" | rev | cut -s -d '-' -f 1 | rev) | |
if [[ "$image_name" != */* ]]; then | |
image_name="library/$image_name" | |
fi | |
# If the update checking is disabled for the current container, we skip the checking | |
if [[ "$check_disabled" = "True" ]]; then | |
if [[ "$ultra_verbose" = true ]]; then | |
echo "The image $image_name was ignored, because the update version checking was disabled for the container" | |
fi | |
continue | |
fi | |
# If the image tag is not a version number | |
if [[ ! "$image_tag" =~ $tag_matching_regexp ]]; then | |
if [[ "$ultra_verbose" = true ]]; then | |
echo "The image $image_name was ignored, because its version contains something other than numbers and dots ($image_tag)" | |
fi | |
continue | |
fi | |
# Check the most recent image version using the Github Packages API, Gitlab Registry API, Google Container Registry API or Docker Hub API | |
# Assuming the image is public | |
token="" | |
url="" | |
base_url="" | |
if [[ "$image_name" == ghcr.io* ]]; then | |
# Github Packages API | |
image_name_github_package=${image_name#ghcr.io/} | |
token=$(curl -s "https://ghcr.io/token?scope=repository:$image_name_github_package:pull" | jq -r '.token') | |
url=$(echo "https://ghcr.io/v2/$image_name_github_package/tags/list?n=1000") | |
base_url="https://ghcr.io" | |
latest_tag_curl_head=200 # HEAD is forbidden for this API | |
elif [[ "$image_name" == registry.gitlab.com* ]]; then | |
# Gitlab Registry API | |
image_name_gitlab_registry=${image_name#registry.gitlab.com/} | |
token=$(curl -s "https://gitlab.com/jwt/auth?scope=repository:${image_name_gitlab_registry}:pull&service=container_registry" | jq -r '.token') | |
url=$(echo "https://registry.gitlab.com/v2/$image_name_gitlab_registry/tags/list?n=1000") | |
base_url="https://registry.gitlab.com" | |
latest_tag_curl_head=200 # HEAD is forbidden for this API | |
elif [[ "$image_name" == gcr.io* ]]; then | |
# Google Container Registry API | |
image_name_google_registry=${image_name#gcr.io/} | |
url=$(echo "https://gcr.io/v2/$image_name_google_registry/tags/list?n=1000") | |
base_url="https://gcr.io" | |
latest_tag_curl_head=200 # HEAD is forbidden for this API | |
else | |
# Docker Hub API | |
url=$(echo "https://registry.hub.docker.com/v2/repositories/$image_name/tags/?page_size=100") | |
base_url="https://registry.hub.docker.com" | |
latest_tag_curl_head=$(curl -s -o /dev/null -I -w "%{http_code}" "$url") # HEAD request | |
fi | |
# If the API returns an error response | |
if [[ "$latest_tag_curl_head" != 200 ]]; then | |
if [[ "$ultra_verbose" = true ]]; then | |
echo "The image $image_name was ignored, because the request to the Docker API returned an incorrect HTTP response (HTTP code $latest_tag_curl_head)" | |
fi | |
continue | |
fi | |
# Get all image tags from the APIs | |
page_count=0 | |
latest_tag="" | |
while [ -n "$url" ]; do | |
if [[ -n "$token" ]]; then | |
latest_tag_curl=$(curl -s -i -H "Authorization: Bearer $token" "$url") | |
else | |
latest_tag_curl=$(curl -s -i "$url") | |
fi | |
latest_tag_response_body=$(echo "$latest_tag_curl" | awk 'f; /^[[:space:]]*\r?$/ {f=1}') | |
if [[ "$image_name" == ghcr.io* ]] || [[ "$image_name" == registry.gitlab.com* ]]; then | |
# Github Packages API or Gitlab Registry API | |
latest_tag_json=$(echo "$latest_tag_response_body" | jq -r '.tags[]') | |
elif [[ "$image_name" == gcr.io* ]]; then | |
# Google Container Registry API | |
latest_tag_json=$(echo "$latest_tag_response_body" | jq -r '.manifest | .[].tag[]') | |
else | |
# Docker Hub API | |
latest_tag_json=$(echo "$latest_tag_response_body" | jq -r '.results[].name') | |
fi | |
# If there was an error parsing the JSON response | |
if [ -z "$latest_tag_json" ]; then | |
break | |
fi | |
# Filter the tags based on the current tag of the image (major version) | |
filtered_tags=$(echo "$latest_tag_json" | grep -v -E '^(latest|edge)$' | grep -E "^(v|[0-9\.])+(-${image_flavour})?$") | |
latest_tag="$latest_tag"$'\n'"$filtered_tags" | |
# Pagination | |
page_count=$((page_count + 1)) | |
if [ "$pagination_limit" -gt 0 ] && [ "$page_count" -ge "$pagination_limit" ]; then | |
if [[ "$ultra_verbose" = true ]]; then | |
echo "Pagination limit of $pagination_limit reached, stopping further API calls for image $image_name." | |
fi | |
break | |
fi | |
next_url=$(echo "$latest_tag_curl" | grep -i '^Link:' | sed -n 's/.*<\([^>]*\)>; *rel="next".*/\1/p') | |
if [ -n "$next_url" ]; then | |
url="$base_url$next_url" | |
else | |
url="" | |
fi | |
done | |
if [ -z "$latest_tag_json" ]; then | |
if [[ "$ultra_verbose" = true ]]; then | |
echo "The image $image_name was ignored, because the request to the Docker API returned an incorrect or empty JSON response" | |
fi | |
continue | |
fi | |
if [[ "$enable_major_versions" = false ]]; then | |
if [[ "$enable_minor_versions" = true ]]; then | |
image_tag_major=$(echo "$image_tag" | cut -d '-' -f 1 | cut -d '.' -f 1) | |
else | |
image_tag_major=$(echo "$image_tag" | cut -d '-' -f 1 | cut -d '.' -f 1,2) | |
fi | |
# Filter the tags based on the version of the image (minor or patch version) | |
latest_tag=$(echo "$latest_tag" | grep -E "^(v)?${image_tag_major}\.[0-9\.]+(-${image_flavour})?$") | |
fi | |
# Sort latest version tag first | |
latest_tag=$(echo "$latest_tag" | sort -Vr | head -n1) | |
# Check if the latest version tag is the same as the image tag ; if not, there is an update available | |
if [ -n "$latest_tag" ] && [[ "$image_tag" != "$latest_tag" ]]; then | |
echo "The image $image_name needs to be updated for container $container_name. Current version: $image_tag, most recent version: $latest_tag." | |
else | |
if [[ "$verbose" = true ]] || [[ "$ultra_verbose" = true ]]; then | |
echo "The image $image_name is up to date (version: $image_tag) for container $container_name." | |
fi | |
fi | |
# Call EOL API | |
image_app_name=$(echo "$image_name" | cut -d '/' -f 2) | |
version_eol=$(echo "$image_tag" | cut -d '-' -f 1) | |
if [[ "$version_eol" =~ ^([0-9]+)\.([0-9]+)\.[0-9]+$ ]]; then | |
version_eol=$(echo "${BASH_REMATCH[1]}.${BASH_REMATCH[2]}") | |
elif [[ "$version_eol" =~ ^([0-9]+)\.([0-9]+)$ ]]; then | |
version_eol=$(echo "${BASH_REMATCH[1]}") | |
fi | |
eol_api_url="https://endoflife.date/api/v1/products/$image_app_name/releases/$version_eol" | |
eol_api_curl_http_code=$(curl -s -o /dev/null -I -L -w "%{http_code}" "$eol_api_url") # HEAD request | |
if [ "$eol_api_curl_http_code" -eq 200 ]; then | |
eol_api_curl_response=$(curl -s "$eol_api_url" -L) | |
eol_date=$(echo "$eol_api_curl_response" | jq -r '.result.eolFrom') | |
is_eol=$(echo "$eol_api_curl_response" | jq -r '.result.isEol') | |
if [ "$is_eol" == "true" ]; then | |
current_date=$(date +%Y-%m-%d) | |
current_date_epoch=$(date -d "$current_date" +%s) | |
eol_date_epoch=$(date -d "$eol_date" +%s) | |
if [ "$eol_date_epoch" -lt "$current_date_epoch" ]; then | |
echo "/!\ The version of the image $image_name ($image_tag) for the container $container_name has reached its end of life on $eol_date and will not be updated anymore. Please upgrade the image to a new major version." | |
fi | |
fi | |
else | |
if [[ "$ultra_verbose" = true ]]; then | |
echo "Cannot retrieve end of life data for image $image_name." | |
fi | |
fi | |
done |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment