added docker update scripts
This commit is contained in:
@@ -0,0 +1,41 @@
|
|||||||
|
# docker-stacks-update
|
||||||
|
|
||||||
|
Finds all running Docker Compose stacks, pulls the latest images, and
|
||||||
|
restarts only the stacks that received updates.
|
||||||
|
|
||||||
|
## Requirements
|
||||||
|
|
||||||
|
- **docker** (with Compose v2 plugin — `docker compose`, not `docker-compose`)
|
||||||
|
- **jq** — used to parse the JSON output of `docker compose ls`
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
Place the script anywhere in your `$PATH`, for example:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cp docker-stacks-update ~/.local/bin/
|
||||||
|
chmod +x ~/.local/bin/docker-stacks-update
|
||||||
|
```
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker-stacks-update
|
||||||
|
```
|
||||||
|
|
||||||
|
Run from any directory — the script finds every running stack
|
||||||
|
system-wide using `docker compose ls`.
|
||||||
|
|
||||||
|
## What it does
|
||||||
|
|
||||||
|
1. **Discover** — lists all running stacks with `docker compose ls
|
||||||
|
--format json`
|
||||||
|
2. **Pull** — runs `docker compose pull` for each stack
|
||||||
|
3. **Detect** — checks whether any service image was actually
|
||||||
|
downloaded (not just "already up to date")
|
||||||
|
4. **Restart** — recreates containers with `docker compose up -d` only
|
||||||
|
for stacks that received new images
|
||||||
|
5. **Orphan cleanup** — any stack whose compose file(s) no longer exist
|
||||||
|
on disk is torn down automatically
|
||||||
|
6. **Report** — prints a summary showing updated (with service names),
|
||||||
|
unchanged, orphaned (removed), and failed stacks
|
||||||
Executable
+218
@@ -0,0 +1,218 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
#
|
||||||
|
# docker-stacks-update - Pull latest images and restart all running
|
||||||
|
# Docker Compose stacks that have updates.
|
||||||
|
#
|
||||||
|
# Usage: docker-stacks-update
|
||||||
|
#
|
||||||
|
# Works from any directory; put it anywhere in $PATH.
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
# ── Terminal colours (disabled when not on a terminal) ──────────────
|
||||||
|
if [[ -t 1 ]]; then
|
||||||
|
GREEN='\033[0;32m'; YELLOW='\033[1;33m'; RED='\033[0;31m'
|
||||||
|
CYAN='\033[0;36m'; BOLD='\033[1m'; NC='\033[0m'
|
||||||
|
else
|
||||||
|
GREEN=''; YELLOW=''; RED=''; CYAN=''; BOLD=''; NC=''
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ── State ───────────────────────────────────────────────────────────
|
||||||
|
updated_names=() # stack names that were updated
|
||||||
|
updated_detail=() # parallel: "stack: service1, service2"
|
||||||
|
unchanged_names=() # stack names already up to date
|
||||||
|
orphaned_names=() # stacks whose compose files are gone – removed
|
||||||
|
failed_names=() # stack names that errored
|
||||||
|
|
||||||
|
# ── Helpers ─────────────────────────────────────────────────────────
|
||||||
|
info() { echo -e "${CYAN}::${NC} $*"; }
|
||||||
|
ok() { echo -e " ${GREEN}✓${NC} $*"; }
|
||||||
|
warn() { echo -e " ${YELLOW}~${NC} $*"; }
|
||||||
|
fail() { echo -e " ${RED}✗${NC} $*"; }
|
||||||
|
|
||||||
|
# ── Pre-flight ──────────────────────────────────────────────────────
|
||||||
|
if ! command -v docker &>/dev/null; then
|
||||||
|
echo -e "${RED}Error: 'docker' not found in PATH.${NC}" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo -e "${BOLD}Docker Compose Stack Updater${NC}"
|
||||||
|
echo -e "${BOLD}────────────────────────────${NC}"
|
||||||
|
echo
|
||||||
|
|
||||||
|
# ── Discover stacks ─────────────────────────────────────────────────
|
||||||
|
info "Scanning for running Docker Compose stacks ..."
|
||||||
|
echo
|
||||||
|
|
||||||
|
stack_json=$(docker compose ls --format json 2>/dev/null || true)
|
||||||
|
|
||||||
|
if [[ -z "$stack_json" ]]; then
|
||||||
|
warn "No running Docker Compose stacks found."
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Parse JSON with jq (required).
|
||||||
|
if ! command -v jq &>/dev/null; then
|
||||||
|
echo -e "${RED}Error: 'jq' is required. Install it first.${NC}" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
readarray -t names < <(echo "$stack_json" | jq -r '.[].Name')
|
||||||
|
readarray -t configs < <(echo "$stack_json" | jq -r '.[].ConfigFiles')
|
||||||
|
|
||||||
|
total=${#names[@]}
|
||||||
|
echo " Found ${total} running stack(s):"
|
||||||
|
printf " • %s\n" "${names[@]}"
|
||||||
|
echo
|
||||||
|
|
||||||
|
# ── Process each stack ──────────────────────────────────────────────
|
||||||
|
for i in "${!names[@]}"; do
|
||||||
|
stack="${names[$i]}"
|
||||||
|
cfg_str="${configs[$i]}"
|
||||||
|
|
||||||
|
echo -e "${BOLD}── ${stack}${NC}"
|
||||||
|
|
||||||
|
# Support multi-file projects (ConfigFiles is comma-separated).
|
||||||
|
IFS=',' read -ra cfg_files <<< "$cfg_str"
|
||||||
|
compose_args=()
|
||||||
|
compose_dir=""
|
||||||
|
for f in "${cfg_files[@]}"; do
|
||||||
|
f_trimmed=$(echo "$f" | xargs)
|
||||||
|
compose_args+=(-f "$f_trimmed")
|
||||||
|
# Use the directory of the first config file as working directory.
|
||||||
|
if [[ -z "$compose_dir" ]]; then
|
||||||
|
compose_dir=$(dirname "$f_trimmed")
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
# Check whether the compose files still exist (orphan detection).
|
||||||
|
missing_files=()
|
||||||
|
for f in "${compose_args[@]}"; do
|
||||||
|
[[ "$f" == "-f" ]] && continue
|
||||||
|
[[ ! -e "$f" ]] && missing_files+=("$f")
|
||||||
|
done
|
||||||
|
|
||||||
|
if [[ ${#missing_files[@]} -gt 0 ]]; then
|
||||||
|
warn "Orphaned stack — compose file(s) missing:"
|
||||||
|
for m in "${missing_files[@]}"; do
|
||||||
|
echo " ${m}"
|
||||||
|
done
|
||||||
|
info "Tearing down orphaned stack ..."
|
||||||
|
if docker compose -p "$stack" down 2>/dev/null; then
|
||||||
|
ok "${stack} — orphaned stack removed"
|
||||||
|
else
|
||||||
|
# Fall back to direct container / network cleanup by project label.
|
||||||
|
info "Fallback: removing containers by project label …"
|
||||||
|
containers=$(docker container ls -q --filter label=com.docker.compose.project="$stack") || true
|
||||||
|
if [[ -n "$containers" ]]; then
|
||||||
|
docker container rm -f $containers 2>/dev/null || true
|
||||||
|
fi
|
||||||
|
networks=$(docker network ls -q --filter label=com.docker.compose.project="$stack") || true
|
||||||
|
if [[ -n "$networks" ]]; then
|
||||||
|
docker network rm $networks 2>/dev/null || true
|
||||||
|
fi
|
||||||
|
ok "${stack} — orphaned containers removed"
|
||||||
|
fi
|
||||||
|
orphaned_names+=("$stack")
|
||||||
|
echo; continue
|
||||||
|
fi
|
||||||
|
|
||||||
|
info "Pulling latest images ..."
|
||||||
|
pull_out=$(docker compose "${compose_args[@]}" pull 2>&1) || {
|
||||||
|
fail "Image pull failed for ${stack}"
|
||||||
|
echo "$pull_out" | head -5
|
||||||
|
failed_names+=("$stack")
|
||||||
|
echo; continue
|
||||||
|
}
|
||||||
|
|
||||||
|
# Always run up -d after a successful pull (idempotent when nothing
|
||||||
|
# changed).
|
||||||
|
info "Recreating containers …"
|
||||||
|
docker compose "${compose_args[@]}" up -d 2>&1 || {
|
||||||
|
fail "Container recreation failed for ${stack}"
|
||||||
|
failed_names+=("$stack"); echo; continue
|
||||||
|
}
|
||||||
|
|
||||||
|
# Determine which services (if any) received new images by parsing the
|
||||||
|
# pull output. Only the pull output is authoritative — up -d restarts
|
||||||
|
# are idempotent and tell us nothing about whether images changed.
|
||||||
|
#
|
||||||
|
# Patterns matched (TTY and non-TTY alike):
|
||||||
|
# " ✔ svc Pulled" / "svc Pulled"
|
||||||
|
# "Downloaded newer image for svc"
|
||||||
|
updated_svc=()
|
||||||
|
|
||||||
|
while IFS= read -r line; do
|
||||||
|
# "Downloaded newer image for svc" — unambiguous.
|
||||||
|
if [[ "$line" =~ Downloaded\ newer\ image\ for\ ([^:/\"]+) ]]; then
|
||||||
|
updated_svc+=("${BASH_REMATCH[1]}"); continue
|
||||||
|
fi
|
||||||
|
|
||||||
|
# "Pulled" not "Already up to date"
|
||||||
|
[[ "$line" =~ Already\ up\ to\ date ]] && continue
|
||||||
|
[[ ! "$line" =~ Pulled ]] && continue
|
||||||
|
|
||||||
|
# Extract the word before "Pulled". Strip leading " ✔ " if
|
||||||
|
# present, then read the first field.
|
||||||
|
cleaned="${line#*✔ }"
|
||||||
|
read -r svc _ <<< "$cleaned"
|
||||||
|
[[ -n "$svc" && "$svc" != "Pulled" ]] && updated_svc+=("$svc")
|
||||||
|
done <<< "$pull_out"
|
||||||
|
|
||||||
|
# Deduplicate while preserving order.
|
||||||
|
seen=""
|
||||||
|
deduped=()
|
||||||
|
for svc in "${updated_svc[@]}"; do
|
||||||
|
if [[ ! " $seen " =~ [[:space:]]${svc}[[:space:]] ]]; then
|
||||||
|
deduped+=("$svc")
|
||||||
|
seen+=" $svc "
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
if [[ ${#deduped[@]} -gt 0 ]]; then
|
||||||
|
ok "${stack} — updated: ${deduped[*]}"
|
||||||
|
updated_names+=("$stack")
|
||||||
|
updated_detail+=("${stack}: ${deduped[*]}")
|
||||||
|
else
|
||||||
|
info "${stack} — already up to date"
|
||||||
|
unchanged_names+=("$stack")
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo
|
||||||
|
done
|
||||||
|
|
||||||
|
# ── Summary ─────────────────────────────────────────────────────────
|
||||||
|
echo -e "${BOLD}──────────────────────────────────────────────────────${NC}"
|
||||||
|
echo -e "${BOLD} Summary${NC}"
|
||||||
|
echo -e "${BOLD}──────────────────────────────────────────────────────${NC}"
|
||||||
|
echo
|
||||||
|
|
||||||
|
total_found=$(( ${#updated_names[@]} + ${#unchanged_names[@]} + ${#orphaned_names[@]} + ${#failed_names[@]} ))
|
||||||
|
echo " Total stacks found: ${total_found}"
|
||||||
|
echo
|
||||||
|
|
||||||
|
if [[ ${#updated_names[@]} -gt 0 ]]; then
|
||||||
|
echo -e " ${GREEN}✓${NC} ${BOLD}Updated (${#updated_names[@]})${NC}"
|
||||||
|
for d in "${updated_detail[@]}"; do
|
||||||
|
echo " ${d}"
|
||||||
|
done
|
||||||
|
echo
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ ${#unchanged_names[@]} -gt 0 ]]; then
|
||||||
|
echo -e " ${YELLOW}−${NC} ${BOLD}Unchanged (${#unchanged_names[@]})${NC}"
|
||||||
|
printf " %s\n" "${unchanged_names[@]}"
|
||||||
|
echo
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ ${#orphaned_names[@]} -gt 0 ]]; then
|
||||||
|
echo -e " ${RED}✗${NC} ${BOLD}Removed (orphaned — ${#orphaned_names[@]})${NC}"
|
||||||
|
printf " %s\n" "${orphaned_names[@]}"
|
||||||
|
echo
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ ${#failed_names[@]} -gt 0 ]]; then
|
||||||
|
echo -e " ${RED}✗${NC} ${BOLD}Failed (${#failed_names[@]})${NC}"
|
||||||
|
printf " %s\n" "${failed_names[@]}"
|
||||||
|
echo
|
||||||
|
fi
|
||||||
Reference in New Issue
Block a user