Are you using Docker on a Linux host?

Are you deploying your containers via Docker-Compose?

Do more than one of your containers/stacks use the same image (such as nginx:latest)?

Is constantly running docker-compose up -d --force-recreate --always-recreate-deps --build --remove-orphans to check & update seem off to you as it will redeploy (interrupt services) if the image is or is not updated?

This ‘bash script to check and update docker stack and container images’ might just be for you!

It will check each image that is currently deployed in a Stack, or just the image deployed for a Docker Container, from a docker-compose deploy.

You will need:

  • A Linux host with Docker and Docker Compose successfully installed
  • A ‘docker-compose.yml‘ file that generates a fully working docker container or stack

If you meet the requirements, then follow these steps to create the following file, and then make a “crontab -e” entry for this file with a frequency to execute every day/week/month so that the check/update is made and your services stay up-to-date with minimal interruption.

A suggested crontab entry that runs every 6 hours (4 times a day) might look like this:

19 */6 * * * ~/dockerenvs/dmz/reverseproxy/deploy-container.sh

Step 1 – Create a file named “deploy-container.sh” in your Docker Container / Stack folder

In this, generally, we create our container/stack’s docker-compose.yml file in a folder for that container/stack. This is best practice, and I would suggest that if you are not already tracking each docker stack as its own repo, you at least organize the folders of stacks into a logical pattern (environment such as Development/Production/DMZ) and then in each of Env folder, you have each Docker Stack as it own folder – so in your DMZ folder, you might have your “Nginx Reverse Proxy” folder, and in that folder, the docker-compose.yml that builds your Nginx Reverse Proxy for your DMZ. It is next to each docker-compose.yml file that you will put this “deploy-container.sh” file.

Step 2 – Set the file to execute

A “chmod +x deploy-container.sh” should do the trick here.

Step 3 – Edit and paste the example script

You should be able to either nano/vim the file or simply open the file in an editor, and paste the following example code:

#!/bin/bash
set -eo pipefail
# Get the name of the current folder (stack)
STACK_NAME=$(basename "$(pwd)")
echo "Starting image update check for stack ${STACK_NAME}..."

# Create an array of all container ids for this stack and get their image names and hashes
declare -a IMAGE_NAMES
declare -a IMAGE_HASHES
declare -a CONTAINER_IDS
mapfile -t CONTAINER_IDS < <(docker-compose ps -q -a)
for CONTAINER_ID in "${CONTAINER_IDS[@]}"; do
    echo "Getting image hash for container ${CONTAINER_ID}"
    IMAGE_NAME=$(docker inspect --format '{{ .Config.Image }}' "$CONTAINER_ID")
    IMAGE_NAMES+=("$IMAGE_NAME")
    IMAGE_HASH=$(docker inspect --format '{{ .Image }}' "$CONTAINER_ID")
    IMAGE_HASHES+=("$IMAGE_HASH")
done
echo "At stage 01 - Image names and hashes retrieved."

# Get the list of latest images from Docker Hub and compare their hashes with current container image hashes
declare -a LATEST_HASHES
for (( i = 0; i < ${#CONTAINER_IDS[@]}; ++i )); do
    #DOCKER_NAME=$(echo "$IMAGE" | awk -F: '{print $1":"$2}')
    IMAGE_NAME=${IMAGE_NAMES[$i]}
    echo "Checking image ${IMAGE_NAME}"
    
    # Pull the latest version of the image if needed, suppress output to avoid clutter in logs
    if ! docker pull "$IMAGE_NAME" > /dev/null 2>&1; then
        echo "Failed to pull image ${IMAGE_NAME}"
        continue
    fi
    
    # Get the latest hash of the image
    LATEST_HASH=$(docker image inspect "$IMAGE_NAME" --format '{{ .Id }}')
    LATEST_HASHES+=("$LATEST_HASH")
done
echo "At stage 02 - Latest image hashes retrieved."

# Compare the current and latest image hashes, set a value to re-run docker compose if any images are newer
UPDATE_PRESENT=0
for (( i = 0; i < ${#CONTAINER_IDS[@]}; ++i )); do
    CURRENT=${IMAGE_HASHES[$i]}
    LATEST=${LATEST_HASHES[$i]}
    
    # Check if current image is up-to-date, otherwise set update present flag
    if [[ $CURRENT != "$LATEST" ]]; then
        echo "Image ${IMAGE} has been updated!"
        UPDATE_PRESENT=1
    fi

    # Output no update
    if [[ $CURRENT == "$LATEST" ]]; then
        echo "Container ${CONTAINER_IDS[$i]} with Image ${IMAGE_NAMES[$i]} check:"
        echo "Current: ${CURRENT}"
        echo "Lastest: ${LATEST}"
    fi
done
echo "At stage 03 - Image updates checked."

# Restart the stack to use the updated images if there were any updates
if [ $UPDATE_PRESENT -eq 1 ]; then
    echo "Updated images for stack: $STACK_NAME - redeploying..."
    docker-compose up -d --force-recreate --always-recreate-deps --build --remove-orphans
else
    echo "Images and Containers are up to date."
fi
echo "Image update check completed."

The script when ran will get each container image name, then get the sha256 hash value for that container’s image.

It will then “docker pull ...” each image, and compare the hash from the image in use verses the image of the latest we just pulled, if they are different, it will set a variable that would normally stay “0” to “1” and then turn on the run of the docker stack re-deploy at the end.

Now you will always have the latest images, and docker containers running without having them re-deploy unless they need to be!

1 Comment

  1. Jonny5

    Posted this on Reddit and I’m discovering that (still need to prove it to myself it works as intended) if I don’t have the ‘–force-recreate’ in the flags Docker Compose will only re-deploy IF one of the images is updated. This might make the whole of the script unnecessary… or not as necessary anyway. Keeping all of this out there so if someone else wants to learn, they have the whole story to educate from.

Leave a comment

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.