Skip to content

Commit 8ae1c70

Browse files
committed
feat: add script to refresh pinned image lock automatically
1 parent b6f2eac commit 8ae1c70

3 files changed

Lines changed: 159 additions & 10 deletions

File tree

IMAGE_LOCK.md

Lines changed: 9 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -5,26 +5,25 @@ This stack is pinned to exact image digests in `docker-compose.yml` for reproduc
55
Tested lock snapshot:
66
- Date: `2026-02-22`
77
- Docker Engine: `29.2.1`
8-
- Platform: `aarch64` (Docker Desktop on macOS)
8+
- Platform: `aarch64 (Docker Desktop)`
99

1010
| Service | Locked Image |
1111
|---|---|
12+
| bazarr | `lscr.io/linuxserver/bazarr@sha256:1cf40186b1bc35bec87f4e4892d5d8c06086da331010be03e3459a86869c5e74` |
13+
| flaresolverr | `ghcr.io/flaresolverr/flaresolverr@sha256:7962759d99d7e125e108e0f5e7f3cdbcd36161776d058d1d9b7153b92ef1af9e` |
1214
| gluetun | `qmcgaw/gluetun@sha256:495cdc65ace4c110cf4de3d1f5f90e8a1dd2eb0f8b67151d1ad6101b2a02a476` |
1315
| qbittorrent | `lscr.io/linuxserver/qbittorrent@sha256:85eb27d2d09cd4cb748036a4c7f261321da516b6f88229176cf05a92ccd26815` |
14-
| prowlarr | `lscr.io/linuxserver/prowlarr@sha256:e74a1e093dcc223d671d4b7061e2b4946f1989a4d3059654ff4e623b731c9134` |
15-
| sonarr | `lscr.io/linuxserver/sonarr@sha256:37be832b78548e3f55f69c45b50e3b14d18df1b6def2a4994258217e67efb1a1` |
1616
| radarr | `lscr.io/linuxserver/radarr@sha256:6d3e68474ea146f995af98d3fb2cb1a14e2e4457ddaf035aa5426889e2f9249c` |
17-
| bazarr | `lscr.io/linuxserver/bazarr@sha256:1cf40186b1bc35bec87f4e4892d5d8c06086da331010be03e3459a86869c5e74` |
18-
| flaresolverr | `ghcr.io/flaresolverr/flaresolverr@sha256:7962759d99d7e125e108e0f5e7f3cdbcd36161776d058d1d9b7153b92ef1af9e` |
17+
| sonarr | `lscr.io/linuxserver/sonarr@sha256:37be832b78548e3f55f69c45b50e3b14d18df1b6def2a4994258217e67efb1a1` |
18+
| prowlarr | `lscr.io/linuxserver/prowlarr@sha256:e74a1e093dcc223d671d4b7061e2b4946f1989a4d3059654ff4e623b731c9134` |
1919
| seerr | `ghcr.io/seerr-team/seerr@sha256:1b5fc1ea825631d9d165364472663b817a4c58ef6aa1013f58d82c1570d7c866` |
2020
| watchtower (optional) | `containrrr/watchtower@sha256:6dd50763bbd632a83cb154d5451700530d1e44200b268a4e9488fefdfcf2b038` |
2121

2222
## Updating The Lock
2323

