#!/usr/bin/env bash # Usage: ./update_docker_images.sh [--prune] [--build] [compose-directory] # By default, pull non-buildable images and update services without pruning or building. # Continue after project failures; report them and exit nonzero at the end. # WARNING: Pruning removes stopped containers, unused images, build cache, # and unused anonymous volumes. set -euo pipefail usage() { printf 'Usage: %s [--prune] [--build] [compose-directory]\n' "$0" printf '%s\n' \ ' --prune Remove unused Docker resources before updating (including volumes).' \ ' --build Build images with --pull before updating services.' \ ' -h, --help Show this help.' \ 'Default: pull images for services without build definitions and update services.' \ 'Services with build definitions keep their local images unless --build is used.' \ 'Project failures are reported at the end; other projects are still updated.' } prune=false build=false positional=() while (( $# )); do case "$1" in --prune) prune=true ;; --build) build=true ;; -h|--help) usage; exit 0 ;; --) shift; positional+=("$@"); break ;; -*) printf 'Unknown option: %s\n' "$1" >&2; usage >&2; exit 2 ;; *) positional+=("$1") ;; esac shift done if (( ${#positional[@]} > 1 )); then usage >&2 exit 2 fi compose_root=${positional[0]:-/etc/docker/compose} if [[ ! -d "$compose_root" ]]; then printf 'Compose directory not found: %s\n' "$compose_root" >&2 exit 1 fi created_builder= cleanup() { local status=$? trap - EXIT if [[ -n "$created_builder" ]]; then if ! docker buildx rm "$created_builder"; then printf 'Could not remove builder: %s\n' "$created_builder" >&2 if (( status == 0 )); then status=1 fi fi fi exit "$status" } trap cleanup EXIT trap 'exit 130' INT trap 'exit 143' TERM if "$prune"; then docker system df docker system prune -a --volumes -f docker system df fi if "$build"; then # Select an existing container builder, not merely check whether one exists. # Capture the list separately so a failed Docker command stops the script. builders=$(docker buildx ls --format '{{.Name}} {{.Driver}}') builder= while read -r name driver; do if [[ "$driver" == docker-container ]]; then builder=$name break fi done <<< "$builders" if [[ -z "$builder" ]]; then # Bootstrap separately so cleanup can run if startup fails. created_builder=$(docker buildx create \ --buildkitd-config /etc/docker/buildx/buildkit.toml \ --driver docker-container \ --driver-opt 'network=treehouse') builder=$created_builder fi # Select the builder for this script without changing the user's default. export BUILDX_BUILDER="$builder" docker buildx inspect "$builder" --bootstrap fi failed_projects=() for directory in "$compose_root"/*; do [[ -d "$directory" ]] || continue [[ -f "$directory/docker-compose.yml" ]] || continue # Manually update Minecraft, so we're not always on the cutting edge [[ "$directory" == *minecraft* ]] && continue printf '\nUpdating %s\n' "$directory" # A subshell restores the working directory even when a command fails. # Because it is tested by 'if', explicitly stop on each failed command; # Bash does not apply 'set -e' inside this conditional subshell. if ( cd -- "$directory" || exit "$?" docker compose pull --ignore-buildable || exit "$?" if "$build"; then docker compose build --pull || exit "$?" fi # Do not let Compose implicitly build images when starting services. docker compose up -d --no-build ); then printf 'Updated %s\n' "$directory" else status=$? printf 'Update failed for %s (exit %s); continuing.\n' "$directory" "$status" >&2 failed_projects+=("$directory") fi done if (( ${#failed_projects[@]} )); then printf '\nFailed projects:\n' >&2 printf ' %s\n' "${failed_projects[@]}" >&2 exit 1 fi