Gemeten: 709 seconden voor 318 images, terwijl de netwerktijd zo'n 48 seconden is. Twee oorzaken. - het aantal tegelijk gold in totaal in plaats van per registry. Registries knijpen per host af en tweederde staat op Docker Hub, dus ghcr, lscr en quay stonden daarachter te wachten zonder dat dat iets oploste. Nu vier per registry, registries naast elkaar - elke ronde controleerde alles opnieuw. Geslaagde controles blijven een week staan, met spreiding per image zodat niet alles op dezelfde dag verloopt. De sleutel is de naam met tag, dus een gewijzigde tag wordt altijd gecontroleerd; mislukkingen komen nooit in de cache Gemeten na de wijziging: koude cache 43s, warme cache 0s. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Q9eqpADJSRs49SoGGr4NAy
310 lines
No EOL
13 KiB
YAML
310 lines
No EOL
13 KiB
YAML
name: Deploy server-up (dev)
|
|
|
|
on:
|
|
push:
|
|
branches: [dev]
|
|
workflow_dispatch:
|
|
|
|
concurrency:
|
|
group: server-up-deploy-dev
|
|
cancel-in-progress: false
|
|
|
|
jobs:
|
|
deploy:
|
|
runs-on: [self-hosted, dev]
|
|
timeout-minutes: 20
|
|
|
|
env:
|
|
DEPLOY_DIR: /opt/docker/server-up
|
|
|
|
steps:
|
|
- name: Checkout
|
|
uses: actions/checkout@v4
|
|
with:
|
|
fetch-depth: 1
|
|
|
|
- name: Bepaal versie
|
|
id: version
|
|
run: |
|
|
set -euo pipefail
|
|
BASE="$(tr -d '[:space:]' < VERSION 2>/dev/null || echo 0.0.0)"
|
|
if [[ "${GITHUB_REF}" == refs/tags/* ]]; then
|
|
VERSION="${GITHUB_REF_NAME#v}"
|
|
CHANNEL="release"
|
|
else
|
|
VERSION="${BASE}"
|
|
CHANNEL="dev"
|
|
fi
|
|
echo "VERSION=${VERSION}" >> "$GITHUB_ENV"
|
|
echo "CHANNEL=${CHANNEL}" >> "$GITHUB_ENV"
|
|
# SU_VERSION wordt door docker compose gebruikt als build-arg
|
|
echo "SU_VERSION=${VERSION}" >> "$GITHUB_ENV"
|
|
echo "Deploying version: ${VERSION} (${CHANNEL})"
|
|
|
|
- name: Tests draaien
|
|
run: |
|
|
set -euo pipefail
|
|
# In een container in plaats van een venv op de runner: die heeft
|
|
# geen python3-venv, en zo draaien de tests bovendien op exact de
|
|
# Python-versie waarop de app in productie draait.
|
|
# PYTHONDONTWRITEBYTECODE + no:cacheprovider voorkomen dat er
|
|
# root-eigen bestanden in de werkmap achterblijven.
|
|
docker run --rm \
|
|
-v "$PWD:/w" -w /w \
|
|
-v su-pip-cache:/root/.cache/pip \
|
|
-e PYTHONDONTWRITEBYTECODE=1 \
|
|
python:3.12-slim \
|
|
sh -c "pip install -q -r server-up/requirements.txt pytest \
|
|
&& python -m pytest tests -q -p no:cacheprovider"
|
|
|
|
- name: Compose-bestanden valideren
|
|
run: |
|
|
set -euo pipefail
|
|
# De testsuite controleert of de sjablonen geldige YAML opleveren.
|
|
# Of `docker compose` ze ook accepteert is een andere vraag: een
|
|
# depends_on in de verkeerde vorm of network_mode naast networks is
|
|
# prima YAML en een kapotte stack. Dat merk je anders pas als iemand
|
|
# de app installeert.
|
|
#
|
|
# Renderen kan alleen waar Python staat (in de container), valideren
|
|
# alleen waar Docker staat (op de runner). Vandaar twee helften.
|
|
docker run --rm \
|
|
-v "$PWD:/w" -w /w \
|
|
-v su-pip-cache:/root/.cache/pip \
|
|
-e PYTHONDONTWRITEBYTECODE=1 \
|
|
python:3.12-slim \
|
|
sh -c "pip install -q -r server-up/requirements.txt \
|
|
&& python tools/render_voor_validatie.py .compose-check \
|
|
&& chown -R $(id -u):$(id -g) .compose-check"
|
|
fouten=0
|
|
for f in .compose-check/*/docker-compose.yml; do
|
|
if ! uitvoer=$(docker compose -f "$f" config -q 2>&1); then
|
|
echo "✖ $(basename "$(dirname "$f")")"
|
|
echo "$uitvoer" | sed 's/^/ /'
|
|
fouten=$((fouten + 1))
|
|
fi
|
|
done
|
|
rm -rf .compose-check
|
|
if [ "$fouten" -ne 0 ]; then
|
|
echo "$fouten sjablo(o)n(en) leveren een compose-bestand op dat docker weigert"
|
|
exit 1
|
|
fi
|
|
echo "Alle compose-bestanden zijn geldig"
|
|
|
|
- name: Images controleren
|
|
run: |
|
|
set -euo pipefail
|
|
# Een image dat niet meer bestaat merk je anders pas als iemand op
|
|
# installeren drukt. Zo verdween Huntarr nadat dat project offline
|
|
# ging, en wezen Forgejo, Planka en Baby Buddy naar tags die er niet
|
|
# zijn. Draait bewust rustig: registries knijpen af, en een afgeknepen
|
|
# verzoek is niet te onderscheiden van een ontbrekend image.
|
|
#
|
|
# Geslaagde controles blijven een week staan in su-image-cache, met
|
|
# spreiding zodat niet alles op dezelfde dag verloopt. Een sjabloon
|
|
# dat naar een andere tag gaat wijzen levert een nieuwe sleutel op en
|
|
# wordt dus altijd gecontroleerd; mislukkingen komen nooit in de cache.
|
|
docker run --rm \
|
|
-v "$PWD:/w" -w /w \
|
|
-v su-image-cache:/cache \
|
|
-e PYTHONDONTWRITEBYTECODE=1 \
|
|
-e SU_IMAGE_CACHE=/cache/images.json \
|
|
python:3.12-slim \
|
|
python tools/controleer_images.py
|
|
|
|
- name: Sync code naar deploy-directory
|
|
run: |
|
|
set -euo pipefail
|
|
# .env staat in .gitignore, dus de checkout heeft er geen. Zonder deze
|
|
# uitzondering wist --delete de lokale instellingen van de server
|
|
# (BIND, SU_IMAGE, PORT) bij elke deploy.
|
|
rsync -a --no-group --delete \
|
|
--exclude='.git/' \
|
|
--exclude='.forgejo/' \
|
|
--exclude='.env' \
|
|
./ "$DEPLOY_DIR/"
|
|
|
|
- name: Instellingen en versie klaarzetten
|
|
run: |
|
|
set -euo pipefail
|
|
# Expliciet cd in plaats van 'working-directory:'. Dat laatste wordt
|
|
# niet door elke runner-implementatie even betrouwbaar toegepast, en
|
|
# als het misgaat belanden VERSION, .env en de container stilletjes in
|
|
# de werkmap van de runner in plaats van in de deploy-map.
|
|
cd "$DEPLOY_DIR"
|
|
echo "Werkmap: $(pwd)"
|
|
|
|
echo "${VERSION}" > VERSION
|
|
|
|
# SU_TAG bepaalt welk image compose start. We schrijven dat bij
|
|
# voorkeur in .env zodat het een handmatige `docker compose up`
|
|
# overleeft, maar dat bestand kan van een andere gebruiker zijn (bv.
|
|
# met sudo aangemaakt). Lukt schrijven niet, dan geven we SU_TAG mee
|
|
# via de omgeving: compose gebruikt die en de deploy loopt gewoon door.
|
|
if [ ! -f .env ] && [ -f .env.example ]; then
|
|
cp .env.example .env 2>/dev/null \
|
|
&& echo "Nieuwe .env aangemaakt vanuit .env.example" \
|
|
|| echo "LET OP: kon geen .env aanmaken in $(pwd)"
|
|
fi
|
|
|
|
if [ -w .env ] || { [ ! -e .env ] && [ -w . ]; }; then
|
|
touch .env
|
|
sed -i '/^SU_TAG=/d' .env
|
|
echo "SU_TAG=${VERSION}" >> .env
|
|
echo "Inhoud van $(pwd)/.env:"
|
|
grep -vE '^\s*(#|$)' .env | sed 's/^/ /'
|
|
else
|
|
echo "LET OP: $(pwd)/.env is niet schrijfbaar voor $(id -un)."
|
|
echo " Eigenaar: $(stat -c '%U:%G %a' .env 2>/dev/null || echo onbekend)"
|
|
echo " Herstel eenmalig met:"
|
|
echo " sudo chown $(stat -c '%U:%G' . ) $(pwd)/.env"
|
|
echo " SU_TAG wordt nu via de omgeving meegegeven."
|
|
fi
|
|
|
|
- name: Build image
|
|
run: |
|
|
set -euo pipefail
|
|
cd "$DEPLOY_DIR"
|
|
export SU_TAG="${VERSION}"
|
|
docker compose build --pull
|
|
docker tag "server-up:${VERSION}" "server-up:${CHANNEL}" || true
|
|
|
|
# Server Up draait niet meer als root. Dat hangt aan het entrypoint en aan
|
|
# `setpriv`, en beide zijn alleen in een echte container te toetsen — de
|
|
# pytest-suite kan er niet bij. Zonder deze stap merk je een regressie hier
|
|
# pas als iemands installatie niet meer opkomt.
|
|
- name: Niet-root modus controleren
|
|
run: |
|
|
set -euo pipefail
|
|
IMG="server-up:${VERSION}"
|
|
|
|
echo "1. setpriv aanwezig in het image"
|
|
docker run --rm --entrypoint setpriv "$IMG" --help >/dev/null
|
|
echo " ok"
|
|
|
|
echo "2. zakt af naar de opgegeven gebruiker"
|
|
uit=$(docker run --rm -e SU_UID=1000 -e SU_GID=1000 \
|
|
-v /var/run/docker.sock:/var/run/docker.sock "$IMG" id)
|
|
echo " $uit"
|
|
case "$uit" in
|
|
uid=1000*) ;;
|
|
*) echo "FOUT: draait niet als uid 1000"; exit 1 ;;
|
|
esac
|
|
|
|
echo "3. zit in de groep van de docker-socket"
|
|
sok_gid=$(stat -c %g /var/run/docker.sock)
|
|
case "$uit" in
|
|
*"$sok_gid"*) echo " gid $sok_gid aanwezig" ;;
|
|
*) echo "FOUT: gid $sok_gid van de socket ontbreekt"; exit 1 ;;
|
|
esac
|
|
|
|
echo "4. kan schrijven in /data"
|
|
docker run --rm -e SU_UID=1000 -e SU_GID=1000 \
|
|
-v "su-nietroot-proef-${VERSION}:/data" "$IMG" \
|
|
sh -c 'touch /data/proef && echo " ok"'
|
|
docker volume rm "su-nietroot-proef-${VERSION}" >/dev/null
|
|
|
|
echo "5. zonder SU_UID blijft het root — de ontsnappingsklep"
|
|
uit0=$(docker run --rm "$IMG" id)
|
|
echo " $uit0"
|
|
case "$uit0" in
|
|
uid=0*) ;;
|
|
*) echo "FOUT: SU_UID=0 hoort root te blijven"; exit 1 ;;
|
|
esac
|
|
|
|
- name: Deploy
|
|
run: |
|
|
set -euo pipefail
|
|
cd "$DEPLOY_DIR"
|
|
export SU_TAG="${VERSION}"
|
|
docker compose up -d --remove-orphans
|
|
docker compose ps
|
|
# Waar draait de container vandaan, en op welk adres luistert hij?
|
|
echo "Compose-projectmap volgens de container:"
|
|
docker inspect server-up \
|
|
--format ' {{index .Config.Labels "com.docker.compose.project.working_dir"}}'
|
|
echo "Poortbinding:"
|
|
docker inspect server-up \
|
|
--format ' {{range $p, $c := .NetworkSettings.Ports}}{{$p}} -> {{range $c}}{{.HostIp}}:{{.HostPort}}{{end}}{{"\n"}}{{end}}'
|
|
|
|
- name: Wachten op healthcheck
|
|
run: |
|
|
set -euo pipefail
|
|
for i in {1..40}; do
|
|
status=$(docker inspect --format '{{.State.Health.Status}}' server-up 2>/dev/null || echo "starting")
|
|
echo "poll $i: $status"
|
|
if [ "$status" = "healthy" ]; then
|
|
echo "server-up is healthy als versie ${VERSION}"
|
|
exit 0
|
|
fi
|
|
sleep 3
|
|
done
|
|
echo "server-up werd niet healthy in tijd — logs:"
|
|
cd "$DEPLOY_DIR" && docker compose logs --tail=200
|
|
exit 1
|
|
|
|
# De backuptests vervangen Docker allemaal door een nepversie. Dat toetst
|
|
# de logica maar niet de keten: pg_dump via `docker exec`, het archief,
|
|
# het terugzetten, en of de database daarna nog schrijft. Juist daar
|
|
# betekent fout zijn gegevensverlies.
|
|
#
|
|
# Draait op het zojuist gebouwde image — dus met exact de Python en de
|
|
# docker-CLI van productie. De werkmap wordt op hetzelfde pad gemount,
|
|
# want de bind mount van de postgres-container wordt door de daemon op de
|
|
# host opgezocht, niet in deze container.
|
|
- name: Backup en terugzetten met echte Docker
|
|
run: |
|
|
set -uo pipefail
|
|
PROEF="$DEPLOY_DIR/.backup-rondgang"
|
|
rm -rf "$PROEF"; mkdir -p "$PROEF"
|
|
docker run --rm \
|
|
-v /var/run/docker.sock:/var/run/docker.sock \
|
|
-v "$PROEF:$PROEF" \
|
|
-v "$PWD:/w" -w /w \
|
|
-e PYTHONDONTWRITEBYTECODE=1 \
|
|
"server-up:${VERSION}" \
|
|
python tools/backup_rondgang.py "$PROEF"
|
|
code=$?
|
|
# De exitcode expliciet tonen. Een run die "Rondgang geslaagd" op het
|
|
# scherm zette en tóch rood werd, liet nergens zien waar dat vandaan
|
|
# kwam — en zonder dit getal is dat niet te achterhalen.
|
|
echo "rondgang exitcode: $code"
|
|
rm -rf "$PROEF" || true
|
|
[ "$code" -eq 0 ] || exit 1
|
|
|
|
# `--doctor` is ruim honderd regels shell die alleen tegen een échte
|
|
# installatie draaien. De pytest-suite komt niet verder dan "er is geen
|
|
# Docker"; hier wel.
|
|
- name: install.sh --doctor doorlichten
|
|
if: success()
|
|
run: |
|
|
set -uo pipefail
|
|
uit="$(sh "$DEPLOY_DIR/install.sh" --doctor --dir "$DEPLOY_DIR" 2>&1)"
|
|
code=$?
|
|
printf '%s\n' "$uit"
|
|
|
|
# Alle secties moeten gedraaid hebben: een shell-fout halverwege zou
|
|
# anders als "niets aan de hand" langskomen.
|
|
for kop in "Installatie" "Docker" "Container" "Bereikbaarheid" \
|
|
"Account" "Mappen" "Bijwerken vanuit de interface"; do
|
|
printf '%s\n' "$uit" | grep -q "^$kop$" || {
|
|
echo "FOUT: --doctor kwam niet tot de sectie '$kop'"; exit 1; }
|
|
done
|
|
|
|
# En de uitslag telt mee. Waarschuwingen mogen (deze deploy draait
|
|
# bewust nog als root), fouten niet.
|
|
if [ "$code" -ne 0 ]; then
|
|
echo "FOUT: --doctor meldt problemen met de deploy"
|
|
exit 1
|
|
fi
|
|
echo "--doctor liep van begin tot eind, zonder fouten"
|
|
|
|
- name: Oude images opruimen (behoud laatste 5)
|
|
if: success()
|
|
run: |
|
|
docker images server-up --format '{{.Tag}} {{.ID}}' \
|
|
| grep -vE '^(latest|release|dev) ' \
|
|
| tail -n +6 \
|
|
| awk '{print $2}' \
|
|
| xargs -r docker rmi -f || true
|
|
docker image prune -f |