Rename roles/OpnForm → roles/opnform so the role resolves as
digitalboard.core.opnform (Ansible collection convention is
lowercase). Update tests/test.yml reference accordingly.
Add automated admin user creation via POST /api/register, gated on
opnform_admin_email + opnform_admin_password. Idempotent through a
prior login probe. Without these vars the manual setup page flow is
preserved.
Add automated OIDC IdentityConnection setup via the per-workspace
/api/open/workspaces/{id}/oidc-connections endpoint, gated on
opnform_oidc_enabled. Hard-coupled to the admin bootstrap (the API
requires an authenticated admin token); validation block fails fast
if OIDC is enabled without admin credentials. Supports both an
explicit opnform_oidc_group_role_mappings list and a fallback
opnform_oidc_admin_group convenience var.
Convert opnform_oidc_scopes from space-separated string to YAML list
to match OpnForm's API expectation. Rewrite README "First login" and
"OIDC setup" sections to reflect that self-hosted OpnForm does not
ship a pre-seeded admin and to document the new bootstrap paths.
BREAKING CHANGE: opnform_oidc_scopes changed from space-separated
string to YAML list. Inventories that override it must update from
"openid profile email" to [openid, profile, email].
189 lines
5.5 KiB
Django/Jinja
189 lines
5.5 KiB
Django/Jinja
#---------------------------------------------------------------------#
|
|
# OpnForm — Beautiful open-source form builder #
|
|
#---------------------------------------------------------------------#
|
|
services:
|
|
api: &api-service
|
|
image: {{ opnform_api_image }}
|
|
container_name: opnform-api
|
|
restart: unless-stopped
|
|
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"
|
|
|
|
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 }}"
|
|
|
|
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
|
|
FRONT_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=Host(`{{ opnform_domain }}`)
|
|
{% if opnform_use_ssl %}
|
|
- traefik.http.routers.{{ opnform_service_name }}.entrypoints=websecure
|
|
- traefik.http.routers.{{ opnform_service_name }}.tls=true
|
|
{% 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
|