Skip to content

Container build en preview deployment via ZAD#844

Merged
ravimeijerrig merged 33 commits into
mainfrom
feat/container-deployment
Jun 17, 2026
Merged

Container build en preview deployment via ZAD#844
ravimeijerrig merged 33 commits into
mainfrom
feat/container-deployment

Conversation

@robbertbos

@robbertbos robbertbos commented Feb 18, 2026

Copy link
Copy Markdown
Member

Summary

  • Multi-stage container/Dockerfile: MkDocs build + gehard nginx-base (RijksICTGilde, non-root, read-only-rootfs) met security headers (CSP, HSTS, etc.)
  • Preview workflow: bouwt container en deployt naar ZAD op PRs en push naar main
  • Production workflow: bouwt container en deployt naar ZAD op version tags (v*)
  • Material Insiders dependency verwijderd (nu gratis in publieke repo)
  • Omgevings-onafhankelijke image: draait standaard op de root, of onder een subpad/eigen domein via de optionele SITE_URL-env (stuurt interne links/beslishulp én canonical/sitemap). getBasePath() in modal.js leest dit basispad i.p.v. hardcoded paden.

Sluit #837

Test plan

  • Lokaal bouwen: docker build -t algoritmekader -f container/Dockerfile .
  • Lokaal draaien: docker run -p 8080:8080 algoritmekaderhttp://localhost:8080
  • Beslishulp AI-verordening openen vanuit modal
  • Onder een subpad: docker run -e SITE_URL=https://.../kader -p 8080:8080 algoritmekader (reverse proxy stuurt het prefix ongestript door)
  • Preview deployment controleren via ZAD comment op PR
  • Production deployment testen via tag push

ravimeijerrig and others added 3 commits January 20, 2026 14:37
- Multi-stage Containerfile: MkDocs build + nginx-unprivileged with security headers
- Preview workflow: builds and deploys to ZAD on PRs and push to main
- Production workflow: builds and deploys to ZAD on version tags
- Remove Material Insiders dependency (now free in public repo)
- Fix getBasePath() in modal.js to use MkDocs base element instead of hardcoded paths
- Remove .claude/ from git, add to .gitignore
@robbertbos robbertbos self-assigned this Feb 18, 2026
@robbertbos robbertbos changed the title Container deployment via ZAD Container build en preview deployment via ZAD Feb 18, 2026
@ravimeijerrig

ravimeijerrig commented Jun 10, 2026

Copy link
Copy Markdown
Collaborator

Info voor Algoritmeregister-team

Routing-eis voor de Algoritmekader-container (productie)

Wat je krijgt

Een container (poort 8080, unprivileged nginx) die de statisch gebouwde Algoritmekader-website serveert. De bestanden staan op de root van de webserver (/usr/share/nginx/html), dus bv. GET / → homepage, GET /voldoen-aan-wetten-en-regels/vereisten/ → die pagina.

De eis: prefix /kader/ moet vóór de container worden gestript

De site wordt gebouwd met SITE_URL=https://algoritmes.overheid.nl/kader/. Daardoor zet MkDocs en krijgen alle interne links, assets (CSS/JS)
en redirects het pad-prefix /kader/. De container zelf weet niets van dat prefix en serveert op root.

De ingress/reverse proxy vóór de container moet het /kader-prefix verwijderen voordat het request de container bereikt:

Publieke request (browser) Wat de container moet ontvangen (poort 8080)
…/kader/ GET /
…/kader/voldoen-aan-wetten-en-regels/vereisten/ GET /voldoen-aan-wetten-en-regels/vereisten/
…/kader/assets/stylesheets/main.css GET /assets/stylesheets/main.css

Doe je dat niet, dan laadt de homepage misschien wél, maar 404'en alle interne links en assets (de pagina komt kapot/zonder opmaak binnen).

Voorbeeld (Kubernetes nginx-ingress)

  metadata:
    annotations:
      nginx.ingress.kubernetes.io/rewrite-target: /$2
  spec:
    rules:
      - host: algoritmes.overheid.nl
        http:
          paths:
            - path: /kader(/|$)(.*)
              pathType: ImplementationSpecific
              backend: { service: { name: algoritmekader, port: { number: 8080 } } }

(Op een ander platform: gelijkwaardige "strip path prefix /kader"-regel.)

Goed om te weten

  • De container is bewust prefix-agnostisch: nginx draait met absolute_redirect off, dus redirects (bv. trailing-slash) zijn relatief. De browser plakt het
    /kader/-prefix er zelf weer voor. Je hoeft de container dus niet te configureren met het prefix — alleen strippen aan de ingress-kant.
  • TLS termineert upstream (de container doet alleen HTTP op 8080). De container zet zelf wel al Strict-Transport-Security en de overige security-headers (CSP,
    X-Frame-Options, enz.) — die hoef je niet dubbel toe te voegen.
  • Health/readiness: een GET op de root van de container (/ op 8080) levert 200 met de homepage. Via de publieke route is dat …/kader/.