24-
1. Pull new candidates:
24+
Run:
2525
```bash
26-
docker compose --profile autoupdate pull
26+
bash scripts/refresh-image-lock.sh
2727
```
28-
2. Smoke test stack behavior.
29-
3. Update digests in `docker-compose.yml`.
30-
4. Update this matrix in `IMAGE_LOCK.md`.
28+
29+
Then smoke test the stack and commit the updated lock files.

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,7 @@ By default, Seerr is bound to `127.0.0.1` for safer local-only access. Set `SEER
9292
| `scripts/health-check.sh` | Checks if everything is running correctly |
9393
| `scripts/auto-heal.sh` | Hourly self-healer (restarts VPN/containers if down) |
9494
| `scripts/install-auto-heal.sh` | Installs auto-heal as a background job via launchd |
95+
| `scripts/refresh-image-lock.sh` | Refreshes pinned image digests and regenerates IMAGE_LOCK.md |
9596

9697
## What It Looks Like
9798

scripts/refresh-image-lock.sh

Lines changed: 149 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,149 @@
1+
#!/bin/bash
2+
# Refreshes image digests in docker-compose.yml and regenerates IMAGE_LOCK.md.
3+
# Uses :latest for each configured image repository, then pins by digest.
4+
5+
set -euo pipefail
6+
7+
SCRIPT_DIR="$(cd "$(dirname "$0")/.." && pwd)"
8+
COMPOSE_FILE="$SCRIPT_DIR/docker-compose.yml"
9+
LOCK_FILE="$SCRIPT_DIR/IMAGE_LOCK.md"
10+
ENV_FILE="$SCRIPT_DIR/.env"
11+
12+
PROFILES=(autoupdate)
13+
OPTIONAL_SERVICES=(watchtower)
14+
15+
for cmd in docker awk sed mktemp; do
16+
if ! command -v "$cmd" >/dev/null 2>&1; then
17+
echo "Missing required command: $cmd"
18+
exit 1
19+
fi
20+
done
21+
22+
if [[ ! -f "$COMPOSE_FILE" ]]; then
23+
echo "Could not find $COMPOSE_FILE"
24+
exit 1
25+
fi
26+
27+
compose() {
28+
docker compose "${PROFILE_ARGS[@]}" "$@"
29+
}
30+
31+
PROFILE_ARGS=()
32+
for profile in "${PROFILES[@]}"; do
33+
PROFILE_ARGS+=(--profile "$profile")
34+
done
35+
36+
tmp_map="$(mktemp)"
37+
tmp_compose="$(mktemp)"
38+
tmp_rendered="$(mktemp)"
39+
40+
cleanup() {
41+
if [[ "${CREATED_ENV:-0}" == "1" ]]; then
42+
rm -f "$ENV_FILE"
43+
fi
44+
rm -f "$tmp_map" "$tmp_compose" "$tmp_rendered"
45+
}
46+
trap cleanup EXIT
47+
48+
CREATED_ENV=0
49+
if [[ ! -f "$ENV_FILE" ]]; then
50+
cp "$SCRIPT_DIR/.env.example" "$ENV_FILE"
51+
CREATED_ENV=1
52+
fi
53+
54+
compose config > "$tmp_rendered"
55+
while IFS= read -r service; do
56+
image_ref="$(awk -v svc="$service" '
57+
$0 ~ ("^ " svc ":$") { in_service=1; next }
58+
in_service && $0 ~ /^ image:[[:space:]]+/ {
59+
line=$0
60+
sub(/^ image:[[:space:]]+/, "", line)
61+
print line
62+
exit
63+
}
64+
in_service && $0 ~ /^ [A-Za-z0-9_-]+:$/ { in_service=0 }
65+
' "$tmp_rendered")"
66+
67+
if [[ -z "$image_ref" ]]; then
68+
echo "Could not resolve image for service: $service"
69+
exit 1
70+
fi
71+
echo "$service|$image_ref" >> "$tmp_map"
72+
done < <(compose config --services)
73+
74+
if [[ ! -s "$tmp_map" ]]; then
75+
echo "Failed to parse service images from compose config."
76+
exit 1
77+
fi
78+
79+
while IFS='|' read -r service image_ref; do
80+
repo="${image_ref%@*}"
81+
candidate="${repo}:latest"
82+
echo "Refreshing $service ($candidate)"
83+
docker pull "$candidate" >/dev/null
84+
digest="$(docker image inspect --format '{{index .RepoDigests 0}}' "$candidate")"
85+
if [[ -z "$digest" ]]; then
86+
echo "Failed to resolve digest for $candidate"
87+
exit 1
88+
fi
89+
sed -i '' -E "s#^${service}\\|.*#${service}|${digest}#" "$tmp_map"
90+
done < "$tmp_map"
91+
92+
awk -F'|' '
93+
NR==FNR { lock[$1]=$2; next }
94+
/^[ ]{2}[a-zA-Z0-9_-]+:$/ {
95+
svc=$0
96+
sub(/^[ ]{2}/, "", svc)
97+
sub(/:$/, "", svc)
98+
current=svc
99+
print
100+
next
101+
}
102+
/^[ ]{4}image:/ && (current in lock) {
103+
print " image: " lock[current]
104+
next
105+
}
106+
{ print }
107+
' "$tmp_map" "$COMPOSE_FILE" > "$tmp_compose"
108+
109+
mv "$tmp_compose" "$COMPOSE_FILE"
110+
111+
docker_engine="$(docker version --format '{{.Server.Version}}')"
112+
platform="$(docker info --format '{{.Architecture}} ({{.OperatingSystem}})')"
113+
date_utc="$(date -u +%F)"
114+
115+
{
116+
echo "# Image Lock Matrix"
117+
echo
118+
echo "This stack is pinned to exact image digests in \`docker-compose.yml\` for reproducible installs."
119+
echo
120+
echo "Tested lock snapshot:"
121+
echo "- Date: \`$date_utc\`"
122+
echo "- Docker Engine: \`$docker_engine\`"
123+
echo "- Platform: \`$platform\`"
124+
echo
125+
echo "| Service | Locked Image |"
126+
echo "|---|---|"
127+
while IFS='|' read -r service digest; do
128+
label="$service"
129+
for optional in "${OPTIONAL_SERVICES[@]}"; do
130+
if [[ "$service" == "$optional" ]]; then
131+
label="$service (optional)"
132+
fi
133+
done
134+
echo "| $label | \`$digest\` |"
135+
done < "$tmp_map"
136+
echo
137+
echo "## Updating The Lock"
138+
echo
139+
echo "Run:"
140+
echo "\`\`\`bash"
141+
echo "bash scripts/refresh-image-lock.sh"
142+
echo "\`\`\`"
143+
echo
144+
echo "Then smoke test the stack and commit the updated lock files."
145+
} > "$LOCK_FILE"
146+
147+
echo "Updated:"
148+
echo " - $COMPOSE_FILE"
149+
echo " - $LOCK_FILE"

0 commit comments

Comments
 (0)