# 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-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: ```bash 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 ```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 }}" ```