# 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; } }