Verificatie na deploy

  1. …/kader/ → homepage met correcte opmaak (CSS/JS geladen, geen 404's in de network-tab).
  2. Diepe link, bv. …/kader/voldoen-aan-wetten-en-regels/vereisten/ → laadt direct.
  3. Zonder trailing slash, bv. …/kader/voldoen-aan-wetten-en-regels/vereisten → redirect naar de versie mét slash, met /kader/ behouden (niet naar algoritmes.overheid.nl/voldoen-…).
  4. Een niet-bestaand pad → de 404-pagina van het Algoritmekader (niet de generieke ingress-404).

Beslispunt

Kan/mag de ingress het prefix niet strippen? Dan moet de container in plaats daarvan op de root van het domein gehost worden en gebouwd worden met SITE_URL=https://algoritmes.overheid.nl/ (zónder /kader/). Dat wijzigt wel de publieke URL. Graag bevestigen welke van de twee het wordt.

@ravimeijerrig ravimeijerrig left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Is dit mogelijk?
Claude: github.event.repository.name behoudt de hoofdletters van de repo ("Algoritmekader"), terwijl GHCR de package lowercase opslaat ("algoritmekader/preview", want metadata-action lowercased bij de push). Als de API hoofdlettergevoelig is, matcht deze query niet en blijven afgesloten PR-images in GHCR staan (cleanup faalt stil door de2>/dev/null + exit 0). Indien dit speelt: lowercasen met CONTAINER_NAME="${CONTAINER_NAME,,}".

MkDocs Material genereert geen <base> element, dus getBasePath() viel altijd
terug op '' en de beslishulp 404'de op deployments met een pad-prefix (/kader,
/Algoritmekader). Het prefix wordt nu build-time uit site_url afgeleid en als
window.AK_BASE_PATH geinjecteerd, zodat het op elke deployment klopt.
De image werd per omgeving gebouwd (SITE_URL build-arg). Nu een omgevings-
onafhankelijke image: gebouwd met een vaste placeholder-site_url, waarna nginx
bij startup de echte origin + pad-prefix (uit $SITE_URL) injecteert via
envsubst + sub_filter -- in canonical/sitemap, de 404-assets en
window.AK_BASE_PATH. $SITE_URL afwezig => serveren op root. Werkt rootless en
op een read-only root filesystem (output naar tmpfs). De env wordt in de
ZAD-deployment-config gezet, niet build-time.
…tput halen

Het clusterdomein werd hardcoded in de workflow samengesteld. De ZAD deploy-
action geeft de URL zelf terug (steps.deploy.outputs.url) en plaatst de PR-
comment; de handmatige constructie (en de ongebruikte name/url-outputs) zijn
verwijderd. Alleen de image-tagprefix wordt nog afgeleid.
- docker/{setup-buildx@v4,login@v4,metadata@v6,build-push@v7}: lost de
  Node.js 20-deprecation op (deze majors draaien op Node 24).
- RijksICTGilde/zad-actions deploy+cleanup van v2 naar v4: v4 leest de
  deploy-URL uit de zad-cli JSON-output i.p.v. te raden, wat de
  'Could not extract URL from API response'-warning verhelpt.
Runtime-stage nu FROM ghcr.io/rijksictgilde/nginx-base (gepind op tag+digest):
dagelijks gepatcht, gesigned + SBOM, non-root, read-only-rootfs. We leveren een
eigen nginx.conf + server-template + security-headers.conf erbovenop -- met de
runtime SITE_URL-injectie behouden, en hun hardening overgenomen: GET/HEAD-only
(405), dotfiles geblokkeerd, asset-caching, en extra headers (object-src 'none',
base-uri/frame-ancestors 'self', COOP/CORP, X-Permitted-Cross-Domain-Policies)
naast onze strikte CSP.
…kerignore

Dependabot's docker-ecosystem detecteert alleen 'Dockerfile', niet
'Containerfile' -- nodig voor automatische base-image-updates. .dockerignore
wordt bovendien (anders dan .containerignore) wel door de docker-CI gebruikt.
Workflows, README en de ignore-zelfreferentie bijgewerkt; docker-ecosystem aan
dependabot.yml toegevoegd (directory /container).
De container serveerde op root en leunde op het strippen van /kader door de
proxy; daardoor misten redirects de prefix (/kader/x zonder slash -> /x/ -> 404)
en kon de kale /kader niet naar /kader/ redirecten (container zag enkel /).

Nu serveert nginx de prefix zelf via alias + de runtime BASE_PATH:
- location ${BASE_PATH}/ met alias, $uri houdt de volledige prefix -> directory-
  redirects behouden /kader;
- bare-prefix redirect /kader -> /kader/ (leeg bij root).

Vereist dat de OpenShift-route /kader ONGESTRIPT doorstuurt (pathType: Prefix,
geen rewrite-target) zodat de container de volledige /kader/... ziet. Gevalideerd
voor zowel root als /kader (incl. beslishulp, canonical/sitemap, 404, dotfiles).
…c naar https:

- preview-build-and-deploy: bouw/deploy geen previews voor bot-authored
  (Dependabot e.d.) of draft-PR's; push naar main blijft bouwen.
