roosterwijs/DEPLOY.md
Ramon 500cf6c558
Some checks are pending
CI / backend (push) Waiting to run
CI / frontend (push) Waiting to run
Herstel installatie-instructies en hard deploy-script (v0.2.03 beta)
De installatiestappen in de README waren niet uitvoerbaar en het
deploy-script kon stilzwijgend werk vernietigen.

README:
- `python manage.py migrate` liep vast op `RuntimeError: DJANGO_SECRET_KEY
  ontbreekt`. DEBUG staat standaard uit en dan is die sleutel verplicht;
  de instructies noemden `DJANGO_DEBUG` nergens. Nu toegevoegd, met uitleg.
- `createsuperuser` stond als "optioneel, voor /admin/". Dat klopt niet: de
  hele API staat op `IsAuthenticated`, dus zonder account kom je de app
  helemaal niet in. Nu gemarkeerd als verplichte stap.
- Stap voor een virtual environment toegevoegd (pip installeerde anders in
  de systeem-Python, wat op recente distributies afketst op PEP 668).
- Node-eis vermeld: Vite 8 vereist 20.19+ of 22.12+.
- Verouderde beschrijvingen bijgewerkt: de tabs van het roosterscherm
  (Week/Genereren/Instellingen in plaats van Weekrooster/Conflicten/
  Schooljaar/Instellingen), Groepsindeling is een eigen scherm en geen tab,
  en er zijn twaalf modules in plaats van "twee voorbeeldmodules".

DEPLOY.md:
- `git clone .../Roostersoftware.git` + `cd Roostersoftware` gebruikte de
  verkeerde repo-naam; dat is `roosterwijs`.
- CSRF-origin en FRONTEND_BASE_URL stonden als http-voorbeeld; achter TLS
  moet dat https zijn.
- Rechtgezet: `DJANGO_CSRF_TRUSTED_ORIGINS` blokkeert het inloggen in de app
  niet (DRF-views zijn `csrf_exempt`), maar wel de Django-admin.
- Vermeld dat er nergens automatisch een account wordt aangemaakt.

scripts/deploy.sh:
- Stopt nu als `.env` ontbreekt; anders start de stack met lege variabelen
  en faalt de backend met een onduidelijke fout.
- Stopt bij lokale wijzigingen in plaats van ze met `git reset --hard`
  weg te gooien; bewust overschrijven kan met `--force`.
- Controleert vooraf of Docker en de compose-plugin er zijn.
- Toont welke commits erbij komen (oud -> nieuw) in plaats van alleen de
  nieuwe HEAD.
- Controleert na afloop of de backend antwoordt en of er een actief account
  bestaat, met het createsuperuser-commando als dat er niet is.

frontend/Dockerfile:
- Bouwt op node:22-alpine in plaats van node:20-alpine, gelijk aan de CI en
  ruim binnen de Node-eis van Vite 8.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FAQ4Np13v8fwbTtKHKnwDz
2026-08-19 14:57:49 +02:00

212 lines
8 KiB
Markdown

