Skip to content

ci: cache docker layers & improve security practices - #104

Merged
Alessevan merged 2 commits into
masterfrom
ci/cache
Sep 16, 2026
Merged

Alessevan merged 2 commits into
masterfrom
ci/cache

Conversation

@AntoineJT

@AntoineJT AntoineJT commented Sep 16, 2026 •

Copy link
Copy Markdown
Member

🇬🇧 English

🎯 Summary

CI rebuilt the Docker image from scratch on every run. This branch adds a layer cache, reorders the Dockerfile so dependencies survive source changes, drops a compiler that turned out to be unused, and pins the base image by digest.

🤔 Motivation

The docker-build-check job had no cache at all, so each run redid the full apt/pip setup and a complete poetry install — twice, since the image is built for linux/amd64 and linux/arm64, with one half under QEMU emulation. A cache alone would have under-delivered: COPY ./ /app sat above poetry install, so any source change invalidated the dependency layer.

Separately, FROM python:3.13-slim was a floating tag. Base image updates arrived silently — no pull request, no record, no rollback point — and the same commit could build a different image.

📦 What's included

  • 🔧 Import and export the GitHub Actions cache in the Docker build job, and cache the Poetry virtualenv in the lint job (setup-python now runs after Poetry is installed, which it needs).
  • 🔧 Install dependencies before copying the source, so the poetry install layer is rebuilt only when the lockfile changes.
  • ✨ Mount a per-architecture BuildKit cache for Poetry's downloads.
  • 🔧 Drop gcc: every dependency ships prebuilt wheels for both target architectures, which removes the apt layer entirely — 479 MB → 313 MB, and no compiler in the production image.
  • 🔒 Pin the base image to python:3.13.15-slim-trixie@sha256:9d2e5553…, so a Python patch bump reads as a version change in the diff and a Debian suite migration cannot arrive as an opaque digest change.
  • ✅ Measured on a multi-arch build: 203s with an empty cache, 16s once imported. Both architectures build green, runtime imports and alembic 1.20.0 verified, hadolint clean with no ignores, actionlint clean.

🧭 Notes

  • ⚠️ The first run here will not be faster: a PR reads caches written on master, so the scope has to be populated by a master build before the gain shows.
  • 🔭 renovate.json and dependabot.yml both watch Docker. With a digest now in FROM, that line is the most likely place for the two bots to collide.
  • 🔭 The docker ecosystem in dependabot.yml carries cooldown: default-days: 7, which delays base-image security patches by a week — the path we rely on now that the image no longer runs apt-get upgrade.
  • 🔭 In deploy.yml, cache-to on a tag run writes to a scope nothing can read back; only its cache-from does real work. Left as-is, out of scope here.
  • 🔭 Running as a non-root USER is deliberately not in this branch — it needs a chown on the server's bind-mounted logs/ and data/, and is handled separately.

🇫🇷 Français

🎯 Résumé

La CI reconstruisait l'image Docker intégralement à chaque exécution. Cette branche ajoute un cache de couches, réordonne le Dockerfile pour que les dépendances survivent aux modifications de source, retire un compilateur finalement inutile, et épingle l'image de base par digest.

🤔 Motivation

Le job docker-build-check n'avait aucun cache : chaque exécution refaisait toute l'installation apt/pip et un poetry install complet — deux fois, puisque l'image est construite pour linux/amd64 et linux/arm64, dont une moitié en émulation QEMU. Un cache seul n'aurait pas suffi : COPY ./ /app précédait poetry install, donc toute modification de source invalidait la couche de dépendances.

Par ailleurs, FROM python:3.13-slim était un tag flottant. Les mises à jour de l'image de base arrivaient silencieusement — sans pull request, sans trace, sans point de retour — et un même commit pouvait produire une image différente.

