Compare commits

..

No commits in common. "30f3c16b59de8aa2362f547730eb4a883c45895c" and "78095cca1d33c463525d2a65636060368749612e" have entirely different histories.

11 changed files with 0 additions and 1030 deletions

View file

@ -1,169 +0,0 @@
# opnform
Deploy [OpnForm](https://github.com/OpnForm/OpnForm) as a self-contained
Docker Compose stack behind Traefik.
## What this role does
- Deploys the full official OpnForm stack: `api`, `api-worker`, `api-scheduler`,
`ui`, `db` (Postgres), `redis`, and `ingress` (nginx)
- Configures all environment variables for self-hosted production use
- Integrates the ingress container with an existing Traefik proxy network
- Waits for the API container to become healthy before returning
## What this role does NOT do (stage 1)
- Does not pre-configure OIDC / identity_connections — set up via Admin UI
## Architecture note: why two reverse proxies?
```
Browser → Traefik (TLS, host routing) → ingress-nginx → api (PHP-FPM) / ui (Nuxt)
```
The `ingress` container looks like a redundant proxy next to Traefik but
does a different job. OpnForm's `api` image is **PHP-FPM only** — it
speaks the FastCGI protocol on port 9000, not HTTP. Traefik cannot
translate FastCGI, so the ingress nginx is required to:
- Translate HTTP `/api/*` requests into FastCGI calls to `api:9000`
- Rewrite request URIs via the `$api_uri` map
- Set Laravel-specific FastCGI params (`SCRIPT_FILENAME`, `REQUEST_URI`)
- Reverse-proxy `/` to the Nuxt UI container on port 3000
Both containers run on the same Docker network on the same host, so the
performance overhead of the extra hop is negligible (in-kernel memory
copy, not a real network round-trip). Removing the ingress would require
a custom OpnForm image with a built-in HTTP server, which is out of
scope for this role.
## Required variables
Provide via OpenBao, Ansible Vault, or extra-vars. **Never commit real
secrets to version control.**
| Variable | Format | Generate with |
|---|---|---|
| `opnform_app_key` | `base64:<32 bytes base64>` | `echo "base64:$(openssl rand -base64 32)"` |
| `opnform_jwt_secret` | 32 bytes base64 | `openssl rand -base64 32` |
| `opnform_front_api_secret` | 32 bytes base64 | `openssl rand -base64 32` |
| `opnform_db_password` | strong password | `openssl rand -base64 24` |
When `opnform_oidc_enabled` is `true`:
| Variable | Source |
|---|---|
| `opnform_oidc_client_secret` | from your Keycloak/Authentik client |
The `assert` task at the top of the role will fail fast if any secret is
missing or malformed.
## First login
OpnForm in self-hosted mode does **not** ship a pre-seeded admin user.
The first user to register becomes the owner of the default workspace,
and further public registration is disabled afterwards (additional
users must be invited via the Admin UI).
This role supports two ways to create that first user:
### Option A — automated bootstrap (recommended)
Set `opnform_admin_email` and `opnform_admin_password` (ideally from
Vault / OpenBao). The role then POSTs to `/api/register` after the
API container is healthy, skipping the setup page entirely. The task
is idempotent: it does a login check first and only registers if the
user does not already exist.
```yaml
opnform_admin_name: "Administrator" # default
opnform_admin_email: "admin@example.com"
opnform_admin_password: "{{ vault_opnform_admin_password }}"
```
Password rules enforced by OpnForm: minimum 8 characters, at least one
letter, one digit, and one of `@$!%*#?&-_+=.,:;<>^()[]{}|~`.
### Option B — manual setup page
Leave `opnform_admin_email` / `opnform_admin_password` empty. Visit
`opnform_base_url` and complete the setup page in the browser.
## OIDC setup
Set `opnform_oidc_enabled: true` and the role creates an
IdentityConnection on the admin's default workspace via
`POST /api/open/workspaces/{id}/oidc-connections`. OpnForm enforces a
single OIDC connection per workspace, so the task is idempotent (GETs
existing connections first and skips if any exist).
**Prerequisite**: the admin bootstrap must be configured
(`opnform_admin_email` + `opnform_admin_password`). The OIDC API
requires an authenticated admin token; the role logs in with those
credentials to make the call. The validation block fails fast if OIDC
is enabled without admin credentials.
### Required when `opnform_oidc_enabled: true`
| Variable | Notes |
|---|---|
| `opnform_oidc_client_secret` | from your IdP, never commit |
| `opnform_oidc_domain` | email domain that triggers OIDC (e.g. `example.com`) |
### Tunables (defaults shown)
```yaml
opnform_oidc_issuer: "https://auth.digitalboard.ch/realms/Digitalboard"
opnform_oidc_client_id: "opnform-digitalboard"
opnform_oidc_client_name: "Digitalboard" # display name in UI
opnform_oidc_slug: "oidc" # used in /auth/{slug}/callback
opnform_oidc_scopes: [openid, profile, email, groups]
```
### Group → role mapping
Two ways, the list takes precedence:
```yaml
# Option 1: full list (any number of mappings)
opnform_oidc_group_role_mappings:
- idp_group: "opnform-admins"
role: admin
- idp_group: "opnform-editors"
role: editor
# Option 2: convenience — single admin group
opnform_oidc_admin_group: "opnform-admins" # mapped to role=admin
```
Valid roles: `owner`, `admin`, `editor`, `member`.
## Example playbook
```yaml
- name: Deploy OpnForm service
hosts: opnform_servers
become: true
roles:
- digitalboard.core.opnform
```
With inventory variables:
```yaml
# group_vars/opnform_servers.yml
opnform_domain: forms.digitalboard.ch
opnform_base_url: "https://forms.digitalboard.ch"
opnform_app_key: "{{ lookup('community.hashi_vault.vault_kv2_get',
'digitalboard/opnform',
mount_point='kv').data.data.app_key }}"
opnform_jwt_secret: "{{ lookup('community.hashi_vault.vault_kv2_get',
'digitalboard/opnform',
mount_point='kv').data.data.jwt_secret }}"
opnform_front_api_secret: "{{ lookup('community.hashi_vault.vault_kv2_get',
'digitalboard/opnform',
mount_point='kv').data.data.front_api_secret }}"
opnform_db_password: "{{ lookup('community.hashi_vault.vault_kv2_get',
'digitalboard/opnform',
mount_point='kv').data.data.db_password }}"
```

View file

@ -1,109 +0,0 @@
#SPDX-License-Identifier: MIT-0
---
# defaults file for opnform
# Base directory configuration (inherited from base role or defined here)
docker_compose_base_dir: /etc/docker/compose
docker_volume_base_dir: /srv/data
# opnform-specific configuration
opnform_service_name: opnform
opnform_docker_compose_dir: "{{ docker_compose_base_dir }}/{{ opnform_service_name }}"
opnform_docker_volume_dir: "{{ docker_volume_base_dir }}/{{ opnform_service_name }}"
opnform_storage_dir: "{{ opnform_docker_volume_dir }}/storage"
opnform_db_data_dir: "{{ opnform_docker_volume_dir }}/db"
opnform_redis_data_dir: "{{ opnform_docker_volume_dir }}/redis"
# Service configuration
opnform_domain: "forms.local.test"
opnform_base_url: "https://forms.local.test"
# Images
opnform_api_image: "jhumanj/opnform-api:latest"
opnform_client_image: "jhumanj/opnform-client:latest"
opnform_redis_image: "redis:7"
opnform_db_image: "postgres:16"
opnform_ingress_image: "nginx:1"
# REQUIRED SECRETS — must be overridden per-inventory.
# Provide via OpenBao lookup, Ansible Vault or extra-vars.
# Never commit real keys to version control.
#
# Generate with:
# opnform_app_key: echo "base64:$(openssl rand -base64 32)"
# opnform_jwt_secret: openssl rand -hex 32
# opnform_front_api_secret: openssl rand -hex 32
#
# opnform_app_key MUST start with the prefix "base64:" — the validate
# task at the top of tasks/main.yml enforces this.
opnform_app_key: ""
opnform_jwt_secret: ""
opnform_front_api_secret: ""
# Database credentials. opnform_db_password must be overridden; the
# validate task fails fast on an empty value.
opnform_db_name: "opnform"
opnform_db_user: "opnform"
opnform_db_password: ""
# Admin bootstrap — when email+password are set, the role creates the
# first user via OpnForm's /api/register endpoint, skipping the
# self-hosted setup page. Leave both empty to keep the manual setup flow.
# Password must satisfy OpnForm's rules: min 8 chars, contain a letter,
# a digit and one of @$!%*#?&-_+=.,:;<>^()[]{}|~
# Provide via OpenBao, Ansible Vault or extra-vars.
opnform_admin_name: "Administrator"
opnform_admin_email: ""
opnform_admin_password: ""
opnform_admin_hear_about_us: "ansible"
# PHP configuration
opnform_php_memory_limit: "1G"
opnform_php_max_execution_time: "600"
opnform_php_upload_max_filesize: "64M"
opnform_php_post_max_size: "64M"
# Nginx ingress
opnform_nginx_max_body_size: "64m"
# Mail configuration (optional — defaults to log driver)
opnform_mail_mailer: "log"
opnform_mail_host: ""
opnform_mail_port: ""
opnform_mail_username: ""
opnform_mail_password: ""
opnform_mail_encryption: ""
opnform_mail_from_address: "noreply@digitalboard.ch"
opnform_mail_from_name: "OpnForm"
# OIDC configuration — when enabled, the role auto-creates an
# IdentityConnection in the first workspace via OpnForm's API after the
# admin bootstrap. Requires opnform_admin_email/_password to be set
# (the API call needs an authenticated admin token).
opnform_oidc_enabled: false
opnform_oidc_issuer: "https://auth.digitalboard.ch/realms/Digitalboard"
opnform_oidc_client_id: "opnform-digitalboard"
opnform_oidc_client_secret: ""
opnform_oidc_client_name: "Digitalboard"
# OpnForm-side identifier used in /auth/{slug}/callback. Lowercase
# alphanumeric + hyphens, unique across all identity_connections.
opnform_oidc_slug: "oidc"
# Email domain that triggers OIDC login for matching users (e.g. users
# with @example.com emails are redirected to the IdP). Required when
# opnform_oidc_enabled is true.
opnform_oidc_domain: ""
opnform_oidc_scopes:
- openid
- profile
- email
- groups
# Convenience: maps a single IdP group to the OpnForm "admin" role.
# Ignored when opnform_oidc_group_role_mappings is non-empty.
opnform_oidc_admin_group: "opnform-admins"
# Full group-to-role mapping list. Takes precedence over the convenience
# var. Each item: {idp_group: "<group name>", role: "owner|admin|editor|member"}
opnform_oidc_group_role_mappings: []
# Traefik configuration
opnform_traefik_network: "proxy"
opnform_use_ssl: true

View file

@ -1,8 +0,0 @@
#SPDX-License-Identifier: MIT-0
---
# handlers file for opnform
- name: restart opnform
community.docker.docker_compose_v2:
project_src: "{{ opnform_docker_compose_dir }}"
state: restarted

View file

@ -1,220 +0,0 @@
---
argument_specs:
main:
short_description: Deploy OpnForm (api + ui + db + redis + ingress) via Docker Compose.
description:
- Renders a Compose stack for the full OpnForm setup (PHP-FPM api,
Nuxt ui, Postgres, Redis, nginx ingress) and exposes it through
Traefik.
- Optionally bootstraps the first admin user via the OpnForm
C(/api/register) endpoint (skipping the self-hosted setup page)
and provisions a single OIDC identity connection in the default
workspace via the workspace API. Both bootstraps are idempotent.
options:
docker_compose_base_dir:
type: path
default: /etc/docker/compose
docker_volume_base_dir:
type: path
default: /srv/data
opnform_service_name:
type: str
default: opnform
opnform_docker_compose_dir:
type: path
description: Defaults to C({{ docker_compose_base_dir }}/{{ opnform_service_name }}).
opnform_docker_volume_dir:
type: path
description: Defaults to C({{ docker_volume_base_dir }}/{{ opnform_service_name }}).
opnform_storage_dir:
type: path
description: OpnForm storage volume mounted into the api container.
opnform_db_data_dir:
type: path
opnform_redis_data_dir:
type: path
opnform_domain:
type: str
default: forms.local.test
description: Hostname used in the traefik Host rule.
opnform_base_url:
type: str
default: https://forms.local.test
description: Public URL OpnForm uses for APP_URL and NUXT_PUBLIC_APP_URL.
opnform_api_image:
type: str
default: jhumanj/opnform-api:latest
opnform_client_image:
type: str
default: jhumanj/opnform-client:latest
opnform_redis_image:
type: str
default: "redis:7"
opnform_db_image:
type: str
default: "postgres:16"
opnform_ingress_image:
type: str
default: "nginx:1"
opnform_app_key:
type: str
required: true
description:
- Laravel application key. Must be prefixed with C(base64:).
Generate with C(echo "base64:$(openssl rand -base64 32)").
Provide via OpenBao, Ansible Vault or extra-vars.
opnform_jwt_secret:
type: str
required: true
description: JWT signing secret. Generate with C(openssl rand -hex 32).
opnform_front_api_secret:
type: str
required: true
description: Shared secret between ui and api. Generate with C(openssl rand -hex 32).
opnform_db_name:
type: str
default: opnform
opnform_db_user:
type: str
default: opnform
opnform_db_password:
type: str
required: true
opnform_admin_name:
type: str
default: Administrator
opnform_admin_email:
type: str
default: ''
description:
- When non-empty (together with C(opnform_admin_password)) the role
bootstraps the first user via C(/api/register), skipping the
self-hosted setup page. Required when C(opnform_oidc_enabled=true).
opnform_admin_password:
type: str
default: ''
description:
- "Must satisfy OpnForm's policy: min 8 chars, letter + digit +
symbol from C(@$!%*#?&-_+=.,:;<>^()[]{}|~)."
opnform_admin_hear_about_us:
type: str
default: ansible
opnform_php_memory_limit:
type: str
default: 1G
opnform_php_max_execution_time:
type: str
default: "600"
opnform_php_upload_max_filesize:
type: str
default: 64M
opnform_php_post_max_size:
type: str
default: 64M
opnform_nginx_max_body_size:
type: str
default: 64m
opnform_mail_mailer:
type: str
default: log
choices: [log, smtp, ses, mailgun, postmark, sendmail]
opnform_mail_host:
type: str
default: ''
opnform_mail_port:
type: str
default: ''
opnform_mail_username:
type: str
default: ''
opnform_mail_password:
type: str
default: ''
opnform_mail_encryption:
type: str
default: ''
choices: ['', tls, ssl]
opnform_mail_from_address:
type: str
default: noreply@digitalboard.ch
opnform_mail_from_name:
type: str
default: OpnForm
opnform_oidc_enabled:
type: bool
default: false
description:
- "When true the role calls the workspace API to create a single
OIDC C(identity_connection) on the default workspace after the
admin bootstrap. Requires C(opnform_admin_email) +
C(opnform_admin_password) so the role can authenticate.
Idempotent: skipped when any connection already exists."
opnform_oidc_issuer:
type: str
default: https://auth.digitalboard.ch/realms/Digitalboard
description: OIDC issuer URL.
opnform_oidc_client_id:
type: str
default: opnform-digitalboard
opnform_oidc_client_secret:
type: str
default: ''
description: Required when C(opnform_oidc_enabled=true).
opnform_oidc_client_name:
type: str
default: Digitalboard
description: Display name shown in the OpnForm UI.
opnform_oidc_slug:
type: str
default: oidc
description:
- OpnForm-side identifier used in C(/auth/{slug}/callback). Lowercase
alphanumeric + hyphens, unique across all C(identity_connections).
opnform_oidc_domain:
type: str
default: ''
description:
- Email domain that triggers OIDC for matching users. Required
when C(opnform_oidc_enabled=true).
opnform_oidc_scopes:
type: list
elements: str
default: [openid, profile, email, groups]
opnform_oidc_admin_group:
type: str
default: opnform-admins
description:
- Convenience setting that maps a single IdP group to the OpnForm
C(admin) role. Ignored when C(opnform_oidc_group_role_mappings)
is non-empty.
opnform_oidc_group_role_mappings:
type: list
elements: dict
default: []
description:
- Full IdP-group -> OpnForm-role mapping. Takes precedence over
C(opnform_oidc_admin_group).
options:
idp_group:
type: str
required: true
description: Group name as it appears in the IdP groups claim.
role:
type: str
required: true
choices: [owner, admin, editor, member]
opnform_traefik_network:
type: str
default: proxy
opnform_use_ssl:
type: bool
default: true

View file

@ -1,16 +0,0 @@
#SPDX-License-Identifier: MIT-0
galaxy_info:
author: Tobias Wüst
description: Deploy OpnForm self-hosted form builder via Docker Compose behind Traefik
company: Digitalboard
license: MIT-0
min_ansible_version: "2.15"
galaxy_tags:
- opnform
- forms
- docker
- traefik
- oidc
dependencies: []

View file

@ -1,265 +0,0 @@
#SPDX-License-Identifier: MIT-0
---
# tasks file for opnform
# =====================================================================
# 0. VALIDATION
# =====================================================================
- name: Validate required secrets
ansible.builtin.assert:
that:
- opnform_app_key | length > 0
- opnform_app_key is match('^base64:[A-Za-z0-9+/=]+$')
- opnform_jwt_secret | length > 0
- opnform_front_api_secret | length > 0
- opnform_db_password | length > 0
fail_msg: >-
OpnForm requires opnform_app_key (prefix 'base64:'), opnform_jwt_secret,
opnform_front_api_secret and opnform_db_password.
Generate with: openssl rand -base64 32
The app_key MUST be prefixed with "base64:"
Provide via OpenBao, Ansible Vault or extra-vars.
success_msg: Secrets validation passed
- name: Validate OIDC configuration when enabled
ansible.builtin.assert:
that:
- opnform_oidc_client_secret | length > 0
- opnform_oidc_domain | length > 0
- opnform_admin_email | length > 0
- opnform_admin_password | length > 0
fail_msg: >-
When opnform_oidc_enabled is true, you must set:
- opnform_oidc_client_secret
- opnform_oidc_domain (email domain that triggers OIDC)
- opnform_admin_email / opnform_admin_password
(the OIDC API requires an authenticated admin; the role logs in
with these credentials to POST the connection)
when: opnform_oidc_enabled | bool
# =====================================================================
# 1. PREPARATION
# =====================================================================
- name: Ensure required packages are installed
ansible.builtin.package:
name:
- python3-docker
state: present
- name: Create docker compose directory
ansible.builtin.file:
path: "{{ opnform_docker_compose_dir }}"
state: directory
mode: '0755'
- name: Create OpnForm data directories
ansible.builtin.file:
path: "{{ item }}"
state: directory
mode: "0755"
loop:
- "{{ opnform_docker_volume_dir }}"
- "{{ opnform_storage_dir }}"
- "{{ opnform_db_data_dir }}"
- "{{ opnform_redis_data_dir }}"
# =====================================================================
# 2. CONFIGURATION FILES
# =====================================================================
- name: Deploy nginx ingress configuration
ansible.builtin.template:
src: nginx.conf.j2
dest: "{{ opnform_docker_compose_dir }}/nginx.conf"
mode: '0644'
notify: restart opnform
- name: Deploy docker-compose file
ansible.builtin.template:
src: docker-compose.yml.j2
dest: "{{ opnform_docker_compose_dir }}/docker-compose.yml"
mode: '0644'
notify: restart opnform
# =====================================================================
# 3. CONTAINER STARTUP
# =====================================================================
- name: Start opnform containers
community.docker.docker_compose_v2:
project_src: "{{ opnform_docker_compose_dir }}"
state: present
wait: true
wait_timeout: 180
# =====================================================================
# 4. WAIT FOR API READINESS
# =====================================================================
- name: Wait for API container to be healthy
ansible.builtin.command:
cmd: docker inspect --format='{% raw %}{{.State.Health.Status}}{% endraw %}' opnform-api
register: api_health
until: api_health.stdout == "healthy"
retries: 30
delay: 10
changed_when: false
# =====================================================================
# 5. ADMIN BOOTSTRAP (optional)
# =====================================================================
# Skips the self-hosted setup page by registering the first user via
# OpnForm's /api/register endpoint. Idempotent: a successful login
# attempt with the same credentials means the user already exists.
- name: Check if OpnForm admin user already exists
ansible.builtin.uri:
url: "https://127.0.0.1/api/login"
method: POST
headers:
Host: "{{ opnform_domain }}"
body_format: json
body:
email: "{{ opnform_admin_email }}"
password: "{{ opnform_admin_password }}"
status_code: [200, 401, 422]
validate_certs: false
register: opnform_admin_login
when:
- opnform_admin_email | length > 0
- opnform_admin_password | length > 0
- name: Create OpnForm admin user via /api/register
ansible.builtin.uri:
url: "https://127.0.0.1/api/register"
method: POST
headers:
Host: "{{ opnform_domain }}"
body_format: json
body:
name: "{{ opnform_admin_name }}"
email: "{{ opnform_admin_email }}"
password: "{{ opnform_admin_password }}"
password_confirmation: "{{ opnform_admin_password }}"
hear_about_us: "{{ opnform_admin_hear_about_us }}"
status_code: [200, 201]
validate_certs: false
no_log: true
when:
- opnform_admin_email | length > 0
- opnform_admin_password | length > 0
- opnform_admin_login.status != 200
# =====================================================================
# 6. OIDC IDENTITY CONNECTION (optional)
# =====================================================================
# Creates a single OIDC connection on the admin's default workspace.
# OpnForm enforces one OIDC connection per workspace, so this block is
# idempotent: we GET existing connections first and skip if any exists.
- name: Log in as admin to obtain OIDC API token
ansible.builtin.uri:
url: "https://127.0.0.1/api/login"
method: POST
headers:
Host: "{{ opnform_domain }}"
body_format: json
body:
email: "{{ opnform_admin_email }}"
password: "{{ opnform_admin_password }}"
status_code: 200
validate_certs: false
register: opnform_oidc_token
no_log: true
when: opnform_oidc_enabled | bool
- name: Fetch admin's workspaces
ansible.builtin.uri:
url: "https://127.0.0.1/api/open/workspaces"
method: GET
headers:
Host: "{{ opnform_domain }}"
Authorization: "Bearer {{ opnform_oidc_token.json.token }}"
status_code: 200
validate_certs: false
register: opnform_workspaces
no_log: true
when: opnform_oidc_enabled | bool
- name: Fetch existing OIDC connections for the default workspace
ansible.builtin.uri:
url: "https://127.0.0.1/api/open/workspaces/{{ opnform_workspaces.json[0].id }}/oidc-connections"
method: GET
headers:
Host: "{{ opnform_domain }}"
Authorization: "Bearer {{ opnform_oidc_token.json.token }}"
status_code: 200
validate_certs: false
register: opnform_existing_oidc
no_log: true
when: opnform_oidc_enabled | bool
- name: Resolve OIDC group-role mappings
ansible.builtin.set_fact:
_opnform_oidc_group_role_mappings: >-
{{
opnform_oidc_group_role_mappings
if (opnform_oidc_group_role_mappings | length > 0)
else
([{'idp_group': opnform_oidc_admin_group, 'role': 'admin'}]
if (opnform_oidc_admin_group | length > 0) else [])
}}
when: opnform_oidc_enabled | bool
- name: Create OIDC identity connection
ansible.builtin.uri:
url: "https://127.0.0.1/api/open/workspaces/{{ opnform_workspaces.json[0].id }}/oidc-connections"
method: POST
headers:
Host: "{{ opnform_domain }}"
Authorization: "Bearer {{ opnform_oidc_token.json.token }}"
body_format: json
body:
name: "{{ opnform_oidc_client_name }}"
slug: "{{ opnform_oidc_slug }}"
domain: "{{ opnform_oidc_domain }}"
issuer: "{{ opnform_oidc_issuer }}"
client_id: "{{ opnform_oidc_client_id }}"
client_secret: "{{ opnform_oidc_client_secret }}"
scopes: "{{ opnform_oidc_scopes }}"
enabled: true
options:
require_state: true
group_role_mappings: "{{ _opnform_oidc_group_role_mappings }}"
status_code: [201]
validate_certs: false
no_log: true
when:
- opnform_oidc_enabled | bool
- opnform_existing_oidc.json | length == 0
- name: Display deployment info
ansible.builtin.debug:
msg: |-
OpnForm deployed at {{ opnform_base_url }}
{% if opnform_admin_email | length > 0 %}
Admin user bootstrapped:
Email: {{ opnform_admin_email }}
Password: (from opnform_admin_password)
{% else %}
No admin bootstrap configured — visit {{ opnform_base_url }} and
complete the self-hosted setup page to create the first user.
Set opnform_admin_email + opnform_admin_password to automate this.
{% endif %}
{% if opnform_oidc_enabled %}
OIDC: connection "{{ opnform_oidc_client_name }}" bootstrapped
(slug: {{ opnform_oidc_slug }}, domain: {{ opnform_oidc_domain }})
Users with @{{ opnform_oidc_domain }} addresses will be
redirected to {{ opnform_oidc_issuer }} on login.
{% else %}
OIDC: disabled (set opnform_oidc_enabled=true to auto-configure)
{% endif %}

View file

@ -1,189 +0,0 @@
#---------------------------------------------------------------------#
# 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

View file

@ -1,43 +0,0 @@
map $original_uri $api_uri {
~^/api(/.*$) $1;
default $original_uri;
}
server {
listen 80;
server_name {{ opnform_domain }};
root /app/public;
client_max_body_size {% raw %}${NGINX_MAX_BODY_SIZE}{% endraw %};
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 X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-Host $host;
proxy_set_header X-Forwarded-Port $server_port;
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 /usr/share/nginx/html/public/index.php;
fastcgi_param REQUEST_URI $api_uri;
}
}

View file

@ -1,2 +0,0 @@
#SPDX-License-Identifier: MIT-0
localhost

View file

@ -1,6 +0,0 @@
#SPDX-License-Identifier: MIT-0
---
- hosts: localhost
remote_user: root
roles:
- opnform

View file

@ -1,3 +0,0 @@
#SPDX-License-Identifier: MIT-0
---
# vars file for opnform