# Deploy-workflow (kort)
> **Belangrijk — gebruik de scripts.** Eerdere mislukte pushes kwamen door een
> achtergebleven `.git/index.lock` die `git commit` blokkeerde (dan valt er
> niets te pushen en zegt git "up-to-date"). De scripts ruimen dat automatisch
> op en **controleren** of de push echt is doorgekomen.
**1. Op je dev-machine** (Windows / PowerShell), in de repo-map:
```powershell
.\scripts\push.ps1 "korte omschrijving van de wijziging"
```
Dit verwijdert een eventuele lock, commit (alleen als er iets is), pusht naar
`origin/<branch>` en meldt **OK** of **LET OP** (met reden) — geen stille
mislukkingen meer. Krijg je "LET OP", dan staat de oorzaak in de git-uitvoer
erboven (meestal een SSH-/sleutelprobleem).
**2. Op de test-server**, in de repo-map:
```bash
./scripts/deploy.sh
```
Dit controleert eerst of Docker en `.env` aanwezig zijn, haalt dan de laatste
code op (`git reset --hard origin/<branch>`), herbouwt de containers (incl.
migraties via de entrypoint) en controleert daarna of de frontend-bundel er
staat, of de backend antwoordt en of er een actief account is om mee in te
loggen.
Staan er **lokale wijzigingen** in de map op de server, dan stopt het script:
`git reset --hard` zou die weggooien. Zet ze eerst veilig, of forceer bewust:
```bash
./scripts/deploy.sh --force
```
Nieuwe **modules** verschijnen daarna pas in de app nadat je ze aanzet via
**⚙ → Modulebeheer**.
---
# E-mail aanzetten (wachtwoordreset-links)
Zonder configuratie gaan e-mails naar de **console-log** van de backend-container
(zichtbaar met `docker compose logs backend`) — prima om te testen. Voor échte
e-mail via SMTP zet je in `.env` op de server:
```
DJANGO_EMAIL_BACKEND=django.core.mail.backends.smtp.EmailBackend
EMAIL_HOST=smtp.jouwprovider.nl
EMAIL_PORT=587
EMAIL_HOST_USER=postvak@jouwschool.nl
EMAIL_HOST_PASSWORD=...
EMAIL_USE_TLS=1
DEFAULT_FROM_EMAIL=Roosterwijs <noreply@jouwschool.nl>
FRONTEND_BASE_URL=https://rooster.example.nl
```
`FRONTEND_BASE_URL` bepaalt de basis van de resetlink in de e-mail (zonder
`FRONTEND_BASE_URL` wordt die afgeleid uit het verzoek). Daarna
`docker compose up -d` (of `./scripts/deploy.sh`) om de nieuwe waarden te laden.
---
# Draaien op de test-server (Docker)
De stack bestaat uit drie containers:
| Container | Rol |
|-----------|-----|
| `db` | PostgreSQL (data in een Docker-volume) |
| `backend` | Django via gunicorn |
| `nginx` | serveert de React-frontend en stuurt `/api` + `/admin` door naar de backend |
Je hoeft op de server **niets** te installeren behalve Docker zelf (met de
`docker compose`-plugin). Python, Node, Postgres enz. zitten in de images.
## Eenmalig: server klaarzetten
1. Zorg dat Docker + Compose op de test-server staan:
```bash
docker --version
docker compose version
```
2. Haal de code op:
```bash
git clone <jouw-forgejo-url>/roosterwijs.git
cd roosterwijs
```
3. Maak het `.env`-bestand aan vanuit het voorbeeld en vul echte waarden in:
```bash
cp .env.example .env
nano .env
```
Belangrijk om aan te passen:
- `DJANGO_SECRET_KEY` — genereer met
`python3 -c "import secrets; print(secrets.token_urlsafe(50))"`
- `DJANGO_ALLOWED_HOSTS` — de hostnaam/IP van de test-server
- `DJANGO_CSRF_TRUSTED_ORIGINS` — exact de origin die de browser gebruikt,
dus `https://<host>` achter TLS (of `http://<host>:8080` zonder TLS)
- `POSTGRES_PASSWORD` — een sterk wachtwoord
`.env` staat in `.gitignore` en komt dus **niet** in git — dat hoort zo.
## Starten
```bash
docker compose up -d --build
```
Wat er dan gebeurt: Postgres start, de backend wacht tot de database er is,
voert automatisch de migraties en `collectstatic` uit, en gunicorn + nginx
komen op. De app is bereikbaar op:
```
http://<server-host>:8080
```
(Poort `8080` staat ingesteld in `docker-compose.yml` bij de `nginx`-service;
pas hem daar aan als die poort bezet is.)
### Eerste keer: beheerder aanmaken
```bash
docker compose exec backend python manage.py createsuperuser
```
Dit account heb je **hoe dan ook** nodig: de hele API staat op
`IsAuthenticated`, dus zonder gebruiker kom je ook de app zelf niet in — niet
alleen `/admin/`. Zonder dit commando wordt er nergens automatisch een account
aangemaakt; `entrypoint.sh` draait alleen migraties en `collectstatic`.
## Updaten na nieuwe code
```bash
git pull
docker compose up -d --build
```
Migraties draaien automatisch mee bij het opstarten van de backend.
## Handige commando's
| Doel | Commando |
|------|----------|
| Logs volgen | `docker compose logs -f` |
| Alleen backend-logs | `docker compose logs -f backend` |
| Status containers | `docker compose ps` |
| Stoppen | `docker compose down` |
| Stoppen **incl. database wissen** | `docker compose down -v` (let op: gegevens weg!) |
| Migratie handmatig | `docker compose exec backend python manage.py migrate` |
| Django-shell | `docker compose exec backend python manage.py shell` |
## Aandachtspunten
- **HTTPS/domein:** deze opzet draait op poort 8080 zonder TLS. Voor productie
zet je er een reverse proxy (bv. Caddy of Traefik) vóór die HTTPS regelt, of
je breidt de nginx-config uit met certificaten. De nginx-container is daar al
het logische punt voor. **Zet daarna `DJANGO_SECURE=1` in `.env`** — dat
activeert veilige cookies, HSTS en een https-redirect in Django.
Zie hieronder wat er dan verder moet kloppen.
### Draaien achter TLS: checklist
Met `DJANGO_SECURE=1` moeten drie dingen kloppen, anders lukt **inloggen niet**
terwijl het inlogscherm zelf gewoon laadt (dat wordt door nginx geserveerd,
zonder tussenkomst van Django).
1. **De proxy moet `X-Forwarded-Proto: https` meesturen.** Django leest die
header (`SECURE_PROXY_SSL_HEADER`) om te bepalen of de verbinding veilig is.
Ontbreekt hij, dan denkt Django dat het http is en stuurt het elk verzoek
naar `/api` en `/admin` door naar https — waarna de proxy het weer als http
aanlevert: een oneindige redirect-lus (`ERR_TOO_MANY_REDIRECTS`) en dus een
mislukte login. Caddy en Traefik zetten deze header standaard; controleer het
als je een eigen nginx/Apache ervoor hebt.
2. **`DJANGO_CSRF_TRUSTED_ORIGINS` moet de https-origin zijn**, exact zoals de
browser hem gebruikt (bv. `https://rooster.example.nl`, zonder poort op 443).
Dit geldt voor de **Django-admin** en andere gewone Django-formulieren: staat
hier een `http://`-adres, dan weigert `/admin/` je aanmelding met een
CSRF-fout (HTTP 403). Het inloggen in de app zelf loopt hier níet op stuk —
DRF-views zijn `csrf_exempt` en `SessionAuthentication` controleert CSRF pas
als er al een sessie is.
3. **`DJANGO_ALLOWED_HOSTS` moet de publieke hostnaam bevatten**, anders volgt
een 400 Bad Request.
Controleren of de header goed aankomt:
```bash
docker compose logs backend | grep -i "Mislukte inlogpoging" # bereikt de login de backend?
curl -sI https://<jouw-domein>/api/auth/me/ | head -n 1 # 200/401 = goed, 301 = lus
```
Krijg je bij die `curl` een `301`, dan komt `X-Forwarded-Proto` niet goed door.
- **Beperk poort 8080 tot de proxy:** in `docker-compose.yml` staat
`"8080:80"`, wat op álle netwerkinterfaces luistert. Daardoor is de app ook
rechtstreeks via `http://<server>:8080` te bereiken — buiten TLS om. Draait je
reverse proxy op dezelfde machine, maak er dan `"127.0.0.1:8080:80"` van.
Draait de proxy op een andere host, laat het dan staan en zet er een firewall
op: nginx vertrouwt de `X-Forwarded-Proto` van de aanroeper.
- **Inloggen verplicht:** de hele API vereist een ingelogde gebruiker; de
frontend toont eerst een inlogscherm. Maak gebruikers aan via
`createsuperuser` (zie boven) of in de Django-admin. Modulebeheer is
voorbehouden aan beheerders (`is_staff`).
- **Backups:** de database leeft in het volume `pgdata`. Maak hiervan backups,
bijvoorbeeld met `docker compose exec db pg_dump -U rooster rooster > backup.sql`.
- **`DJANGO_DEBUG` blijft 0** op de server. Zet hem nooit op 1 in productie.
- **Test eerst hier, dan productie:** bouw het image één keer, test op deze
server, en draai exact dezelfde code/compose op de productieserver met een
eigen `.env`.