digitalboard.core/roles/OpnForm/README.md

4.5 KiB

opnform

Deploy 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-create an admin user (use the default credentials below)
  • 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

After the role completes, OpnForm seeds a default admin user. Visit the URL in opnform_base_url and log in with:

  • Email: admin@opnform.com
  • Password: password

On first login OpnForm will prompt you to change email and password. Self-hosted instances disable public registration after this — invite further users via the Admin UI.

If the login does not respond

The DB seed may have failed. Re-run it manually:

cd /etc/docker/compose/opnform
docker compose exec api php artisan migrate:refresh --seed
docker compose exec api php artisan app:init-project

OIDC setup (stage 2, not yet automated)

Manual setup via the Admin UI is currently the supported path:

  1. Settings → Identity Connections → Add Connection
  2. Provider: OIDC
  3. Issuer: https://auth.digitalboard.ch/realms/Digitalboard
  4. Client ID / Secret: from your Keycloak client
  5. Add Group Role Mapping: Entra/Keycloak group Object ID → OpnForm role

Direct DB manipulation of identity_connections / group_role_mappings is possible but fragile across OpnForm versions. A future iteration of this role may automate it.

Example playbook

- name: Deploy OpnForm service
  hosts: opnform_servers
  become: true
  roles:
    - digitalboard.core.opnform

With inventory variables:

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