# Deployment

Vereisten, Docker, meerdere containers en eigen hosting.

Nimbly ondersteunt zowel deployment in containers als handmatig op een VPS. Containers verpakken de app en de runtime samen voor herhaalbare updates via images. Handmatig deployen op een VPS is net zo goed een optie voor zelfbeheerde Apache/PHP-servers waar je liever directe controle over de server houdt. Kies het model dat past bij hoe de site wordt beheerd.

Elke site antwoordt op `/health` voor de controle van een load balancer of container: `200` met `ok` als PHP draait en `ext/data` beschrijfbaar is, anders `503`. Het antwoord wordt nooit gecachet. Een route `ext/uri/health` vervangt het.

Achter een load balancer die TLS afhandelt, noem je zijn adres in `.env`:

```env
TRUSTED_PROXIES=10.0.0.0/8
```

Adressen en bereiken, gescheiden door komma's. Een request dat rechtstreeks van een van die adressen komt, krijgt zijn schema, poort en bezoekersadres uit `X-Forwarded-Proto`, `X-Forwarded-Port` en `X-Forwarded-For`, zodat de sessiecookie `secure` is, absolute links met `https://` beginnen en de inlogpauze en de statistieken de bezoeker tellen in plaats van de balancer. Vanaf elk ander adres, en zonder deze instelling, worden die headers genegeerd.

Het eigen toegangslog van de webserver, en wat dat log leest (de botregels hieronder), heeft ook het adres van de bezoeker nodig. De containerimages regelen dat bij het starten vanuit dezelfde instelling. Zet op een eigen server `mod_remoteip` aan en zet de regels die dit commando toont in de `<VirtualHost>` van de site (niet in `.htaccess`), en doe dat opnieuw als de instelling verandert:

```bash
sudo a2enmod remoteip
./nimbly host:proxy-conf
```

Een ban op adres houdt achter een load balancer niets tegen: de verbinding komt van de balancer. Laat de balancer of zijn firewall de adressen weigeren die de regels vinden.

## Bots

Bots zijn welkom als ze `robots.txt` volgen, alleen adressen opvragen die de site heeft gepubliceerd, en hooguit één pagina per seconde lezen. Nimbly antwoordt correct en laat het bannen aan de server over:

- **Tripwire.** Elke site heeft één adres, `/nb-` plus twaalf tekens die van de `PEPPER` zijn afgeleid, dat `robots.txt` verbiedt en waar elke pagina onzichtbaar naar linkt (`[#bot-tripwire-link#]` in de pagina-shell). Een request daarvoor krijgt status `418`. Het adres is per site vast en verschilt tussen sites; een eigen pagina-shell heeft de shortcode ook nodig.
- **Serverregels.** `core/cli/setup/fail2ban/` bevat drie fail2ban-filters en één jail-bestand voor een server met Apache-toegangslogs: een ban bij elk `418`-antwoord, bij meer dan 60 paginarequests per minuut, en bij 20 keer niet gevonden in tien minuten. Afbeeldingen, styles, scripts, API-aanroepen en adminschermen zijn geen paginarequests, en een ontbrekende afbeelding, style, script of document telt niet als niet gevonden: dat is een kapotte link van de site zelf. Kopieer de filters naar `/etc/fail2ban/filter.d/` en het jail-bestand naar `/etc/fail2ban/jail.d/`, zet je eigen servers in `ignoreip`, en draai elk filter met `fail2ban-regex` tegen je logs voordat je het aanzet.

## Deployment met Docker

Maak vanuit de projectcheckout de bestanden voor het app-image in `ext/`:

```bash
./nimbly docker:init
```

Commit de gemaakte `ext/Dockerfile`, `ext/.dockerignore` en `ext/.github/workflows/docker-publish.yml` in de repository `ext/`. Bij een push bouwt GitHub Actions het app-image vanuit het actuele Nimbly-image `master` en publiceert het naar GHCR. Core publiceert doorlopend `ghcr.io/nimbly-web-systems/nimbly-core:master`; de gemaakte applicatiebuilds gebruiken dat image en clonen de bijbehorende `master`-bron voor hun asset-build.

Om deployment lokaal te testen zonder te wachten tot GitHub Actions een nieuw core-image publiceert, bouw je een lokaal core-image en geef je dat mee als basis voor de app-build:

```bash
docker build -t nimbly-core-local:master -f docker/prod/Dockerfile .
docker build -t nimbly-app-local:test \
  --build-arg NIMBLY_BASE_IMAGE=nimbly-core-local:master \
  -f ext/Dockerfile ext
```

Kies vóór het deployen waar de productiedata leidend is:

- **Onveranderlijk van ontwikkeling naar live:** bewerk content in ontwikkeling, commit die naar `ext/` en deploy een nieuw image. Productie wordt niet als editor gebruikt. Er is geen Git-token met schrijfrechten nodig.
- **Live bewerken:** redacteuren wijzigen content, uploads of records op de productiesite. In deze modus schrijft productie naar `ext/data/`, en die wijzigingen moeten terug naar de repository `ext/` worden gepusht voordat de volgende deploy de container vervangt.

Voor live bewerken moet het productie-image `ext/.git` bevatten en moet de container `GIT_TOKEN` krijgen. De schema's van prod en stage bevatten `ext:sync`, dat wijzigingen in de data commit en terug pusht naar de repository `ext/`. Gebruik voor GitHub een fine-grained token dat alleen geldt voor de repository `ext/`, met **Contents: Read and write**. De container stelt bij het starten de Git-inloggegevens in vanuit dit token; commit het token niet en bak het niet in het image.

Voor lokale tests met Docker of een zelfbeheerde VPS is een ongevolgd env-bestand prima:

```bash
docker run --rm --env-file .env.prod ghcr.io/your-org/your-nimbly-app:main
```

Houd `.env`, `.env.prod`, `.env.stage` en vergelijkbare bestanden buiten Git. Commit alleen voorbeeldbestanden zoals `.env.example` of `.env.prod.example` met nepwaarden. Stel op beheerde containerplatforms deze waarden in als runtime-omgevingsvariabelen of geheimen:

- **AWS ECS/Fargate:** bewaar geheimen in AWS Secrets Manager of SSM Parameter Store en verwijs ernaar vanuit het blok `secrets` van de task definition.
- **AWS App Runner:** stel runtime-omgevingsvariabelen en geheimen in op de service.
- **Andere containerhosts:** gebruik het mechanisme voor geheimen en omgevingsvariabelen van het platform, niet de build van het Docker-image.

Minimaal Compose-voorbeeld voor productie:

```yaml
services:
  web:
    image: ghcr.io/your-org/your-nimbly-app:main
    restart: unless-stopped
    env_file:
      - .env.prod
    ports:
      - "80:80"
    environment:
      APP_ENV: prod
      SITE_URL: https://example.com
      SITE_NAME: Example
      MAIL_SERVICE: resend
      MAIL_FROM: no-reply@example.com
      MAIL_FROM_NAME: Example
    volumes:
      - nimbly-data:/var/www/nimbly/ext/data

volumes:
  nimbly-data:
```

Wordt productiecontent ter plekke bewerkt, zet `.git` dan niet in `ext/.dockerignore`. Zonder `ext/.git` of `GIT_TOKEN` start de container nog wel, maar kan `ext:sync` live wijzigingen niet pushen, en kunnen die wijzigingen bij de volgende deploy van een image verloren gaan.

Updates van containers zijn updates van images: push `ext/`, laat de workflow een nieuw image publiceren, pull het image op de host en herstart de container. De container draait `system:setup` bij het starten en draait elke minuut de scheduler. Zorg in de modus voor live bewerken dat `ext:sync` de actuele productiedata heeft gepusht voordat je de container vervangt.

## Meerdere containers

Een site kan in meerdere identieke containers achter een load balancer draaien. Daar is geen instelling voor: alles wat een site onthoudt, is een bestand onder `ext/` (records, uploads, sessies, jobs, de status van de scheduler, requeststatistieken, caches), dus de containers hoeven alleen die map te delen.

- **Eén volume.** Koppel hetzelfde volume op `/var/www/nimbly/ext/data` in elke container. Koppel bij live bewerken in plaats daarvan de hele `ext/`, zodat er één Git-checkout is voor `ext:sync` en niet één per container. Het volume moet bestandslocks (`flock`) ondersteunen: NFSv4 en Amazon EFS doen dat.
- **Dezelfde `.env`.** Elke container krijgt dezelfde `PEPPER` en instellingen.
- **Load balancer.** Laat zijn controle naar `/health` wijzen en zet `TRUSTED_PROXIES` (hierboven) op zijn adres. Sticky sessions zijn niet nodig: een login op de ene container geldt ook op de andere.
- **Scheduler.** Elke container draait hem elke minuut; een lock op het gedeelde volume laat één ervan het werk doen, zodat een taak één keer draait.
- **Schrijven.** Wijzigingen aan de records van een resource gebeuren één voor één over alle containers heen, dus twee redacteuren die hetzelfde record opslaan, verliezen niets.

```yaml
services:
  web:
    image: ghcr.io/your-org/your-nimbly-app:main
    restart: unless-stopped
    env_file:
      - .env.prod
    deploy:
      replicas: 2
    volumes:
      - nimbly-data:/var/www/nimbly/ext/data
      - nimbly-thumbs:/var/www/nimbly/ext/static/_thumb_

volumes:
  nimbly-data:
    driver_opts:
      type: nfs
      o: addr=10.0.0.10,nfsvers=4.1,rw
      device: ":/nimbly/example/data"
  nimbly-thumbs:
    driver_opts:
      type: nfs
      o: addr=10.0.0.10,nfsvers=4.1,rw
      device: ":/nimbly/example/thumbs"
```

Thumbnails worden gecachet in `ext/static/_thumb_`. Koppel daar ook een tweede gedeeld volume: anders maakt elke container zijn eigen thumbnails, en blijven de thumbnails van een verwijderde afbeelding achter op de containers die het verwijderen niet afhandelden.

Elke container draait bij het starten `system:setup` en maakt daarna `www-data` eigenaar van de bestanden van de site, het gedeelde volume inbegrepen. Het volume moet root dus eigenaren laten wijzigen (op NFS: `no_root_squash`, of een export die al van `www-data` is), en een volume met heel veel bestanden maakt het starten traag. Twee containers mogen tegelijk op een leeg volume starten.

`core/tests/multi-node.sh` probeert dit uit op een werkstation: het start een wegwerpsite in twee containers op één map (`docker/dev/docker-compose.nodes.yml`) en controleert de login, gelijktijdig schrijven naar één record, de scheduler en de requeststatistieken. Het heeft alleen op een lokale schijf gedraaid, niet op een netwerkvolume en niet achter een echte load balancer.

## Handmatige deployment op een VPS

Handmatig deployen op een VPS wordt ondersteund voor zelfbeheerde installaties. De basis voor een release is dezelfde weg als in CI:

```bash
./nimbly deps
APP_ENV=prod SITE_NAME="My Site" ADMIN_EMAIL=admin@example.com ADMIN_PASSWORD="change-me" PEPPER="$PEPPER" ./nimbly system:setup
./nimbly build
./nimbly help
find core -name '*.php' -print0 | xargs -0 -n1 php -l
```

Is er geen PHP op de host, gebruik dan de CLI en PHP-lint via Docker:

```bash
APP_ENV=prod SITE_NAME="My Site" ADMIN_EMAIL=admin@example.com ADMIN_PASSWORD="change-me" PEPPER="$PEPPER" ./nimbly --docker system:setup
docker compose -f docker/dev/docker-compose.yml run --rm --build nimbly sh -lc "find core -name '*.php' -print0 | xargs -0 -n1 php -l"
```

Houd `.env` op de doelserver en zorg dat die de `APP_ENV` voor productie, een vaste `PEPPER`, de juiste `SITE_URL` en de mailinstellingen bevat. `system:setup` kun je veilig opnieuw draaien, ook als een deployment ontbrekende mappen, routes, rollen, `.jobs` of `.htaccess` moet aanmaken.

## Scheduler en jobs

Installeer bij handmatige deployments op een VPS de scheduler-orchestrator één keer per server. Registreer elk gedeployd project; planning is ook nodig zonder taken van de applicatie zelf:

```bash
sudo php /var/www/site/core/cli/nimbly.php scheduler:install
sudo php /var/www/site/core/cli/nimbly.php scheduler:project:add site /var/www/site
sudo php /var/www/site/core/cli/nimbly.php scheduler:cron:install --user=www-data
```

Dit maakt `/etc/cron.d/nimbly-scheduler`, dat elke minuut `/usr/local/bin/nimbly-scheduler-orchestrator` draait als `www-data`, en `/etc/logrotate.d/nimbly-scheduler`, dat het schedulerlog wekelijks roteert. De orchestrator start daarna de schedulers van de geregistreerde projecten los van elkaar. In Docker-containers draait de scheduler vanzelf, dus daar is geen cron op de host nodig.

Draai `schedule:init` één keer om de schemabestanden van het project in `ext/cli/` te maken:

```bash
./nimbly schedule:init
```

Dit maakt drie bestanden:

| Bestand | Gebruikt als |
|---|---|
| `ext/cli/schedule.inc` | `APP_ENV=dev` of geen bestand voor de omgeving gevonden |
| `ext/cli/schedule.prod.inc` | `APP_ENV=prod` |
| `ext/cli/schedule.stage.inc` | `APP_ENV=stage` |

De schema's van prod en stage bevatten `ext:sync`, dat elk uur wijzigingen in de data van `ext/` commit en terug pusht naar de repository. Het schema van dev niet. Pas elk bestand apart aan voor taken die per omgeving verschillen.

Bij het overzetten van een bestaande VPS installeer je eerst de orchestrator, registreer je de projecten, draai je `scheduler:run --dry-run`, draai je de wrapper één keer met de hand als `www-data`, en controleer je `/var/log/nimbly-scheduler.log`. Maak een back-up van de oude crontab van `www-data` onder `/etc/nimbly/backups/`, en haal daarna alleen de overgezette regels `schedule:run` per project weg. Haal geen cronjobs weg die er niets mee te maken hebben.

`git pull` vanuit de admin blijft beschikbaar onder Settings als eenvoudige manier om een zelfbeheerde site bij te werken. Kies voor sites met een releaseproces liever een herhaalbare deployment: pull de bedoelde Git-revisie, draai setup, build en lint, en zorg dat de scheduler actief is.

---
