add docker update script
This commit is contained in:
Executable
+241
@@ -0,0 +1,241 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# update-docker.sh — pull/build the latest images for every app under docker/,
|
||||||
|
# recreate only the containers whose image changed, validate they come back
|
||||||
|
# healthy, and run Nextcloud's post-upgrade occ steps. Prints a per-app summary
|
||||||
|
# (updated / no update / failed) and exits non-zero if anything failed.
|
||||||
|
#
|
||||||
|
# In-place by design: it never runs a global `docker compose down`, because the
|
||||||
|
# `shared` network is created by the nginx compose file and every other app
|
||||||
|
# joins it as external. Tearing it all down at once would break that network and
|
||||||
|
# cause needless downtime. `docker compose up -d` recreates only the changed
|
||||||
|
# containers and leaves the rest (and the network) alone.
|
||||||
|
#
|
||||||
|
# Usage:
|
||||||
|
# ./update-docker.sh # update every app under docker/
|
||||||
|
# ./update-docker.sh nextcloud nginx # update only the named apps
|
||||||
|
# ./update-docker.sh --prune # also reclaim dangling images afterward
|
||||||
|
#
|
||||||
|
# Env overrides:
|
||||||
|
# DOCKER_DIR=/path/to/docker # default: ../docker relative to this script
|
||||||
|
# HEALTH_TIMEOUT=180 # seconds to wait for containers to be healthy
|
||||||
|
#
|
||||||
|
# Runs on the server (Rocky Linux 9), where each app's .env is present so compose
|
||||||
|
# can resolve secrets. Run it from anywhere; it cd's into each app dir itself.
|
||||||
|
|
||||||
|
set -uo pipefail
|
||||||
|
|
||||||
|
SCRIPT_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)
|
||||||
|
DOCKER_DIR="${DOCKER_DIR:-$(cd "$SCRIPT_DIR/.." && pwd)/docker}"
|
||||||
|
HEALTH_TIMEOUT="${HEALTH_TIMEOUT:-180}"
|
||||||
|
|
||||||
|
PRUNE=false
|
||||||
|
REQUESTED=()
|
||||||
|
for arg in "$@"; do
|
||||||
|
case "$arg" in
|
||||||
|
--prune) PRUNE=true ;;
|
||||||
|
-*) echo "unknown option: $arg" >&2; exit 2 ;;
|
||||||
|
*) REQUESTED+=("$arg") ;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
# `docker compose pull --ignore-buildable` cleanly skips services with a build
|
||||||
|
# section (added in Compose v2.30). Without it, pulling minecraft would error on
|
||||||
|
# its local-only image tag — harmless here since we treat pull failures as
|
||||||
|
# non-fatal and rebuild buildable services anyway, but the flag keeps output clean.
|
||||||
|
PULL_FLAGS=""
|
||||||
|
if docker compose pull --help 2>/dev/null | grep -q -- '--ignore-buildable'; then
|
||||||
|
PULL_FLAGS="--ignore-buildable"
|
||||||
|
fi
|
||||||
|
|
||||||
|
UPDATED=()
|
||||||
|
NOUPDATE=()
|
||||||
|
FAILED=()
|
||||||
|
|
||||||
|
# Stable "ref=imageID" listing of every image the project resolves to, so we can
|
||||||
|
# tell whether a pull/build actually changed anything.
|
||||||
|
image_signature() {
|
||||||
|
local img id
|
||||||
|
while IFS= read -r img; do
|
||||||
|
[ -z "$img" ] && continue
|
||||||
|
id=$(docker image inspect -f '{{.Id}}' "$img" 2>/dev/null || echo '<none>')
|
||||||
|
printf '%s=%s\n' "$img" "$id"
|
||||||
|
done < <(docker compose config --images 2>/dev/null) | sort
|
||||||
|
}
|
||||||
|
|
||||||
|
compose_has_build() {
|
||||||
|
docker compose config 2>/dev/null | grep -qE '^[[:space:]]+build:'
|
||||||
|
}
|
||||||
|
|
||||||
|
# Wait until every container in the project is running, and (if it has a
|
||||||
|
# healthcheck) healthy. Returns non-zero on timeout.
|
||||||
|
validate_app() {
|
||||||
|
local deadline cid cname state health all_ok pending
|
||||||
|
deadline=$(( SECONDS + HEALTH_TIMEOUT ))
|
||||||
|
while :; do
|
||||||
|
all_ok=true
|
||||||
|
pending=""
|
||||||
|
while IFS= read -r cid; do
|
||||||
|
[ -z "$cid" ] && continue
|
||||||
|
cname=$(docker inspect -f '{{.Name}}' "$cid" 2>/dev/null | sed 's#^/##')
|
||||||
|
state=$(docker inspect -f '{{.State.Status}}' "$cid" 2>/dev/null)
|
||||||
|
health=$(docker inspect -f '{{if .State.Health}}{{.State.Health.Status}}{{else}}none{{end}}' "$cid" 2>/dev/null)
|
||||||
|
if [ "$state" != "running" ]; then
|
||||||
|
all_ok=false; pending+=" $cname=$state"
|
||||||
|
elif [ "$health" = "starting" ] || [ "$health" = "unhealthy" ]; then
|
||||||
|
all_ok=false; pending+=" $cname=health:$health"
|
||||||
|
fi
|
||||||
|
done < <(docker compose ps -a -q)
|
||||||
|
|
||||||
|
$all_ok && return 0
|
||||||
|
if [ "$SECONDS" -ge "$deadline" ]; then
|
||||||
|
echo " validation timed out after ${HEALTH_TIMEOUT}s:$pending"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
sleep 5
|
||||||
|
done
|
||||||
|
}
|
||||||
|
|
||||||
|
# Nextcloud post-upgrade steps.
|
||||||
|
#
|
||||||
|
# The official image's entrypoint already runs `occ upgrade` on start when it
|
||||||
|
# detects a version change, and holds the instance in maintenance mode while it
|
||||||
|
# does (https://github.com/nextcloud/docker README). The old upgrade.sh failed
|
||||||
|
# because it ran `occ upgrade` after a fixed `sleep 10` — i.e. while the
|
||||||
|
# entrypoint was still mid-upgrade. So here we first wait for maintenance mode to
|
||||||
|
# clear, then run occ ourselves.
|
||||||
|
#
|
||||||
|
# Exit codes from core/Command/Upgrade.php (current master): 0 = success AND
|
||||||
|
# "no upgrade required" (ERROR_UP_TO_DATE == 0), 2 = maintenance mode,
|
||||||
|
# 5 = failure. So a non-zero from `occ upgrade` is a genuine problem, not the
|
||||||
|
# benign "already up to date" it used to be in older versions (which returned 3).
|
||||||
|
nextcloud_occ() {
|
||||||
|
local rc=0 deadline
|
||||||
|
deadline=$(( SECONDS + HEALTH_TIMEOUT ))
|
||||||
|
echo " occ: waiting for the entrypoint's auto-upgrade / maintenance mode to clear"
|
||||||
|
while docker compose exec -T -u www-data nextcloud php occ maintenance:mode 2>/dev/null \
|
||||||
|
| grep -q 'currently enabled'; do
|
||||||
|
if [ "$SECONDS" -ge "$deadline" ]; then
|
||||||
|
echo " still in maintenance mode after ${HEALTH_TIMEOUT}s — aborting occ steps"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
sleep 5
|
||||||
|
done
|
||||||
|
|
||||||
|
# Safety-net upgrade. Normally a no-op (the entrypoint already migrated), so
|
||||||
|
# this should print "No upgrade required" and return 0. A non-zero exit is real.
|
||||||
|
echo " occ: upgrade"
|
||||||
|
docker compose exec -T -u www-data nextcloud php occ upgrade 2>&1 | sed 's/^/ /'
|
||||||
|
local up=${PIPESTATUS[0]}
|
||||||
|
if [ "$up" -ne 0 ]; then
|
||||||
|
echo " occ upgrade exited $up (2=maintenance, 5=failure) — see output above"
|
||||||
|
rc=1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Documented post-upgrade index optimization; idempotent and safe to repeat.
|
||||||
|
echo " occ: add missing DB indices"
|
||||||
|
docker compose exec -T -u www-data nextcloud php occ db:add-missing-indices 2>&1 | sed 's/^/ /'
|
||||||
|
[ "${PIPESTATUS[0]}" -ne 0 ] && rc=1
|
||||||
|
|
||||||
|
echo " occ: status"
|
||||||
|
docker compose exec -T -u www-data nextcloud php occ status 2>&1 | sed 's/^/ /'
|
||||||
|
[ "${PIPESTATUS[0]}" -ne 0 ] && rc=1
|
||||||
|
|
||||||
|
return $rc
|
||||||
|
}
|
||||||
|
|
||||||
|
update_app() {
|
||||||
|
local name="$1"
|
||||||
|
local dir="$DOCKER_DIR/$name"
|
||||||
|
|
||||||
|
if ! cd "$dir" 2>/dev/null; then
|
||||||
|
FAILED+=("$name (cannot cd into $dir)"); return
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "==> $name: checking for updates"
|
||||||
|
local before after
|
||||||
|
before=$(image_signature)
|
||||||
|
|
||||||
|
# Pull registry images. Lenient: a warning/error here (e.g. an image that is
|
||||||
|
# only built locally) shouldn't abort the run — validation catches real breakage.
|
||||||
|
docker compose pull $PULL_FLAGS 2>&1 | sed 's/^/ /'
|
||||||
|
|
||||||
|
if compose_has_build; then
|
||||||
|
echo " rebuilding local images against latest base"
|
||||||
|
docker compose build --pull 2>&1 | sed 's/^/ /'
|
||||||
|
if [ "${PIPESTATUS[0]}" -ne 0 ]; then
|
||||||
|
FAILED+=("$name (build failed)"); return
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
after=$(image_signature)
|
||||||
|
if [ "$before" = "$after" ]; then
|
||||||
|
echo " no update needed"
|
||||||
|
NOUPDATE+=("$name"); return
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo " new image(s) detected — recreating containers"
|
||||||
|
docker compose up -d 2>&1 | sed 's/^/ /'
|
||||||
|
if [ "${PIPESTATUS[0]}" -ne 0 ]; then
|
||||||
|
FAILED+=("$name (up -d failed)"); return
|
||||||
|
fi
|
||||||
|
|
||||||
|
if ! validate_app; then
|
||||||
|
FAILED+=("$name (unhealthy after update)"); return
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ "$name" = "nextcloud" ]; then
|
||||||
|
if ! nextcloud_occ; then
|
||||||
|
FAILED+=("$name (occ steps failed)"); return
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo " updated and validated"
|
||||||
|
UPDATED+=("$name")
|
||||||
|
}
|
||||||
|
|
||||||
|
# Build the work list: every dir under docker/ that has a compose file, with
|
||||||
|
# nginx first so the shared network owner is touched before its dependents.
|
||||||
|
all_apps=()
|
||||||
|
while IFS= read -r d; do
|
||||||
|
name=$(basename "$d")
|
||||||
|
if compgen -G "$d/compose.y*ml" >/dev/null || compgen -G "$d/docker-compose.y*ml" >/dev/null; then
|
||||||
|
all_apps+=("$name")
|
||||||
|
fi
|
||||||
|
done < <(find "$DOCKER_DIR" -mindepth 1 -maxdepth 1 -type d | sort)
|
||||||
|
|
||||||
|
if [ "${#REQUESTED[@]}" -gt 0 ]; then
|
||||||
|
# Validate requested names against what actually exists.
|
||||||
|
apps=()
|
||||||
|
for req in "${REQUESTED[@]}"; do
|
||||||
|
found=false
|
||||||
|
for a in "${all_apps[@]}"; do [ "$a" = "$req" ] && found=true; done
|
||||||
|
if $found; then apps+=("$req"); else
|
||||||
|
echo "no such app under docker/: $req" >&2; exit 2
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
else
|
||||||
|
apps=("${all_apps[@]}")
|
||||||
|
fi
|
||||||
|
|
||||||
|
# nginx first, the rest after.
|
||||||
|
ordered=()
|
||||||
|
for a in "${apps[@]}"; do [ "$a" = nginx ] && ordered+=("nginx"); done
|
||||||
|
for a in "${apps[@]}"; do [ "$a" != nginx ] && ordered+=("$a"); done
|
||||||
|
|
||||||
|
for name in "${ordered[@]}"; do
|
||||||
|
update_app "$name"
|
||||||
|
done
|
||||||
|
|
||||||
|
echo
|
||||||
|
echo "================ summary ================"
|
||||||
|
echo "updated: ${UPDATED[*]:-none}"
|
||||||
|
echo "no update: ${NOUPDATE[*]:-none}"
|
||||||
|
echo "failed: ${FAILED[*]:-none}"
|
||||||
|
|
||||||
|
if $PRUNE && [ "${#UPDATED[@]}" -gt 0 ]; then
|
||||||
|
echo
|
||||||
|
echo "pruning dangling images"
|
||||||
|
docker image prune -f 2>&1 | sed 's/^/ /'
|
||||||
|
fi
|
||||||
|
|
||||||
|
[ "${#FAILED[@]}" -eq 0 ]
|
||||||
Reference in New Issue
Block a user