Created
March 31, 2026 12:28
-
-
Save ir31k0/c3903898173dc34c7d829875f2b1f7a6 to your computer and use it in GitHub Desktop.
This script checks for updates of the images for current installed Docker containers. Fork of https://gist.github.com/Eliastik/38e391183c137442403e4dc46d63ed26
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 | |
| # Original: https://gist.github.com/Eliastik/38e391183c137442403e4dc46d63ed26 | |
| # Author: Eliastik ( eliastiksofts.com/contact ) | |
| # Based on version 1.4.6 (22 february 2026) - Eliastik | |
| # Modified by: ir31k0 | |
| # | |
| # 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/ | |
| # | |
| # Modifications: | |
| # 31.03.2026: | |
| # - Changed Docker Hub host https://registry.hub.docker.com -> https://hub.docker.com | |
| # - Removed 'docker.io/' prefix from the image_name when fetching tags from Docker Hub | |
| # - Reworked the simplified output | |
| # - Format file (indent: 4) | |
| verbose=false | |
| ultra_verbose=false | |
| enable_simplified_output=false | |
| enable_major_versions=false | |
| enable_minor_versions=false | |
| tag_matching_regexp="^(v|[0-9\.])+(-[a-zA-Z]*)?$" | |
| pagination_limit=10 | |
| function check_command() { | |
| local cmd="$1" | |
| if ! command -v "$cmd" >/dev/null 2>&1; then | |
| echo "Error: $cmd is not installed. Please install it to run this script." >&2 | |
| exit 1 | |
| fi | |
| } | |
| check_command curl | |
| check_command jq | |
| # 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" = "-s" ]] || [[ "$argument" = "--simple" ]]; then | |
| enable_simplified_output=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] [-s]" | |
| 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 "s (--simple) Enable simplified output" | |
| 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) | |
| # Check if using Github Repo informations instead of Docker API | |
| github_repo=$(docker inspect --format='{{ index .Config.Labels "org.eliastik.checkUpdatesDocker.github_repo"}}' "$container") | |
| latest_tag="" | |
| latest_tag_json="" | |
| 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 [[ -n "$github_repo" ]]; then | |
| if [[ "$ultra_verbose" = true ]]; then | |
| echo "Using GitHub Releases API for $image_name (repo: $github_repo)" | |
| fi | |
| github_api_url="https://api.github.com/repos/$github_repo/releases/latest" | |
| latest_tag_curl_head=$(curl -s -o /dev/null -I -w "%{http_code}" "$github_api_url") | |
| latest_tag_json=$(curl -s "$github_api_url" | jq -r '.tag_name') | |
| latest_tag="$latest_tag_json" | |
| if [[ -z "$latest_tag" ]] || [[ "$latest_tag" == "null" ]]; then | |
| if [[ "$ultra_verbose" = true ]]; then | |
| echo "Could not retrieve latest release from GitHub for $github_repo" | |
| fi | |
| fi | |
| elif [[ "$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://hub.docker.com/v2/repositories/${image_name#docker.io/}/tags/?page_size=100") | |
| base_url="https://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 | |
| # Filter tags to keep only those with the same versioning scheme as the current tag | |
| # (same number of segments and same number of digits in the major version) | |
| # This prevents false positives when -mm is used with images using date-based or | |
| # incompatible versioning schemes (e.g. jellyfin 10.11.6 vs 2026020905, pihole 2025.11.1 vs v5.8.1) | |
| image_tag_clean=$(echo "$image_tag" | sed 's/^v//' | cut -d '-' -f 1) | |
| segment_count=$(echo "$image_tag_clean" | tr '.' '\n' | wc -l) | |
| image_tag_major_value=$(echo "$image_tag_clean" | cut -d '.' -f 1) | |
| major_digit_count=${#image_tag_major_value} | |
| segment_pattern="([0-9]+)" | |
| for i in $(seq 2 $segment_count); do | |
| segment_pattern="${segment_pattern}\.([0-9]+)" | |
| done | |
| latest_tag=$(echo "$latest_tag" | grep -E "^(v)?${segment_pattern}(-${image_flavour})?$" | while read -r tag; do | |
| tag_clean=$(echo "$tag" | sed 's/^v//' | cut -d '-' -f 1) | |
| tag_major=$(echo "$tag_clean" | cut -d '.' -f 1) | |
| if [ ${#tag_major} -eq $major_digit_count ]; then | |
| echo "$tag" | |
| fi | |
| done) | |
| 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 | |
| # Check if latest tag is higher than image_tag | |
| higher_tag=$(printf '%s\n' "$image_tag" "$latest_tag" | sort -Vr | head -n1) | |
| if [[ "$higher_tag" != "$image_tag" ]]; then | |
| if [[ "$enable_simplified_output" = true ]]; then | |
| echo "🆕 $container_name($image_name): $image_tag -> $latest_tag" | |
| else | |
| echo "The image $image_name needs to be updated for container $container_name. Current version: $image_tag, most recent version: $latest_tag." | |
| fi | |
| else | |
| if [[ "$verbose" = true ]] || [[ "$ultra_verbose" = true ]]; then | |
| if [[ "$enable_simplified_output" = true ]]; then | |
| echo "⚠️ $container_name($image_name): $image_tag -> $latest_tag (retrieved latest tag is older than the current version, consider increasing the pagination limit '-p')" | |
| else | |
| echo "/!\ Warning: the retrieved latest tag ($latest_tag) for image $image_name (container $container_name) is older than the current version ($image_tag). API data may be incomplete or paginated. Consider increasing the pagination limit (-p)." | |
| fi | |
| fi | |
| fi | |
| else | |
| if [[ "$verbose" = true ]] || [[ "$ultra_verbose" = true ]]; then | |
| if [[ "$enable_simplified_output" = true ]]; then | |
| echo "✅ $container_name($image_name): $image_tag" | |
| else | |
| echo "The image $image_name is up to date (version: $image_tag) for container $container_name." | |
| fi | |
| fi | |
| fi | |
| # Call EOL API | |
| image_app_name=$(echo "$image_name" | cut -d '/' -f 2) | |
| version_eol_base=$(echo "$image_tag" | cut -d '-' -f 1 | sed 's/^v//') | |
| version_eol_candidates=() | |
| if [[ "$version_eol_base" =~ ^([0-9]+)\.([0-9]+)\.([0-9]+)$ ]]; then | |
| version_eol_candidates=("${BASH_REMATCH[1]}.${BASH_REMATCH[2]}.${BASH_REMATCH[3]}" "${BASH_REMATCH[1]}.${BASH_REMATCH[2]}" "${BASH_REMATCH[1]}") | |
| elif [[ "$version_eol_base" =~ ^([0-9]+)\.([0-9]+)$ ]]; then | |
| version_eol_candidates=("${BASH_REMATCH[1]}.${BASH_REMATCH[2]}" "${BASH_REMATCH[1]}") | |
| elif [[ "$version_eol_base" =~ ^([0-9]+)$ ]]; then | |
| version_eol_candidates=("${BASH_REMATCH[1]}") | |
| fi | |
| eol_api_curl_response="" | |
| eol_api_curl_http_code=0 | |
| for version_eol in "${version_eol_candidates[@]}"; do | |
| 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") | |
| if [ "$eol_api_curl_http_code" -eq 200 ]; then | |
| eol_api_curl_response=$(curl -s "$eol_api_url" -L) | |
| break | |
| fi | |
| done | |
| 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 | |
| if [[ "$enable_simplified_output" = true ]]; then | |
| echo "⚠️ $container_name($image_name): $image_tag reached its end of life on $eol_date" | |
| else | |
| 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 | |
| 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