- CSP img-src: sta elke HTTPS-host toe zodat externe afbeeldingen in content
  (diagrammen, toegankelijkheidsverklaring-badge) laden zonder per-host allowlist.

@ravimeijerrig ravimeijerrig left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

docs/html/beslishulp.html:9 hardcodes data-fontPath="/Algoritmekader/assets/". The RijksSansWeb-*.woff2 files live at docs/assets/, so this only resolves on GitHub Pages (/Algoritmekader). In the container the site is under /kader (or /pr{N}/kader), and sub_filter only rewrites /ak-runtime-base — not /Algoritmekader. So the iframe requests /Algoritmekader/assets/RijksSansWeb-*.woff2 → 404, and the beslishulp falls back to default fonts. This is exactly the base-agnostic goal the rest of the branch achieves, so it stands out.

@uittenbroekrobbert wat denk jij?

@uittenbroekrobbert

Copy link
Copy Markdown
Collaborator

Dank Ravi — sinds je comment (10 juni) is de routing-aanpak bewust omgedraaid (commit f71727a8, "Container serveert de base-path zelf"). De instructie hierboven is daarmee op één punt achterhaald: het prefix /kader moet juist níét meer gestript worden.

Nieuwe situatie: de container krijgt SITE_URL=https://algoritmes.overheid.nl/kader mee en serveert het volledige /kader/-pad zélf (nginx location /kader/, plus bare-prefix-redirect /kader/kader/). De ingress moet het pad ongewijzigd doorzettenpathType: Prefix, géén rewrite-target. SITE_URL en het doorgestuurde prefix moeten hetzelfde zijn.

Publieke request Wat de container ontvangt (poort 8080)
…/kader/ GET /kader/
…/kader/voldoen-aan-wetten-en-regels/vereisten/ GET /kader/voldoen-aan-wetten-en-regels/vereisten/
…/kader/assets/stylesheets/main.css GET /kader/assets/stylesheets/main.css

Ingress-voorbeeld (nginx-ingress) — zónder rewrite:

spec:
  rules:
    - host: algoritmes.overheid.nl
      http:
        paths:
          - path: /kader
            pathType: Prefix
            backend: { service: { name: algoritmekader, port: { number: 8080 } } }

Health/readiness: let op — met een prefix gezet geeft GET / op de container een 404 (er is alleen een location /kader/). Richt de probe op /kader/ (publiek …/kader/), die geeft 200.

Onveranderd geldig uit je comment: TLS termineert upstream (container doet alleen HTTP op 8080); de container zet zelf alle security-headers (HSTS, CSP, X-Frame-Options, …) — niet dubbel toevoegen; redirects zijn relatief (trailing-slash blijft binnen /kader/); een onbekend pad geeft de Algoritmekader-404.

Je beslispunt: strippen is dus niet meer aan de orde. De gekozen weg is unstripped doorzetten. De root-variant (SITE_URL=https://algoritmes.overheid.nl/ zonder /kader/) blijft enkel een fallback als een subpad-route echt niet kan.

github.event.repository.name behoudt de repo-casing (Algoritmekader), maar
GHCR slaat de package lowercase op (algoritmekader/preview) en de packages-API
is hoofdlettergevoelig. Zonder lowercasen 404't de versions-query, wordt stil
ingeslikt door 2>/dev/null en blijven PR-images na sluiten in GHCR staan.
data-fontPath was hardcoded op /Algoritmekader/assets/, wat alleen op GitHub
Pages resolvet. In de container (onder /kader of root) gaf dat 404 -> fallback-
fonts, want sub_filter herschrijft alleen /ak-runtime-base, niet /Algoritmekader.
Met ../assets/ resolvet het pad relatief t.o.v. <base>/html/beslishulp.html en
klopt het op elke deployment (gh-pages /Algoritmekader/assets/, container
/kader/assets/, root /assets/). Lokaal in de container geverifieerd.
Bundle v1.2.15 leest data-fontPath niet (alleen showCloseOnEnd[Msg],
showExportPDF, showExplanationField) en bakt de fonts zelf in als
data:font/woff2. Het attribuut deed dus niets; weghalen voorkomt verwarring
en een misleidend pad. De overige data-* attributen blijven: die worden wel
gelezen.
@uittenbroekrobbert

Copy link
Copy Markdown
Collaborator

@ravimeijerrig uitgezocht tegen de echte bundle (v1.2.15): de beslishulp bakt z'n fonts zelf in als data:font/woff2 (RijksoverheidSansWebText/SerifWeb) en leest data-fontPath niet — alleen showCloseOnEnd[Msg], showExportPDF en showExplanationField worden gelezen (via currentScript.getAttribute("data-"+n)). Er was dus geen externe woff2-request en geen 404/fallback voor de beslishulp-fonts; de premisse klopte niet voor deze versie.

We hebben het dode data-fontPath-attribuut daarom verwijderd (83eb100a) i.p.v. het pad te repareren. De RijksSansWeb-fonts van de hoofdsite laden los daarvan al base-agnostisch via ../assets/ in extra.css.

@ravimeijerrig ravimeijerrig merged commit 46917ce into main Jun 17, 2026
5 checks passed
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.

3 participants