Harden container deployment lifecycle
deploy / deploy (push) Canceled after 0s

This commit is contained in:
Jens
2026-07-21 21:35:52 +02:00
parent a4eced8be5
commit 213bdb60a5
7 changed files with 62 additions and 9 deletions
+1 -1
View File
@@ -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"]
+4
View File
@@ -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"]
+4
View File
@@ -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"]
+13 -3
View File
@@ -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
+8 -2
View File
@@ -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.
+6 -3
View File
@@ -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 "$@"
@@ -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