📦 Contenu

  • 🔧 Import et export du cache GitHub Actions dans le job de build Docker, et cache du virtualenv Poetry dans le job lint (setup-python s'exécute désormais après l'installation de Poetry, dont il a besoin).
  • 🔧 Installation des dépendances avant la copie des sources, pour que la couche poetry install ne soit reconstruite qu'au changement du lockfile.
  • ✨ Montage d'un cache BuildKit par architecture pour les téléchargements de Poetry.
  • 🔧 Retrait de gcc : toutes les dépendances fournissent des wheels précompilées pour les deux architectures cibles, ce qui supprime la couche apt — 479 Mo → 313 Mo, et plus de compilateur dans l'image de production.
  • 🔒 Épinglage de l'image de base sur python:3.13.15-slim-trixie@sha256:9d2e5553… : une montée de version corrective de Python se lit comme un changement de version dans le diff, et un changement de suite Debian ne peut plus arriver sous forme de digest opaque.
  • ✅ Mesuré sur un build multi-arch : 203 s avec un cache vide, 16 s une fois importé. Les deux architectures compilent, imports à l'exécution et alembic 1.20.0 vérifiés, hadolint propre sans aucune suppression, actionlint propre.

🧭 À noter

  • ⚠️ La première exécution ici ne sera pas plus rapide : une PR lit les caches écrits sur master, il faut donc qu'un build sur master ait rempli le scope avant que le gain apparaisse.
  • 🔭 renovate.json et dependabot.yml surveillent tous deux Docker. Avec un digest désormais dans FROM, cette ligne est l'endroit le plus probable d'une collision entre les deux bots.
  • 🔭 L'écosystème docker de dependabot.yml porte cooldown: default-days: 7, ce qui retarde d'une semaine les correctifs de sécurité de l'image de base — le chemin sur lequel on s'appuie maintenant que l'image ne fait plus apt-get upgrade.
  • 🔭 Dans deploy.yml, le cache-to d'une exécution sur tag écrit dans un scope que rien ne peut relire ; seul son cache-from sert réellement. Laissé tel quel, hors périmètre.
  • 🔭 Le passage à un USER non-root n'est volontairement pas dans cette branche : il impose un chown sur les logs/ et data/ montés côté serveur, et est traité séparément.

📋 Checklist

  • 🇬🇧 Confirm the CI run is green on this PR / 🇫🇷 Vérifier que l'exécution de la CI est verte sur cette PR
  • 🇬🇧 After a master build has populated the cache, confirm a later run shows a cache hit / 🇫🇷 Une fois le cache rempli par un build sur master, vérifier qu'une exécution ultérieure montre un cache hit

AntoineJT and others added 2 commits September 15, 2026 21:37
Builds re-did every step on each run: the full apt/pip setup and a
complete `poetry install`, both of them twice because the image is built
for amd64 and arm64.

- Import and export the GitHub Actions cache in the Docker build job,
  and cache the Poetry virtualenv in the lint job.
- Install dependencies before copying the source, so the install layer
  is rebuilt only when the lockfile changes.
- Mount a per-architecture BuildKit cache for Poetry's downloads.
- Drop gcc: every dependency ships prebuilt wheels for both target
  architectures, so the apt layer is no longer needed (479MB -> 313MB).

A multi-arch build takes 203s with an empty cache and 16s once the cache
is imported.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The floating `python:3.13-slim` tag moved silently: nothing recorded when
the base image changed, and the same commit could build a different
image. Pinning makes builds reproducible and turns each base update into
a reviewable pull request.

The tag names the exact version and Debian suite alongside the digest so
the two agree: a Python patch bump reads as 3.13.15 -> 3.13.16 in the
diff, and a move to a new Debian suite has to rewrite `-trixie` rather
than arriving as an opaque digest change. Dependabot only compares tags
sharing the same suffix, so `-slim-trixie` keeps tracking its own
variant; a digest with no tag would be tracked against `python:latest`.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

@Alessevan Alessevan left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why not.

@Alessevan
Alessevan merged commit 5270ee5 into master Sep 16, 2026
5 checks passed
@Alessevan
Alessevan deleted the ci/cache branch September 16, 2026 21:02
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants