From 817a7cdf77fe8e4c505523499437be7a7736398d Mon Sep 17 00:00:00 2001 From: claude-macmini Date: Tue, 8 Sep 2026 18:56:30 +0200 Subject: [PATCH] OpnForm docker-compose deploy-config (forms.doc-gen.eu) Coolify docker-compose (git-based), exact gepinde image-tags. GEEN route B / geen registry-image / geen host-watcher. Zie README.md. Co-Authored-By: Claude Opus 4.8 --- README.md | 81 ++++++++++++++++++++ docker-compose.yml | 184 +++++++++++++++++++++++++++++++++++++++++++++ docker/nginx.conf | 61 +++++++++++++++ 3 files changed, 326 insertions(+) create mode 100644 README.md create mode 100644 docker-compose.yml create mode 100644 docker/nginx.conf diff --git a/README.md b/README.md new file mode 100644 index 0000000..e0cd929 --- /dev/null +++ b/README.md @@ -0,0 +1,81 @@ +# OpnForm — forms.doc-gen.eu + +Self-hosted, open-source formulierenbouwer ([OpnForm](https://github.com/OpnForm/OpnForm)) +op de Hetzner CX33, voor cursusevaluaties, inschrijvingen, kennistoetsen en +bestand-inleveren. EU-soeverein (Hetzner + eigen mailserver), geen Amerikaanse diensten +in de draaiende keten. + +Deze repo is de **ref-kopie / bron van waarheid** van de deploy-configuratie én tegelijk +de **git-bron die Coolify klont** om te deployen. + +## ⚠️ Uitzondering: dit draait NIET via route B + +Anders dan casuspositie/doc-gen/transcriber/oefendossiers/joplin/freescout/hedgedoc is +OpnForm **multi-image** en wordt het als **Coolify docker-compose-service (git-based)** +gedraaid, met **exact gepinde image-tags**: + +- **Geen** registry-image, **geen** Forgejo Actions/CI (er wordt niets gebouwd). +- **Geen** host-watcher-entry — voeg OpnForm dus **niet** toe aan `deploy-watcher/watch.sh`. +- Coolify klont deze repo en draait `docker compose up`. De losse `docker/nginx.conf` + wordt door Coolify **niet** geïnterpoleerd (bevat nginx-`$`-variabelen) — dáárom + git-based i.p.v. een raw compose-blok. + +Zie ook de infra-skill `forgejo-coolify-pipeline` (§ OpnForm) en runbook +`runbooks/opnform-cx33.md`. + +## Componenten (compose) + +| service | image (gepind) | rol | +|---|---|---| +| `api` | `jhumanj/opnform-api:1.13.2` | Laravel API (php-fpm :9000) — draait zelf de migraties bij boot | +| `api-worker` | idem | queue-worker (mail, exports) | +| `api-scheduler` | idem | cron/scheduler | +| `ui` | `jhumanj/opnform-client:1.13.2` | Nuxt SSR frontend (:3000) | +| `redis` | `redis:7.4.11` | sessies, cache, queue | +| `db` | `postgres:16.15` | database | +| `ingress` | `nginx:1.30.4` | HTTP↔FastCGI + `/`-routing → het enige web-endpoint (poort 80) | + +Persistente volumes: `postgres-data`, `opnform_storage` (geüploade formulier-bestanden, +lokaal — **geen S3**), `redis-data`. + +Domein/Traefik/LE: via de Coolify-magie `SERVICE_FQDN_INGRESS_80` op de `ingress`-service. +`APP_URL`/`FRONT_URL`/`NUXT_PUBLIC_*` volgen automatisch `${SERVICE_URL_INGRESS}`. + +## Updaten naar een nieuwe OpnForm-versie + +1. Kijk de nieuwe tag na op Docker Hub (`jhumanj/opnform-api`, `jhumanj/opnform-client`) en + de bijbehorende [release notes](https://github.com/OpnForm/OpnForm/releases). Controleer of + `docker-compose.yml`/`docker/nginx.conf`/de env-variabelen in die release zijn gewijzigd + (vergelijk met `OpnForm/OpnForm` op de tag) en neem wijzigingen hier over. +2. Bump de twee `jhumanj/opnform-*`-tags (en evt. postgres/redis/nginx) in `docker-compose.yml`. +3. `git commit` + `git push` naar `main`. +4. In Coolify (tunnel `ssh deploy@46.224.126.208 -L 8000:localhost:8000` → `http://localhost:8000`, + óf via de API met de Mac-Mini-`COOLIFY_TOKEN`): de OpnForm-resource **Redeploy**. + De api-container draait bij boot zelf `php artisan migrate --force`. +5. Verifieer: `curl -sL -o /dev/null -w '%{http_code} %{ssl_verify_result}\n' https://forms.doc-gen.eu/` → `200 0`. + +## Secrets & env + +Staan **niet** in deze repo. Ze leven in de **Coolify-env** van de resource en in +`~/.config/doc-gen-deploy/opnform.env` (0600) op de Mac Mini: +`APP_KEY`, `JWT_SECRET`, `FRONT_API_SECRET`, `DB_PASSWORD`, `DB_DATABASE`, `DB_USERNAME`, +`MAIL_USERNAME`, `MAIL_PASSWORD`, `MAIL_FROM_ADDRESS`, en `SERVICE_FQDN_INGRESS_80`. + +Mail loopt via `mail.2487.eu:465` (SSL). Beheerdersaccounts: gratis self-hosted OpnForm staat +**max. 2 gebruikers** toe (formulier-invullers tellen niet mee — die zijn anoniem/ongelimiteerd). + +## Coolify-object + +- Resource-type: **Docker Compose (git)**, project `My first project` / env `production`, + server `localhost` (CX33). +- App-uuid: _zie infra-issue en `runbooks/opnform-cx33.md`._ + +## Beheer, backup & health + +- **Backup**: `pg_dump` van de OpnForm-Postgres + het `opnform_storage`-volume in de + off-site restic-backup (`/usr/local/bin/docgen-backup.sh`, niet-fataal). +- **Health**: `opnform-health.timer` (5 min, `curl .../` → 200, ntfy `docgen-backup` bij 2× niet-200). +- **Eerste admin**: ga naar `https://forms.doc-gen.eu/` → `/setup` en maak het beheerdersaccount + (e-mail + wachtwoord). Publieke registratie staat daarna uit; extra gebruikers via uitnodiging. + +Volledige onboarding + verificaties: infra-runbook `runbooks/opnform-cx33.md`. diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..64d63d0 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,184 @@ +# OpnForm — forms.doc-gen.eu (CX33) +# ===================================================================== +# Deploy-model: Coolify **docker-compose** (git-based), GEEN route B. +# - Geen registry-image, geen Forgejo Actions/CI, geen host-watcher. +# - Coolify klont deze repo en draait `docker compose up`. De losse +# docker/nginx.conf wordt door Coolify NIET geinterpoleerd (bevat +# nginx-$-variabelen) — daarom git-based i.p.v. raw compose. +# Updaten (nieuwe OpnForm-versie): de image-tags hieronder bumpen -> +# commit+push naar main -> in Coolify "Redeploy". Zie README.md. +# Alle images zijn EXACT gepind (geen :latest). +# Secrets (${APP_KEY} etc.) staan NIET hier maar in de Coolify-env + +# ~/.config/doc-gen-deploy/opnform.env (0600) op de Mac Mini. +# ===================================================================== + +services: + # --- Laravel API (php-fpm op :9000) ------------------------------- + api: &api + image: jhumanj/opnform-api:1.13.2 + restart: unless-stopped + volumes: + - opnform_storage:/usr/share/nginx/html/storage:rw + environment: &api-env + APP_NAME: "OpnForm" + APP_ENV: production + APP_DEBUG: "false" + APP_KEY: ${APP_KEY} + # APP_URL/FRONT_URL volgen automatisch het toegewezen domein + # (sslip tijdens de test, forms.doc-gen.eu na cutover). + APP_URL: ${SERVICE_URL_INGRESS} + FRONT_URL: ${SERVICE_URL_INGRESS} + FRONT_API_SECRET: ${FRONT_API_SECRET} + LOG_CHANNEL: errorlog + LOG_LEVEL: warning + DB_CONNECTION: pgsql + DB_HOST: db + DB_PORT: "5432" + DB_DATABASE: ${DB_DATABASE:-opnform} + DB_USERNAME: ${DB_USERNAME:-opnform} + DB_PASSWORD: ${DB_PASSWORD} + FILESYSTEM_DRIVER: local + LOCAL_FILESYSTEM_VISIBILITY: public + CACHE_DRIVER: redis + QUEUE_CONNECTION: redis + SESSION_DRIVER: redis + REDIS_HOST: redis + REDIS_PORT: "6379" + REDIS_PASSWORD: "null" + MAIL_MAILER: smtp + MAIL_HOST: mail.2487.eu + MAIL_PORT: "465" + MAIL_USERNAME: ${MAIL_USERNAME} + MAIL_PASSWORD: ${MAIL_PASSWORD} + MAIL_ENCRYPTION: ssl + MAIL_FROM_ADDRESS: ${MAIL_FROM_ADDRESS} + MAIL_FROM_NAME: "OpnForm" + JWT_TTL: "1440" + JWT_SECRET: ${JWT_SECRET} + # Productie: token gebonden aan IP+UA (docker-setup.sh eist false = validatie AAN). + JWT_SKIP_IP_UA_VALIDATION: "false" + # Reverse proxy = Coolify/Traefik -> ingress(nginx) -> php-fpm; vertrouw de interne docker-netwerken. + TRUSTED_PROXIES: "10.0.0.0/8,172.16.0.0/12,192.168.0.0/16" + PHP_MEMORY_LIMIT: "512M" + PHP_MAX_EXECUTION_TIME: "600" + PHP_UPLOAD_MAX_FILESIZE: "64M" + PHP_POST_MAX_SIZE: "64M" + SHOW_OFFICIAL_TEMPLATES: "true" + depends_on: + db: + condition: service_healthy + redis: + condition: service_healthy + healthcheck: + test: ["CMD-SHELL", "php /usr/share/nginx/html/artisan about || exit 1"] + interval: 30s + timeout: 15s + retries: 3 + start_period: 90s + + # --- Queue-worker (mail, exports) --------------------------------- + api-worker: + <<: *api + command: ["php", "artisan", "queue:work", "--tries=3", "--timeout=120"] + depends_on: + api: + condition: service_healthy + healthcheck: + test: ["CMD-SHELL", "pgrep -f 'artisan queue:work' > /dev/null || exit 1"] + interval: 60s + timeout: 10s + retries: 3 + start_period: 30s + + # --- Scheduler (cron) --------------------------------------------- + api-scheduler: + <<: *api + command: ["php", "artisan", "schedule:work"] + depends_on: + api: + condition: service_healthy + healthcheck: + test: ["CMD-SHELL", "php /usr/share/nginx/html/artisan app:scheduler-status --mode=check --max-minutes=3 || exit 1"] + interval: 60s + timeout: 30s + retries: 3 + start_period: 90s + + # --- Frontend (Nuxt SSR op :3000) --------------------------------- + ui: + image: jhumanj/opnform-client:1.13.2 + restart: unless-stopped + environment: + NUXT_PUBLIC_APP_URL: ${SERVICE_URL_INGRESS} + NUXT_PUBLIC_API_BASE: ${SERVICE_URL_INGRESS}/api + # Server-side calls van Nuxt lopen intern via de nginx-ingress. + NUXT_PRIVATE_API_BASE: http://ingress/api + NUXT_API_SECRET: ${FRONT_API_SECRET} + NUXT_PUBLIC_ENV: production + NUXT_PUBLIC_ROOT_REDIRECT_URL: "" + depends_on: + api: + condition: service_healthy + healthcheck: + test: ["CMD-SHELL", "wget --spider -q http://localhost:3000/login || exit 1"] + interval: 30s + timeout: 10s + retries: 3 + start_period: 45s + + # --- Redis (sessies, cache, queue) -------------------------------- + redis: + image: redis:7.4.11 + restart: unless-stopped + volumes: + - redis-data:/data + healthcheck: + test: ["CMD-SHELL", "redis-cli ping | grep PONG"] + interval: 30s + timeout: 5s + retries: 5 + + # --- PostgreSQL ---------------------------------------------------- + db: + image: postgres:16.15 + restart: unless-stopped + environment: + POSTGRES_DB: ${DB_DATABASE:-opnform} + POSTGRES_USER: ${DB_USERNAME:-opnform} + POSTGRES_PASSWORD: ${DB_PASSWORD} + volumes: + - postgres-data:/var/lib/postgresql/data + healthcheck: + test: ["CMD-SHELL", "pg_isready -U ${DB_USERNAME:-opnform} -d ${DB_DATABASE:-opnform}"] + interval: 30s + timeout: 5s + retries: 5 + + # --- Nginx ingress: HTTP<->FastCGI + /-routing (het enige web-endpoint) --- + ingress: + image: nginx:1.30.4 + restart: unless-stopped + environment: + # Coolify: expose service 'ingress' op container-poort 80 achter het + # toegewezen domein (Traefik + Let's Encrypt). Leeg = auto (sslip); + # zet dit op https://forms.doc-gen.eu voor de cutover. + SERVICE_FQDN_INGRESS_80: "" + NGINX_MAX_BODY_SIZE: 64m + volumes: + - ./docker/nginx.conf:/etc/nginx/templates/default.conf.template:ro + depends_on: + api: + condition: service_started + ui: + condition: service_started + healthcheck: + test: ["CMD-SHELL", "nginx -t >/dev/null 2>&1 || exit 1"] + interval: 30s + timeout: 5s + retries: 3 + start_period: 10s + +volumes: + postgres-data: + opnform_storage: + redis-data: diff --git a/docker/nginx.conf b/docker/nginx.conf new file mode 100644 index 0000000..1619739 --- /dev/null +++ b/docker/nginx.conf @@ -0,0 +1,61 @@ +# OpnForm ingress — HTTP<->FastCGI + routing. +# Upstreams via docker-compose SERVICE-namen (api/ui): die zijn altijd +# resolvbaar op het compose-netwerk, ook als Coolify de container-namen wijzigt. +# Dit bestand wordt als nginx-TEMPLATE gemount; alleen ${NGINX_MAX_BODY_SIZE} +# wordt door de nginx-entrypoint (envsubst) ingevuld. Alle andere $-tokens zijn +# nginx-variabelen en blijven ongemoeid (staan niet in de container-env). + +map $request_uri $api_uri { + ~^/api(/.*$) $1; + default $request_uri; +} + +server { + listen 80; + server_name opnform; + root /usr/share/nginx/html/public; + client_max_body_size ${NGINX_MAX_BODY_SIZE}; + + access_log /dev/stdout; + error_log /dev/stderr error; + + index index.html index.htm index.php; + + location / { + proxy_http_version 1.1; + proxy_pass http://ui:3000; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Host $host; + proxy_set_header X-Forwarded-Port $server_port; + proxy_set_header X-Forwarded-Proto $http_x_forwarded_proto; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection "Upgrade"; + } + + location ~/(api|open|local\/temp|forms\/assets)/ { + set $original_uri $uri; + try_files $uri $uri/ /index.php$is_args$args; + } + + location ~ \.php$ { + fastcgi_split_path_info ^(.+\.php)(/.+)$; + fastcgi_pass api:9000; + fastcgi_index index.php; + include fastcgi_params; + fastcgi_param SCRIPT_FILENAME $document_root/index.php; + fastcgi_param REQUEST_URI $api_uri; + # Laravel accepteert deze headers alleen van IP's in TRUSTED_PROXIES. + # Zo blijft het echte client-IP + het https-schema achter Traefik/nginx behouden. + fastcgi_param HTTP_X_FORWARDED_FOR $proxy_add_x_forwarded_for; + fastcgi_param HTTP_X_FORWARDED_HOST $http_x_forwarded_host; + fastcgi_param HTTP_X_FORWARDED_PORT $http_x_forwarded_port; + fastcgi_param HTTP_X_FORWARDED_PROTO $http_x_forwarded_proto; + } + + # Weiger toegang tot .-bestanden + location ~ /\. { + deny all; + } +}