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, andingress(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 toapi:9000 - Rewrite request URIs via the
$api_urimap - 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:
- Settings → Identity Connections → Add Connection
- Provider: OIDC
- Issuer:
https://auth.digitalboard.ch/realms/Digitalboard - Client ID / Secret: from your Keycloak client
- 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 }}"