OpnForm draait vanaf fase 3b (#76/#84) op de derden-box; per service een memory-limit zodat een lek of piek nooit de hele box meetrekt. Idle-gebruik gemeten 2026-09-13 (totaal ~500 MiB); limits ruim erboven. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
273 lines
10 KiB
YAML
273 lines
10 KiB
YAML
# OpnForm — forms.doc-gen.eu (derden-box sinds infra#84; daarvóór CX33/docgen)
|
|
# =====================================================================
|
|
# 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
|
|
mem_limit: 512m # RAM-guardrail derden-box (infra#84); idle ~80 MiB
|
|
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
|
|
# Poort 587 (STARTTLS), NIET 465: Hetzner knijpt uitgaand 25/465 af op de CX33.
|
|
MAIL_PORT: "587"
|
|
MAIL_USERNAME: ${MAIL_USERNAME}
|
|
MAIL_PASSWORD: ${MAIL_PASSWORD}
|
|
MAIL_ENCRYPTION: tls
|
|
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
|
|
mem_limit: 384m # RAM-guardrail derden-box (infra#84); idle ~225 MiB
|
|
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:
|
|
# 127.0.0.1 i.p.v. localhost: localhost kan naar ::1 (IPv6) resolven terwijl
|
|
# Nuxt op 0.0.0.0 (IPv4) luistert -> onterecht 'unhealthy'.
|
|
test: ["CMD-SHELL", "wget --spider -q http://127.0.0.1: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
|
|
mem_limit: 128m # RAM-guardrail derden-box (infra#84); idle ~20 MiB
|
|
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
|
|
mem_limit: 384m # RAM-guardrail derden-box (infra#84); idle ~70 MiB
|
|
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
|
|
mem_limit: 64m # RAM-guardrail derden-box (infra#84); idle ~15 MiB
|
|
environment:
|
|
# Coolify: expose service 'ingress' op container-poort 80 achter het
|
|
# toegewezen domein (Traefik + Let's Encrypt). Het echte domein zet je
|
|
# in Coolify (docker_compose_domains), niet hier.
|
|
SERVICE_FQDN_INGRESS_80: ""
|
|
# De nginx-config staat INLINE (zie 'configs' onderaan) i.p.v. als
|
|
# bind-mount: Coolify materialiseert bij een git-compose alleen de
|
|
# compose + .env in de app-map, niet losse repo-bestanden — een
|
|
# ./docker/nginx.conf-mount wordt dan een lege map. Inline is self-contained.
|
|
configs:
|
|
- source: nginx_conf
|
|
target: /etc/nginx/conf.d/default.conf
|
|
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
|
|
# KRITIEK: de ingress zit op meerdere netwerken; zonder dit label kiest Traefik
|
|
# willekeurig een netwerk-IP van de ingress — soms een app-net waar Traefik zélf
|
|
# NIET op zit → verbinding hangt (HTTP 000, vooral ná een container-IP-wissel bij
|
|
# herstart/redeploy). Pin Traefik expliciet op het 'coolify'-net.
|
|
labels:
|
|
- "traefik.docker.network=coolify"
|
|
# ALLEEN de ingress hangt aan het gedeelde 'coolify'-netwerk (voor Traefik).
|
|
# De backend-services blijven op het privé-app-netwerk (default) — anders
|
|
# botst de generieke servicenaam 'redis'/'db' met Coolify's eigen
|
|
# coolify-redis/coolify-db op het gedeelde net (NOAUTH/verkeerde backend).
|
|
networks:
|
|
- default
|
|
- coolify
|
|
|
|
volumes:
|
|
postgres-data:
|
|
opnform_storage:
|
|
redis-data:
|
|
|
|
# Het 'coolify'-netwerk bestaat al (Coolify's proxy-net); alleen extern refereren.
|
|
networks:
|
|
coolify:
|
|
external: true
|
|
|
|
# Nginx-ingress-config inline. Alle nginx-$-variabelen zijn als $$ geschreven
|
|
# zodat docker compose ze NIET interpoleert (compose zet $$ -> $ terug); pas de
|
|
# gerenderde config bevat de juiste nginx-variabelen. client_max_body_size is
|
|
# hardgecodeerd (64m). Upstreams via de compose SERVICE-namen api/ui.
|
|
configs:
|
|
nginx_conf:
|
|
content: |
|
|
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 64m;
|
|
|
|
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;
|
|
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;
|
|
}
|
|
|
|
location ~ /\. {
|
|
deny all;
|
|
}
|
|
}
|