roosterwijs/DEPLOY.md
Ramon f795e85333
Some checks failed
CI / backend (push) Has been cancelled
CI / frontend (push) Has been cancelled
Installatiescript met opslagkeuze, standaard /srv/server-up (v0.2.04 beta)
Nieuw: scripts/install.sh — installeert de stack in zeven zichtbare stappen
met drie vragen, standaard in /srv/server-up (aan te passen met
ROOSTERWIJS_DIR). Het script:
- controleert git, docker, de compose-plugin en of de daemon bereikbaar is;
- maakt de doelmap aan, of legt uit welk commando daarvoor nodig is als de
  rechten ontbreken (roept zelf geen sudo aan);
- kloont of werkt de checkout bij;
- laat kiezen waar de gegevens komen (zie hieronder);
- genereert .env met een willekeurige DJANGO_SECRET_KEY en een sterk
  databasewachtwoord, en zet hostnaam, CSRF-origin en DJANGO_SECURE op basis
  van twee vragen; een bestaande .env wordt nooit zomaar overschreven;
- bouwt en start de containers;
- meldt of er een account is en biedt aan er een aan te maken.

Opslagkeuze bij de installatie:
- docker-compose.yml gebruikt nu ${DATA_DB}, ${DATA_MEDIA} en ${DATA_STATIC}.
  Een naam = Docker-volume, een pad = gewone map naast de code.
- De standaardwaarden zijn de bestaande named volumes, dus installaties die
  al draaien veranderen niet en er gaan geen gegevens verloren.
- Kies je voor mappen, dan komt alles onder /srv/server-up/data/.

Verder:
- .gitignore: /data/ uitgesloten. Zonder dat zou de gegevensmap als lokale
  wijziging gelden en zou deploy.sh weigeren te draaien.
- deploy.sh maakt de gegevensmappen aan als de opslagkeuze paden gebruikt,
  zodat Docker ze niet als root aanmaakt.
- DEPLOY.md en README: installatie via het script, de opslagkeuze in een
  tabel, en de dump/restore-stappen die nodig zijn bij het omzetten van
  volumes naar mappen.

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

9.5 KiB

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:

.\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:

./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:

./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

Het installatiescript doet alles hieronder in zeven stappen en stelt drie vragen. Standaard komt alles in /srv/server-up.

git clone <jouw-forgejo-url>/roosterwijs.git /tmp/roosterwijs-install
/tmp/roosterwijs-install/scripts/install.sh

Heb je nog geen schrijfrechten op /srv, dan zegt het script welk commando je eenmalig als beheerder moet draaien. Een andere map kan met:

ROOSTERWIJS_DIR=/pad/naar/map ./scripts/install.sh

Het script controleert Docker, haalt de code op, vraagt waar de gegevens moeten staan, genereert .env met een willekeurige DJANGO_SECRET_KEY en een sterk databasewachtwoord, bouwt de containers en biedt aan een beheerder aan te maken.

De opslagkeuze

Bij stap 4 kies je waar database, media en statics terechtkomen:

Keuze Waar Wanneer
1. Docker-volumes door Docker beheerd (docker volume ls) standaard; afgeschermd, maar alleen bereikbaar via docker-commando's
2. Gewone mappen /srv/server-up/data/ alles zichtbaar naast de code, makkelijk te back-uppen en in te zien

De keuze staat als DATA_DB, DATA_MEDIA en DATA_STATIC in .env en is later te wijzigen. Let op: omzetten verplaatst geen gegevens. Wissel je van volumes naar mappen, maak dan eerst een dump en zet die daarna terug:

docker compose exec db pg_dump -U rooster rooster > ~/rooster.sql   # vóór het omzetten
# ... DATA_* aanpassen in .env, dan:
docker compose down && docker compose up -d db
cat ~/rooster.sql | docker compose exec -T db psql -U rooster rooster
docker compose up -d

Zonder die stap start de app met een lege database — de oude gegevens staan dan nog wel in het oude volume, maar worden niet meer gebruikt.

Handmatig, zonder script

git clone <jouw-forgejo-url>/roosterwijs.git /srv/server-up
cd /srv/server-up
cp .env.example .env
nano .env

Belangrijk om aan te passen:

  • DJANGO_SECRET_KEY — genereer met openssl rand -base64 48
  • DJANGO_ALLOWED_HOSTS — de hostnaam/IP van de 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
  • DATA_DB / DATA_MEDIA / DATA_STATIC — zie de tabel hierboven

.env staat in .gitignore en komt dus niet in git — dat hoort zo.

Starten

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

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

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:

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.