From 213bdb60a53d31f3ed23aaf5632471c33b184563 Mon Sep 17 00:00:00 2001 From: Jens Date: Tue, 21 Jul 2026 21:35:52 +0200 Subject: [PATCH] Harden container deployment lifecycle --- Dockerfile | 2 +- docker-compose.unraid.yml | 4 ++++ docker-compose.yml | 4 ++++ docs/ai/PROJECT_STATE.md | 16 ++++++++++--- scripts/deploy_docker.sh | 10 ++++++-- scripts/entrypoint.sh | 9 ++++--- tests/unit/test_deployment_configuration.py | 26 +++++++++++++++++++++ 7 files changed, 62 insertions(+), 9 deletions(-) create mode 100644 tests/unit/test_deployment_configuration.py diff --git a/Dockerfile b/Dockerfile index 8d7b09e..ee96343 100644 --- a/Dockerfile +++ b/Dockerfile @@ -35,4 +35,4 @@ USER app EXPOSE 8080 ENTRYPOINT ["/app/scripts/entrypoint.sh"] -CMD ["gunicorn", "config.wsgi:application", "--bind", "0.0.0.0:8080", "--workers", "2", "--threads", "4", "--timeout", "90"] +CMD ["gunicorn", "config.wsgi:application", "--bind", "0.0.0.0:8080", "--workers", "2", "--threads", "4", "--timeout", "90", "--no-control-socket"] diff --git a/docker-compose.unraid.yml b/docker-compose.unraid.yml index fc60da7..c07280b 100644 --- a/docker-compose.unraid.yml +++ b/docker-compose.unraid.yml @@ -30,11 +30,15 @@ services: ports: ["1226:8080"] worker: <<: *app + environment: + VACATURERADAR_RUN_MIGRATIONS: "0" cap_drop: - ALL command: ["celery", "-A", "config", "worker", "-l", "INFO", "-Q", "high,default,low", "--concurrency=2"] scheduler: <<: *app + environment: + VACATURERADAR_RUN_MIGRATIONS: "0" cap_drop: - ALL command: ["celery", "-A", "config", "beat", "-l", "INFO", "--schedule", "/tmp/celerybeat-schedule"] diff --git a/docker-compose.yml b/docker-compose.yml index 1cdc366..af5bdcf 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -41,6 +41,8 @@ services: worker: <<: *app + environment: + VACATURERADAR_RUN_MIGRATIONS: "0" cap_drop: - ALL command: ["celery", "-A", "config", "worker", "-l", "INFO", "-Q", "high,default,low", "--concurrency=2"] @@ -53,6 +55,8 @@ services: scheduler: <<: *app + environment: + VACATURERADAR_RUN_MIGRATIONS: "0" cap_drop: - ALL command: ["celery", "-A", "config", "beat", "-l", "INFO", "--schedule", "/tmp/celerybeat-schedule"] diff --git a/docs/ai/PROJECT_STATE.md b/docs/ai/PROJECT_STATE.md index f656a5e..731b98c 100644 --- a/docs/ai/PROJECT_STATE.md +++ b/docs/ai/PROJECT_STATE.md @@ -23,14 +23,23 @@ De repository bevat een uitvoerbare Django-MVP met: Release-audit op 2026-07-21: -- 144 geslaagde tests en 2 lokaal overgeslagen Playwright-varianten; de HTML/a11y-fallback is geslaagd; +- 146 geslaagde tests en 2 lokaal overgeslagen Playwright-varianten; de HTML/a11y-fallback is geslaagd; - 81,98% branch-aware codedekking over `apps` en `config`; - Ruff en Django system checks geslaagd; - geen ontbrekende migraties; - backlog- en repositoryvalidatie geslaagd; - de lockfile gebruikt publieke PyPI-bronnen in plaats van een niet-overdraagbare interne registry; - releaseblokkers hersteld in template-rendering, JSON/BOM-verwerking, leases, feedback, dossierautorisatie, ATS-provenance, employer-resolutie en DNS-rebindcontrole; -- een echte Dockerimagebuild en live server-smoke blijven afhankelijk van de Gitea-runner/server, omdat Docker lokaal niet beschikbaar is. +- een echte Dockerimagebuild op de Unraid-server, migratiecontrole en live LAN-smoke voor liveness, readiness, login en static assets zijn geslaagd; +- deploymenthardening voorkomt gelijktijdige migraties door worker/scheduler, schakelt de Gunicorn-controlsocket uit voor de read-only container en houdt de lokale healthcheckhost toegestaan. + +## Deploymentstatus + +- `main` is via de bestaande Gitea-SSH-sleutel naar `NuklearRabbit/VacatureRadar` gepusht. +- De applicatie draait op de Unraid-server via `http://192.168.10.150:1226/` met PostgreSQL, Redis, web, worker en scheduler. +- De server-side `.env` heeft rechten `0600`; gegenereerde secrets zijn niet naar Git of logs gekopieerd. +- De self-hosted Gitea Actions-runner is niet actief. Deze sessie gebruikte daarom een directe, commitgebonden SSH-deploy; push-to-deploy vereist nog runnerherstel/registratie. +- De huidige uitrol is alleen geschikt voor het beheerde LAN. Publieke productie vereist eerst een gekozen domein, reverse proxy/TLS en `DJANGO_DEBUG=0` met secure cookies. ## Laatste uitgevoerde backlogtaak @@ -142,11 +151,12 @@ Deze blokkeren onafhankelijke code niet: - live IMAP/SMTP-account en app-password; - Unraid-domein, DNS, TLS en reverse proxy; - containerregistry en credentials; +- actieve/voor deze repository geregistreerde Gitea Actions-runner; - keuze/verkrijging van een gelicenseerde actuele Belgische postcode-/gemeentedataset. ## Bekende verificatiebeperking van de aangeleverde basis -De Pythonapplicatie en tests zijn lokaal geverifieerd. Een echte Dockerimagebuild en volledige PostgreSQL/Redis/Unraid-smoke vereisen nog steeds een omgeving met Docker; daarom is dat nog niet als uitgevoerd te claimen in deze workspace. +De Pythonapplicatie, Dockerimage en PostgreSQL/Redis/Unraid-stack zijn geverifieerd. De Playwright-browservarianten zijn lokaal niet uitgevoerd; hun HTML/a11y-fallback is wel geslaagd. Een publieke HTTPS-smoke blijft afhankelijk van domein-, DNS- en reverse-proxyconfiguratie. ## Hervatten diff --git a/scripts/deploy_docker.sh b/scripts/deploy_docker.sh index 79090af..551747d 100644 --- a/scripts/deploy_docker.sh +++ b/scripts/deploy_docker.sh @@ -73,6 +73,14 @@ app_scheme="${APP_SCHEME:-http}" app_hostnames="${APP_HOSTS:-$app_host}" app_origin="${APP_ORIGINS:-}" +# De lokale healthcheck gebruikt 127.0.0.1. Houd die host en localhost altijd +# toegestaan, ook wanneer APP_HOSTS expliciet door de runner is ingesteld. +for required_host in 127.0.0.1 localhost; do + if [[ ",$app_hostnames," != *",$required_host,"* ]]; then + app_hostnames="${app_hostnames},${required_host}" + fi +done + if [[ -z "$app_origin" ]]; then if [[ "$app_scheme" == "https" ]]; then if [[ "$app_port" == "443" ]]; then @@ -149,8 +157,6 @@ if [[ "${SKIP_HEALTHCHECK:-0}" != "1" ]]; then done fi -docker compose -f "$compose_file" exec -T web python manage.py collectstatic --noinput - cat <<"EOF" Deploy klaargezet. diff --git a/scripts/entrypoint.sh b/scripts/entrypoint.sh index de7af60..af8bdc4 100644 --- a/scripts/entrypoint.sh +++ b/scripts/entrypoint.sh @@ -3,10 +3,13 @@ set -eu mkdir -p /app/media /app/logs /app/local mkdir -p /tmp/celery -python manage.py migrate --noinput -if [ "${VACATURERADAR_AUTO_BOOTSTRAP:-0}" = "1" ]; then - python manage.py bootstrap_instance || true +if [ "${VACATURERADAR_RUN_MIGRATIONS:-1}" = "1" ]; then + python manage.py migrate --noinput + + if [ "${VACATURERADAR_AUTO_BOOTSTRAP:-0}" = "1" ]; then + python manage.py bootstrap_instance || true + fi fi exec "$@" diff --git a/tests/unit/test_deployment_configuration.py b/tests/unit/test_deployment_configuration.py new file mode 100644 index 0000000..3e6afec --- /dev/null +++ b/tests/unit/test_deployment_configuration.py @@ -0,0 +1,26 @@ +from pathlib import Path + +import yaml + +ROOT = Path(__file__).resolve().parents[2] + + +def test_only_web_runs_migrations_in_compose() -> None: + for filename in ("docker-compose.yml", "docker-compose.unraid.yml"): + compose = yaml.safe_load((ROOT / filename).read_text(encoding="utf-8")) + services = compose["services"] + + assert ( + services["web"].get("environment", {}).get("VACATURERADAR_RUN_MIGRATIONS", "1") == "1" + ) + assert services["worker"]["environment"]["VACATURERADAR_RUN_MIGRATIONS"] == "0" + assert services["scheduler"]["environment"]["VACATURERADAR_RUN_MIGRATIONS"] == "0" + + +def test_runtime_is_compatible_with_read_only_container() -> None: + dockerfile = (ROOT / "Dockerfile").read_text(encoding="utf-8") + deploy_script = (ROOT / "scripts" / "deploy_docker.sh").read_text(encoding="utf-8") + + assert '"--no-control-socket"' in dockerfile + assert "exec -T web python manage.py collectstatic" not in deploy_script + assert "for required_host in 127.0.0.1 localhost" in deploy_script