The api service now also receives FRONT_API_SECRET so AuthenticateJWT accepts the UI's server-side JWT forwards instead of blacklisting them on UA mismatch. On the ui service the var is renamed FRONT_API_SECRET -> NUXT_API_SECRET so Nuxt's runtimeConfig.apiSecret is actually populated (NUXT_<key> convention) and injected as x-api-secret, short-circuiting the UA-fingerprint check that otherwise 401s every reload.
215 lines
6.9 KiB
Django/Jinja
215 lines
6.9 KiB
Django/Jinja
#---------------------------------------------------------------------#
|
|
# OpnForm — Beautiful open-source form builder #
|
|
#---------------------------------------------------------------------#
|
|
services:
|
|
api: &api-service
|
|
image: {{ opnform_api_image }}
|
|
container_name: opnform-api
|
|
restart: unless-stopped
|
|
{% if opnform_extra_hosts | length > 0 %}
|
|
extra_hosts:
|
|
{% for host in opnform_extra_hosts %}
|
|
- "{{ host }}"
|
|
{% endfor %}
|
|
{% endif %}
|
|
volumes:
|
|
- {{ opnform_storage_dir }}:/usr/share/nginx/html/storage:rw
|
|
environment: &api-env
|
|
APP_ENV: production
|
|
APP_KEY: "{{ opnform_app_key }}"
|
|
APP_URL: "{{ opnform_base_url }}"
|
|
APP_DEBUG: "false"
|
|
SELF_HOSTED: "true"
|
|
{% if opnform_oidc_enabled and (_opnform_force_login_effective | default(false)) %}
|
|
OIDC_FORCE_LOGIN: "true"
|
|
{% endif %}
|
|
|
|
LOG_CHANNEL: errorlog
|
|
LOG_LEVEL: info
|
|
|
|
DB_CONNECTION: pgsql
|
|
DB_HOST: db
|
|
DB_PORT: "5432"
|
|
DB_DATABASE: "{{ opnform_db_name }}"
|
|
DB_USERNAME: "{{ opnform_db_user }}"
|
|
DB_PASSWORD: "{{ opnform_db_password }}"
|
|
|
|
REDIS_HOST: redis
|
|
REDIS_PORT: "6379"
|
|
|
|
CACHE_STORE: redis
|
|
CACHE_DRIVER: redis
|
|
QUEUE_CONNECTION: redis
|
|
SESSION_DRIVER: redis
|
|
SESSION_LIFETIME: "120"
|
|
BROADCAST_CONNECTION: log
|
|
|
|
FILESYSTEM_DISK: local
|
|
FILESYSTEM_DRIVER: local
|
|
LOCAL_FILESYSTEM_VISIBILITY: public
|
|
|
|
MAIL_MAILER: "{{ opnform_mail_mailer }}"
|
|
MAIL_HOST: "{{ opnform_mail_host }}"
|
|
MAIL_PORT: "{{ opnform_mail_port }}"
|
|
MAIL_USERNAME: "{{ opnform_mail_username }}"
|
|
MAIL_PASSWORD: "{{ opnform_mail_password }}"
|
|
MAIL_ENCRYPTION: "{{ opnform_mail_encryption }}"
|
|
MAIL_FROM_ADDRESS: "{{ opnform_mail_from_address }}"
|
|
MAIL_FROM_NAME: "{{ opnform_mail_from_name }}"
|
|
|
|
JWT_TTL: "1440"
|
|
JWT_SECRET: "{{ opnform_jwt_secret }}"
|
|
|
|
# Shared secret for trusted SSR requests from the Nuxt UI. The UI
|
|
# forwards JWTs server-side with its own user agent; without this
|
|
# secret the API's AuthenticateJWT middleware would reject those
|
|
# requests (UA mismatch -> token blacklisted -> the next genuine
|
|
# browser request 401s). Must match FRONT_API_SECRET on the ui
|
|
# service.
|
|
FRONT_API_SECRET: "{{ opnform_front_api_secret }}"
|
|
|
|
PHP_MEMORY_LIMIT: "{{ opnform_php_memory_limit }}"
|
|
PHP_MAX_EXECUTION_TIME: "{{ opnform_php_max_execution_time }}"
|
|
PHP_UPLOAD_MAX_FILESIZE: "{{ opnform_php_upload_max_filesize }}"
|
|
PHP_POST_MAX_SIZE: "{{ opnform_php_post_max_size }}"
|
|
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: 60s
|
|
networks:
|
|
- opnform-internal
|
|
|
|
api-worker:
|
|
<<: *api-service
|
|
container_name: opnform-api-worker
|
|
command: ["php", "artisan", "queue:work"]
|
|
environment:
|
|
<<: *api-env
|
|
IS_API_WORKER: "true"
|
|
healthcheck:
|
|
test: ["CMD-SHELL", "pgrep -f 'php artisan queue:work' > /dev/null || exit 1"]
|
|
interval: 60s
|
|
timeout: 10s
|
|
retries: 3
|
|
start_period: 30s
|
|
|
|
api-scheduler:
|
|
<<: *api-service
|
|
container_name: opnform-api-scheduler
|
|
command: ["php", "artisan", "schedule:work"]
|
|
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: 70s
|
|
|
|
ui:
|
|
image: {{ opnform_client_image }}
|
|
container_name: opnform-ui
|
|
restart: unless-stopped
|
|
environment:
|
|
NUXT_PUBLIC_APP_URL: "{{ opnform_base_url }}"
|
|
NUXT_PUBLIC_API_BASE: "/api"
|
|
NUXT_PRIVATE_API_BASE: "http://ingress/api"
|
|
NUXT_PUBLIC_ENV: production
|
|
# Nuxt runtimeConfig.apiSecret is fed by NUXT_API_SECRET (Nuxt
|
|
# convention: NUXT_<key> populates runtimeConfig.<key>). The UI
|
|
# injects this as `x-api-secret` on SSR-side forwards to Laravel,
|
|
# which then short-circuits the UA-fingerprint check in
|
|
# AuthenticateJWT — without it every reload would invalidate the
|
|
# JWT (UA `node` vs UA at issue time) and 401.
|
|
NUXT_API_SECRET: "{{ opnform_front_api_secret }}"
|
|
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
|
|
networks:
|
|
- opnform-internal
|
|
|
|
redis:
|
|
image: {{ opnform_redis_image }}
|
|
container_name: opnform-redis
|
|
restart: unless-stopped
|
|
volumes:
|
|
- {{ opnform_redis_data_dir }}:/data
|
|
healthcheck:
|
|
test: ["CMD-SHELL", "redis-cli ping | grep PONG"]
|
|
interval: 30s
|
|
timeout: 5s
|
|
networks:
|
|
- opnform-internal
|
|
|
|
db:
|
|
image: {{ opnform_db_image }}
|
|
container_name: opnform-db
|
|
restart: unless-stopped
|
|
environment:
|
|
POSTGRES_DB: "{{ opnform_db_name }}"
|
|
POSTGRES_USER: "{{ opnform_db_user }}"
|
|
POSTGRES_PASSWORD: "{{ opnform_db_password }}"
|
|
volumes:
|
|
- {{ opnform_db_data_dir }}:/var/lib/postgresql/data
|
|
healthcheck:
|
|
test: ["CMD-SHELL", "pg_isready -U {{ opnform_db_user }}"]
|
|
interval: 30s
|
|
timeout: 5s
|
|
networks:
|
|
- opnform-internal
|
|
|
|
ingress:
|
|
image: {{ opnform_ingress_image }}
|
|
container_name: opnform-ingress
|
|
restart: unless-stopped
|
|
volumes:
|
|
- ./nginx.conf:/etc/nginx/templates/default.conf.template:ro
|
|
environment:
|
|
NGINX_MAX_BODY_SIZE: "{{ opnform_nginx_max_body_size }}"
|
|
depends_on:
|
|
api:
|
|
condition: service_started
|
|
ui:
|
|
condition: service_started
|
|
healthcheck:
|
|
test: ["CMD-SHELL", "nginx -t && curl -f http://localhost/ || exit 1"]
|
|
interval: 30s
|
|
timeout: 5s
|
|
retries: 3
|
|
start_period: 10s
|
|
networks:
|
|
- opnform-internal
|
|
- {{ opnform_traefik_network }}
|
|
labels:
|
|
- traefik.enable=true
|
|
- traefik.docker.network={{ opnform_traefik_network }}
|
|
- traefik.http.routers.{{ opnform_service_name }}.rule={% set _all_domains = [opnform_domain] + (opnform_extra_domains | default([])) %}{% for d in _all_domains %}Host(`{{ d }}`){% if not loop.last %} || {% endif %}{% endfor +%}
|
|
{% if opnform_use_ssl %}
|
|
- traefik.http.routers.{{ opnform_service_name }}.entrypoints=websecure
|
|
- traefik.http.routers.{{ opnform_service_name }}.tls=true
|
|
{% if traefik_cert_mode | default('selfsigned') == 'acme' %}
|
|
- traefik.http.routers.{{ opnform_service_name }}.tls.certresolver={{ traefik_ssl_cert_resolver | default('dns') }}
|
|
{% endif %}
|
|
{% else %}
|
|
- traefik.http.routers.{{ opnform_service_name }}.entrypoints=web
|
|
{% endif %}
|
|
- traefik.http.services.{{ opnform_service_name }}.loadbalancer.server.port=80
|
|
|
|
networks:
|
|
opnform-internal:
|
|
driver: bridge
|
|
{{ opnform_traefik_network }}:
|
|
external: true
|