Skip to content

Instantly share code, notes, and snippets.

@mbierman
Last active November 1, 2025 21:39
Show Gist options
  • Select an option

  • Save mbierman/6cf22430ca0c2ddb699ac8780ef281ef to your computer and use it in GitHub Desktop.

Select an option

Save mbierman/6cf22430ca0c2ddb699ac8780ef281ef to your computer and use it in GitHub Desktop.
Update Docker containers on Fireawlla
#!/bin/bash
# version 3.3.1
# https://gist.github.com/mbierman/6cf22430ca0c2ddb699ac8780ef281ef
DOCKER=$(which docker)
image=$1
wait=2
# Parse -f flag (must come before the image name)
if [ "$1" = "-f" ]; then
FORCE=true
shift
fi
image=$1
case "$image" in
homebridge) container="homebridge/homebridge:latest" ;;
docker-notify) container="schlabbi/docker-notify:latest" ;;
unifi) container="jacobalberty/unifi:latest" ;;
ddns) container="oznu/cloudflare-ddns:latest" ;;
*)
echo -e "Container not supported\nUse \"homebridge\", \"docker-notify\", \"unifi\", or \"ddns\""
exit 1
;;
esac
echo -e " \n\nWant to update $container? \n\nPress any key to continue"
local_id=$(sudo "$DOCKER" images -q "$container")
echo "Pulling Container..."
# Pull the latest image and capture the output
PULL_OUTPUT=$(sudo "$DOCKER" pull "$container" 2>&1)
echo "$PULL_OUTPUT"
new_id=$(sudo "$DOCKER" images -q "$container")
SHOULD_RESTART=false
if [ "$FORCE" = true ]; then
echo "Force update enabled: proceeding with container recreation."
SHOULD_RESTART=true
elif [ "$local_id" != "$new_id" ]; then
echo "Update available: New image ID detected. Proceeding with container recreation."
SHOULD_RESTART=true
elif ! echo "$PULL_OUTPUT" | grep -q "Image is up to date"; then
echo "Update available: New layers or content pulled, even though image ID is unchanged. Proceeding with container recreation."
SHOULD_RESTART=true
fi
# Only proceed with the rest of the script (update) if a change was detected
if [ "$SHOULD_RESTART" = true ]; then
echo running on "$(hostname)"...
# --- Firewalla Check and Exit (Handles UniFi on Firewalla) ---
if [ -d /home/pi/.firewalla ] || [ -d /data/firewalla ]; then
echo "Running on 🔥 Firewalla! Using docker-compose for $1."
FIREWALLA_DIR="/home/pi/.firewalla/run/docker/$1"
if [ ! -d "$FIREWALLA_DIR" ]; then
echo "Firewalla setup not found: Directory $FIREWALLA_DIR does not exist. Please set up the container manually first. Exiting."
exit 0
fi
if [ ! -f "$FIREWALLA_DIR/docker-compose.yaml" ]; then
echo "Firewalla setup not found: docker-compose.yml not found in $FIREWALLA_DIR. Please set up the container manually first. Exiting."
exit 0
fi
# Integrated user's prompt placement
read -rp "Do you want to begin? Press any key to continue: "
cd "$FIREWALLA_DIR" || exit
echo "Running docker-compose up for $1 (UniFi, Homebridge, etc.)..."
sudo docker-compose up -d --force-recreate # stop old container, recreate new one
sudo docker ps
# Optional: prune dangling images
repo_only="${container%%:*}"
echo "Removing dangling images for $repo_only..."
sudo docker image ls --filter "dangling=true" | grep "$repo_only" | awk '{print $3}' | xargs -r sudo docker image rm
SHOULD_RESTART=false
if $FORCE; then
echo "Force update enabled: proceeding with container recreation."
SHOULD_RESTART=true
elif [ "$local_id" != "$new_id" ]; then
echo "Update available: New image ID detected. Proceeding with container recreation."
SHOULD_RESTART=true
elif ! echo "$PULL_OUTPUT" | grep -q "Image is up to date"; then
echo "Update available: New layers or content pulled, even though image ID is unchanged. Proceeding with container recreation."
SHOULD_RESTART=true
fi
# Exit here, as docker-compose handles the full update and restart for Firewalla.
exit 0
fi
# --- END Firewalla Check ---
# Detect Synology DSM once
IS_SYNO=false
if [ -f /etc.defaults/VERSION ]; then
IS_SYNO=true
fi
# Before attempting to build arguments on Synology, check if the container exists at all.
# This prevents the script from attempting a first-time install if the user only wants to update.
if $IS_SYNO; then
EXISTING_CONTAINER_ID=$(sudo "$DOCKER" ps -a --filter "name=^$1$" --format '{{.ID}}')
if [ -z "$EXISTING_CONTAINER_ID" ]; then
echo "Synology setup not found: Container named '$1' does not exist (running or stopped). Please set up the container manually first. Exiting."
exit 0
fi
fi
if $IS_SYNO; then
echo "Running Synnology DSM-specific logic"
echo -e "\n\nchecking for new $container container..."
# 1. Inspect the current container once (used by homebridge to preserve user env)
# The '|| echo ""' prevents script failure if the container is not running.
current_env=$(sudo "$DOCKER" container inspect "$1" | jq -r '.[0].Config.Env[]' 2>/dev/null || echo "")
echo -e "\n\nPulling latest image for $container..."
sudo "$DOCKER" pull "$container"
# 2. Inspect the new image once (used by all container blocks)
info=$(sudo "$DOCKER" image inspect "$container")
echo "$info"
if [ "$1" = "homebridge" ]; then
# Extract new environment from the already inspected image info
new_env=$(echo "$info" | jq -r '.[0].Config.Env[]')
declare -A env_map
# Merge current environment safely (USER settings)
while IFS='=' read -r key value; do
[[ -n "$key" && "$key" != "$value" ]] || continue # skip empty/malformed lines
env_map["$key"]="$value"
done <<< "$current_env"
# Merge new environment safely (NEW IMAGE defaults)
# NOTE: New image defaults overwrite old user settings if keys conflict.
while IFS='=' read -r key value; do
[[ -n "$key" && "$key" != "$value" ]] || continue # skip empty/malformed lines
env_map["$key"]="$value"
done <<< "$new_env"
# Build the final --env arguments
args_env=()
for key in "${!env_map[@]}"; do
args_env+=(--env "$key=${env_map[$key]}")
done
args=( "${args_env[@]}" \
--name homebridge \
--hostname homebridge \
--volume /volume1/docker/homebridge:/homebridge:rw \
--network host \
--workdir /homebridge \
--restart always \
--log-driver db \
--runtime runc \
--detach \
"$container" )
# Conditionally add variable-based envs
# These lines use environment variables found in the image to set their own values.
[ -n "$S6_OVERLAY_VERSION" ] && args+=( --env="$S6_OVERLAY_VERSION" )
[ -n "$HOMEBRIDGE_APT_PKG_VERSION" ] && args+=( --env="$HOMEBRIDGE_APT_PKG_VERSION" )
printf 'Args: \n'
printf '%s\n' "${args[@]}"
elif [ "$1" = "docker-notify" ]; then
# These container sections extract specific versions from the 'info' variable
nodeVersion=$(echo "$info" | jq '.[0] .ContainerConfig .Env ' | \
sed -e 's/[,"]//g' | sed -e "s|\s*||g" | grep 'NODE_VERSION' | cut -f2 -d"=")
yarnVersion=$(echo "$info" | jq '.[0] .ContainerConfig .Env ' | \
sed -e 's/[,"]//g' | sed -e "s|\s*||g" | grep 'YARN_VERSION' | cut -f2 -d"=")
args=(\
--name=docker-notify \
--hostname=docker-notify \
--env=PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin \
--env=NODE_VERSION="$nodeVersion" \
--env=YARN_VERSION="$yarnVersion" \
--env=TZ=America/Los_Angeles \
--volume=/volume1/docker/docker-notify/config.json:/usr/src/app/config.json:rw \
--volume=/volume1/docker/docker-notify:/usr/src/app/cache:rw \
--network=host \
--workdir=/usr/src/app \
--restart=always \
--log-driver=db --detach=true -t schlabbi/docker-notify:latest /bin/sh -c 'node index.js')
elif [ "$1" = "unifi" ]; then
GOSU_VERSION=$(echo "$info" | jq '.[0] .Config .Env' | sed -e 's/[,"]//g' | grep 'GOSU_VERSION' | cut -f2 -d"=")
args=(\
--name=unifi \
--hostname=unifi \
--env=PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin \
--env=BASEDIR=/usr/lib/unifi \
--env=DATADIR=/unifi/data \
--env=LOGDIR=/unifi/log \
--env=CERTDIR=/unifi/cert \
--env=RUNDIR=/unifi/run \
--env=ORUNDIR=/var/run/unifi \
--env=ODATADIR=/var/lib/unifi \
--env=OLOGDIR=/var/log/unifi \
--env=CERTNAME=cert.pem \
--env=CERT_PRIVATE_NAME=privkey.pem \
--env=CERT_IS_CHAIN=false \
--env=GOSU_VERSION="$GOSU_VERSION" \
--env=BIND_PRIV=false \
--env=RUNAS_UID0=false \
--env=UNIFI_GID=999 \
--env=UNIFI_UID=999 \
--env=TZ=America/Los_Angeles \
--volume=/volume1/docker/UniFi:/unifi:rw \
--volume=/unifi \
--volume=/unifi/run \
--network=host \
--workdir=/unifi \
--restart=always \
--label='maintainer=Jacob Alberty <[email protected]>' \
--label='jacobalberty/unifi:latest=' \
--label='ghcr.io/jacobalberty/unifi-docker:latest=' \
--log-driver=db \
--runtime=runc \
--detach=true -t jacobalberty/unifi:latest unifi)
printf 'args: %s\n' "${args[@]}"
# Removed redundant image pull here
echo -e "\n\npulling new $container container"
elif [ "$1" = "ddns" ]; then
BASEDIR=$(dirname "$0")
# FIX: Add error suppression and use xargs to trim whitespace from key
API_KEY="$(cat "$BASEDIR"/updatedocker.txt 2>/dev/null | grep API | cut -f2 -d "=" | xargs)"
echo "$API_KEY"
# FIX: Check if the variable is empty (-z), not if it is a file
if [ -z "$API_KEY" ]; then
echo -e "\nSorry, no Cloudflair API key found. Add to $BASEDIR/updatedocker.txt"
exit 1
fi
args=(
--name=cloudflare-ddns \
--hostname=oznu-cloudflare-ddns \
--env=SUBDOMAIN=private \
--env=PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin \
--env=QEMU_ARCH=x86_64 \
--env=S6_KEEP_ENV=1 \
--env=API_KEY="${API_KEY}" \
--env=S6_BEHAVIOUR_IF_STAGE2_FAILS=2 \
--env=CF_API=https://api.cloudflare.com/client/v4 \
--env=RRTYPE=A \
--env='CRON=*/5 * * * *' \
--env=TZ=America/Los_Angeles \
--env=PROXIED=false \
--env=ZONE=thebiermans.net \
--network=host \
--restart=always \
--log-driver=db --runtime=runc --detach=true -t oznu/cloudflare-ddns:latest)
else
echo -e "Container not supported\nUse \"homebridge\", \"docker-notify\", \"unifi\" "
exit
fi
fi # Closes the IS_SYNO block
# Show the docker run arguments
printf 'args:\n'
printf '%s\n' "${args[@]}"
# Centralized stop/remove logic
echo -e "\nChecking for existing container '$1'..."
old_ids=$(sudo docker ps -a --filter "name=^$1$" --format '{{.ID}}')
if [ -n "$old_ids" ]; then
echo "Stopping existing container(s): $old_ids"
sudo docker stop $old_ids
echo "Removing existing container(s): $old_ids"
sudo docker rm $old_ids
# Wait until fully gone
while sudo docker ps -a --filter "name=^$1$" --format '{{.ID}}' | grep -q '.'; do
echo "Waiting for '$1' to be fully removed..."
sleep 1
done
else
echo "No existing container named '$1' found."
fi
# Run the new container with merged environment and runtime options
echo -e "\nStarting new container '$1'..."
sudo docker run "${args[@]}"
# --- Final Cleanup and Wait Logic (now properly inside the SHOULD_RESTART block) ---
# Clean up dangling images safely
echo "Removing old dangling images for $container ..."
repo_only="${container%%:*}"
sudo $DOCKER image ls --filter "dangling=true" | grep "$repo_only" | awk '{print $3}' | xargs -r sudo docker image rm
echo -e "\n please wait ($wait seconds) for the container to restart"
# Wait until the container reports "Up"
function finished () {
local cname="$1"
local ready=""
while [ "$ready" != "Up" ]; do
sleep "$wait"
ready=$(sudo docker ps --filter "name=^${cname}$" --format '{{.Status}}' | grep -o 'Up')
done
}
finished "$1"
# Remove the old image of the container (if any dangling images exist)
echo "Cleaning up dangling images for $container..."
repo_only="${container%%:*}"
sudo docker image ls --filter "dangling=true" | grep "$repo_only" | awk '{print $3}' | xargs -r sudo docker image rm
else
# If no update detected, exit cleanly.
echo "No update detected. Exiting without restart."
exit 0
fi
@mbierman
Copy link
Author

mbierman commented Dec 14, 2021

I use this to update various docker containers on several platforms so I don't have to remember how and I don't make any mistakes. To install, ssh to your Firewalla (or synology):

% cd ~/.firewalla/run/docker
% curl -o dockerupdate.sh https://gist.githubusercontent.com/mbierman/6cf22430ca0c2ddb699ac8780ef281ef/raw/57f0de8bd42a4dad39f1ed7fc5f746d53cedc7ed/updatedocker.sh
% chmod +x dockerupdate.sh

Then to update,

~/.firewalla/run/docker/dockerupdate.sh [docker_image_name]

Tested docker images:

  • unifi
  • homebridge

You will need to adjust this script a bit to use it on something other than a firewalla.

If you want to force an update even if there is no new image,

~/.firewalla/run/docker/dockerupdate.sh -f [docker_image_name]

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment