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 <noreply@anthropic.com>
This commit is contained in:
commit
817a7cdf77
3 changed files with 326 additions and 0 deletions
81
README.md
Normal file
81
README.md
Normal file
|
|
@ -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`.
|
||||
184
docker-compose.yml
Normal file
184
docker-compose.yml
Normal file
|
|
@ -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:
|
||||
61
docker/nginx.conf
Normal file
61
docker/nginx.conf
Normal file
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue