Skip to content

Instantly share code, notes, and snippets.

@davidakerr
Created January 7, 2026 20:06
Show Gist options
  • Select an option

  • Save davidakerr/674e4a69f895344b8febd166adf63b72 to your computer and use it in GitHub Desktop.

Select an option

Save davidakerr/674e4a69f895344b8febd166adf63b72 to your computer and use it in GitHub Desktop.
#!/usr/bin/env bash
# Pullio Without Tmp Files and Notifications
COMPOSE_BINARY="${COMPOSE_BINARY:-$(which 'docker-compose')}"
DOCKER_BINARY="${DOCKER_BINARY:-$(which 'docker')}"
TAG=""
DEBUG=""
PARALLEL=1
COMPOSE_TYPE="NONE"
# Check for V1
if [[ -n "${COMPOSE_BINARY}" ]]; then
COMPOSE_TYPE="V1"
# If V1 is not found, check for V2
elif [[ -n "${DOCKER_BINARY}" ]] && docker compose version &>/dev/null; then
COMPOSE_TYPE="V2"
fi
while [ "$1" != "" ]; do
PARAM=$(printf "%s\n" $1 | awk -F= '{print $1}')
VALUE=$(printf "%s\n" $1 | sed 's/^[^=]*=//g')
if [[ $VALUE == "$PARAM" ]]; then
shift
VALUE=$1
fi
case $PARAM in
--tag)
[[ -n $VALUE ]] && [[ $VALUE != "--"* ]] && TAG=".$VALUE"
;;
--debug)
[[ $VALUE != "--"* ]] && DEBUG="${VALUE:-debug}"
;;
--parallel)
[[ $VALUE =~ ^[0-9]+$ ]] && PARALLEL=$VALUE
;;
esac
shift
done
echo "Running with \"DEBUG=$DEBUG\", \"TAG=$TAG\", and \"PARALLEL=$PARALLEL\"."
# Setups the environment variables
setup_environment() {
# Always export these variables
export COMPOSE_BINARY DOCKER_BINARY TAG DEBUG COMPOSE_TYPE
# Export functions and additional variables only for parallel execution
if [ "$PARALLEL" -gt 1 ]; then
export -f process_container compose_pull_wrapper compose_up_wrapper
export -f export_env_vars
export sum
fi
}
compose_pull_wrapper() {
cd "$1" || exit 1
case $COMPOSE_TYPE in
"V1")
"${COMPOSE_BINARY}" pull "$2"
;;
"V2")
"${DOCKER_BINARY}" compose pull "$2"
;;
"NONE")
if [[ -n "${DOCKER_BINARY}" ]]; then
"${DOCKER_BINARY}" run --rm -v /var/run/docker.sock:/var/run/docker.sock -v "$1:$1" -w="$1" linuxserver/docker-compose pull "$2"
else
echo "Error: Neither Docker Compose nor Docker binary is available. Cannot pull." >&2
return 1
fi
;;
esac
}
compose_up_wrapper() {
cd "$1" || exit 1
case $COMPOSE_TYPE in
"V1")
"${COMPOSE_BINARY}" up -d --always-recreate-deps "$2"
;;
"V2")
"${DOCKER_BINARY}" compose up -d --always-recreate-deps "$2"
;;
"NONE")
if [[ -n "${DOCKER_BINARY}" ]]; then
"${DOCKER_BINARY}" run --rm -v /var/run/docker.sock:/var/run/docker.sock -v "$1:$1" -w="$1" linuxserver/docker-compose up -d --always-recreate-deps "$2"
else
echo "Error: Neither Docker Compose nor Docker binary is available. Cannot bring up services." >&2
return 1
fi
;;
esac
}
export_env_vars() {
export PULLIO_CONTAINER=${1}
export PULLIO_IMAGE=${2}
export PULLIO_AVATAR=${3}
export PULLIO_OLD_IMAGE_ID=${4}
export PULLIO_NEW_IMAGE_ID=${5}
export PULLIO_OLD_VERSION=${6}
export PULLIO_NEW_VERSION=${7}
export PULLIO_OLD_REVISION=${8}
export PULLIO_NEW_REVISION=${9}
export PULLIO_COMPOSE_SERVICE=${10}
export PULLIO_COMPOSE_WORKDIR=${11}
export PULLIO_AUTHOR_URL=${13}
}
process_container() {
local container_name="$1"
echo "$container_name: Checking..."
image_name=$("${DOCKER_BINARY}" inspect --format='{{.Config.Image}}' "$container_name")
container_image_digest=$("${DOCKER_BINARY}" inspect --format='{{.Image}}' "$container_name")
docker_compose_service=$("${DOCKER_BINARY}" inspect --format='{{ index .Config.Labels "com.docker.compose.service" }}' "$container_name")
docker_compose_version=$("${DOCKER_BINARY}" inspect --format='{{ index .Config.Labels "com.docker.compose.version" }}' "$container_name")
docker_compose_workdir=$("${DOCKER_BINARY}" inspect --format='{{ index .Config.Labels "com.docker.compose.project.working_dir" }}' "$container_name")
old_opencontainers_image_version=$("${DOCKER_BINARY}" inspect --format='{{ index .Config.Labels "org.opencontainers.image.version" }}' "$container_name")
old_opencontainers_image_revision=$("${DOCKER_BINARY}" inspect --format='{{ index .Config.Labels "org.opencontainers.image.revision" }}' "$container_name")
pullio_update=$("${DOCKER_BINARY}" inspect --format='{{ index .Config.Labels "org.hotio.pullio'"${TAG}"'.update" }}' "$container_name")
pullio_script_update=($("${DOCKER_BINARY}" inspect --format='{{ index .Config.Labels "org.hotio.pullio'"${TAG}"'.script.update" }}' "$container_name"))
pullio_registry_authfile=$("${DOCKER_BINARY}" inspect --format='{{ index .Config.Labels "org.hotio.pullio'"${TAG}"'.registry.authfile" }}' "$container_name")
pullio_author_avatar=$("${DOCKER_BINARY}" inspect --format='{{ index .Config.Labels "org.hotio.pullio'"${TAG}"'.author.avatar" }}' "$container_name")
pullio_author_url=$("${DOCKER_BINARY}" inspect --format='{{ index .Config.Labels "org.hotio.pullio'"${TAG}"'.author.url" }}' "$container_name")
if [[ (-n $docker_compose_version) && ($pullio_update == true) ]]; then
if [[ -f $pullio_registry_authfile ]]; then
echo "$container_name: Registry login..."
jq -r .password <"$pullio_registry_authfile" | "${DOCKER_BINARY}" login --username "$(jq -r .username <"$pullio_registry_authfile")" --password-stdin "$(jq -r .registry <"$pullio_registry_authfile")"
fi
echo "$container_name: Pulling image..."
if ! compose_pull_wrapper "$docker_compose_workdir" "${docker_compose_service}"; then
echo "$container_name: Pulling failed!"
fi
image_digest=${DEBUG}$("${DOCKER_BINARY}" image inspect --format='{{.Id}}' "${image_name}")
new_opencontainers_image_version=$("${DOCKER_BINARY}" image inspect --format='{{ index .Config.Labels "org.opencontainers.image.version" }}' "$image_name")
new_opencontainers_image_revision=$("${DOCKER_BINARY}" image inspect --format='{{ index .Config.Labels "org.opencontainers.image.revision" }}' "$image_name")
if [[ "${image_digest}" != "$container_image_digest" ]] && [[ $pullio_update == true ]]; then
if [[ -n "${pullio_script_update[*]}" ]]; then
echo "$container_name: Stopping container..."
"${DOCKER_BINARY}" stop "${container_name}"
echo "$container_name: Executing update script..."
export_env_vars "$container_name" "${image_name}" "${pullio_author_avatar}" "${container_image_digest/sha256:/}" "${image_digest/sha256:/}" "${old_opencontainers_image_version}" "${new_opencontainers_image_version}" "${old_opencontainers_image_revision}" "${new_opencontainers_image_revision}" "${docker_compose_service}" "${docker_compose_workdir}" "${pullio_author_url}"
"${pullio_script_update[@]}"
fi
echo "$container_name: Updating container..."
if ! compose_up_wrapper "$docker_compose_workdir" "${docker_compose_service}"; then
echo "${container_name}: Updating container failed!"
fi
fi
fi
}
# Respect ctrl+c
trap 'exit 130' INT
# Setup the environment
sum="$(sha1sum "$0" | awk '{print $1}')"
declare -a containers
readarray -t containers < <("${DOCKER_BINARY}" ps --format '{{.Names}}' | sort -k1)
setup_environment
echo "Processing ${#containers[@]} containers with parallelism of $PARALLEL"
if [ "$PARALLEL" -gt 1 ]; then
printf '%s\n' "${containers[@]}" | xargs -P "$PARALLEL" -I {} bash -c 'process_container "$@"' _ {}
else
for container_name in "${containers[@]}"; do
process_container "$container_name"
done
fi
echo "Pruning docker images..."
"${DOCKER_BINARY}" image prune --force
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment