Compare commits
3 commits
9a9039c4d3
...
12f25f5aeb
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
12f25f5aeb | ||
|
|
51919829e3 | ||
| 27255a4bfa |
28 changed files with 2 additions and 1888 deletions
|
|
@ -19,7 +19,6 @@ readme: README.md
|
||||||
authors:
|
authors:
|
||||||
- Bert-Jan Fikse <bert-jan@whatwedo.ch>
|
- Bert-Jan Fikse <bert-jan@whatwedo.ch>
|
||||||
- Tobias Wüst <tobias.wuest@wksbern.ch>
|
- Tobias Wüst <tobias.wuest@wksbern.ch>
|
||||||
- Simon Bärlocher <simon@whatwedo.ch>
|
|
||||||
|
|
||||||
### OPTIONAL but strongly recommended
|
### OPTIONAL but strongly recommended
|
||||||
# A short summary description of the collection
|
# A short summary description of the collection
|
||||||
|
|
|
||||||
|
|
@ -1,145 +0,0 @@
|
||||||
# Ansible Role: bookstack
|
|
||||||
|
|
||||||
Deploys [BookStack](https://www.bookstackapp.com/) as a self-contained Docker
|
|
||||||
Compose stack behind Traefik, with its own MariaDB container, OIDC SSO
|
|
||||||
(Entra ID by default) and a daily systemd-timer driven backup of database
|
|
||||||
and uploads.
|
|
||||||
|
|
||||||
## Requirements
|
|
||||||
|
|
||||||
- Docker Engine + Compose plugin on the target host
|
|
||||||
- Traefik already running, with the external network referenced by
|
|
||||||
`bookstack_traefik_network` (default: `proxy`)
|
|
||||||
- `community.docker` collection on the controller
|
|
||||||
- DNS for `bookstack_domain` pointing at the Traefik host
|
|
||||||
|
|
||||||
## Required variables
|
|
||||||
|
|
||||||
The role asserts these are set; the play fails fast if any is empty:
|
|
||||||
|
|
||||||
| Variable | Description |
|
|
||||||
|---|---|
|
|
||||||
| `bookstack_db_root_password` | MariaDB root password |
|
|
||||||
| `bookstack_db_password` | MariaDB user password |
|
|
||||||
| `bookstack_admin_password` | Initial local admin password |
|
|
||||||
| `bookstack_oidc_client_id` | Entra ID App Registration ID (if OIDC on) |
|
|
||||||
| `bookstack_oidc_client_secret` | Entra ID client secret (if OIDC on) |
|
|
||||||
| `bookstack_entra_tenant_id` | Entra tenant UUID (if OIDC on) |
|
|
||||||
|
|
||||||
Provide via OpenBao lookup, Ansible Vault or `--extra-vars`. Never commit
|
|
||||||
real secrets.
|
|
||||||
|
|
||||||
## Optional variables
|
|
||||||
|
|
||||||
See `defaults/main.yml`. Frequently overridden:
|
|
||||||
|
|
||||||
- `bookstack_domain`, `bookstack_base_url`
|
|
||||||
- `bookstack_image`, `bookstack_db_image` (pin in production)
|
|
||||||
- `bookstack_oidc_enabled` (set `false` to disable OIDC entirely)
|
|
||||||
- `bookstack_oidc_auto_initiate` (`true` redirects straight to IdP)
|
|
||||||
- `bookstack_oidc_user_to_groups` (`true` syncs roles from Entra groups)
|
|
||||||
- `bookstack_backup_enabled`, `bookstack_backup_schedule`,
|
|
||||||
`bookstack_backup_retention_days`
|
|
||||||
|
|
||||||
## Entra ID app registration
|
|
||||||
|
|
||||||
1. Azure Portal → Entra ID → App registrations → New registration
|
|
||||||
2. Redirect URI (Web): `https://<bookstack_domain>/oidc/callback`
|
|
||||||
3. Front-channel logout URL: `https://<bookstack_domain>/logout`
|
|
||||||
4. Certificates & secrets → New client secret →
|
|
||||||
`bookstack_oidc_client_secret`
|
|
||||||
5. For group sync (`bookstack_oidc_user_to_groups: true`):
|
|
||||||
- Token configuration → Add groups claim → Security groups
|
|
||||||
- In BookStack, create roles whose **External Auth ID** equals the
|
|
||||||
Entra group Object ID, so the mapping resolves on first login.
|
|
||||||
|
|
||||||
## What the role does
|
|
||||||
|
|
||||||
| Phase | Action |
|
|
||||||
|---|---|
|
|
||||||
| Validate | `assert` all required secrets are set |
|
|
||||||
| Prepare | install packages, create volume dirs, generate persistent `APP_KEY`, verify Traefik network |
|
|
||||||
| Deploy | render `docker-compose.yml`, pull images, bring stack up |
|
|
||||||
| Configure | wait for the app, create the initial local admin via `php artisan bookstack:create-admin` (idempotent) |
|
|
||||||
| Backup | render `/usr/local/bin/bookstack-backup.sh` + systemd timer (daily 03:00, 14-day retention) |
|
|
||||||
|
|
||||||
## Example playbook
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
- name: Deploy BookStack service
|
|
||||||
hosts: bookstack_servers
|
|
||||||
become: true
|
|
||||||
roles:
|
|
||||||
- digitalboard.core.bookstack
|
|
||||||
```
|
|
||||||
|
|
||||||
With inventory variables:
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
# group_vars/bookstack_servers.yml
|
|
||||||
bookstack_domain: wiki.digitalboard.ch
|
|
||||||
bookstack_base_url: "https://wiki.digitalboard.ch"
|
|
||||||
bookstack_entra_tenant_id: "{{ lookup('community.hashi_vault.vault_kv2_get',
|
|
||||||
'digitalboard/bookstack',
|
|
||||||
mount_point='kv').data.data.tenant_id }}"
|
|
||||||
bookstack_oidc_client_id: "{{ lookup('community.hashi_vault.vault_kv2_get',
|
|
||||||
'digitalboard/bookstack',
|
|
||||||
mount_point='kv').data.data.client_id }}"
|
|
||||||
bookstack_oidc_client_secret: "{{ lookup('community.hashi_vault.vault_kv2_get',
|
|
||||||
'digitalboard/bookstack',
|
|
||||||
mount_point='kv').data.data.client_secret }}"
|
|
||||||
bookstack_db_root_password: "{{ lookup('community.hashi_vault.vault_kv2_get',
|
|
||||||
'digitalboard/bookstack',
|
|
||||||
mount_point='kv').data.data.db_root_password }}"
|
|
||||||
bookstack_db_password: "{{ lookup('community.hashi_vault.vault_kv2_get',
|
|
||||||
'digitalboard/bookstack',
|
|
||||||
mount_point='kv').data.data.db_password }}"
|
|
||||||
bookstack_admin_password: "{{ lookup('community.hashi_vault.vault_kv2_get',
|
|
||||||
'digitalboard/bookstack',
|
|
||||||
mount_point='kv').data.data.admin_password }}"
|
|
||||||
```
|
|
||||||
|
|
||||||
## Backup / restore
|
|
||||||
|
|
||||||
Backups land in `{{ bookstack_backup_dir }}` (default
|
|
||||||
`/srv/data/bookstack/backup`) with three files per run:
|
|
||||||
|
|
||||||
- `bookstack-db-<stamp>.sql.gz` — mariadb-dump
|
|
||||||
- `bookstack-files-<stamp>.tar.gz` — uploads, attachments
|
|
||||||
- `bookstack-appkey-<stamp>.txt` — APP_KEY (required for restore!)
|
|
||||||
|
|
||||||
Manual trigger: `systemctl start bookstack-backup.service`
|
|
||||||
Timer status: `systemctl list-timers bookstack-backup.timer`
|
|
||||||
|
|
||||||
Restore procedure:
|
|
||||||
|
|
||||||
1. Stop the stack: `docker compose down` in `bookstack_docker_compose_dir`
|
|
||||||
2. Restore the APP_KEY: copy the `.txt` content to
|
|
||||||
`{{ bookstack_docker_volume_dir }}/.app_key` (the key MUST match or
|
|
||||||
encrypted DB values become unreadable)
|
|
||||||
3. Start only the DB container, then load the dump:
|
|
||||||
```bash
|
|
||||||
gunzip -c bookstack-db-<stamp>.sql.gz \
|
|
||||||
| docker exec -i bookstack-db \
|
|
||||||
mariadb -u root -p"<root-pw>" bookstack
|
|
||||||
```
|
|
||||||
4. Extract the files: `tar -xzf bookstack-files-<stamp>.tar.gz -C
|
|
||||||
{{ bookstack_appdata_dir }}/www/`
|
|
||||||
5. Bring the stack back up: `docker compose up -d`
|
|
||||||
|
|
||||||
## Notes
|
|
||||||
|
|
||||||
- `bookstack_oidc_auto_initiate: false` (default) shows a login page
|
|
||||||
with an SSO button alongside the local login form. With `true`, users
|
|
||||||
go straight to the IdP — the local admin then has to use
|
|
||||||
`https://<domain>/login?email_login=1`.
|
|
||||||
- `bookstack_oidc_user_to_groups: true` only makes sense once BookStack
|
|
||||||
roles with the correct **External Auth IDs** (= Entra group Object
|
|
||||||
IDs) exist; otherwise users lose their role assignment on every login.
|
|
||||||
- Image tags default to pinned versions; bump them deliberately rather
|
|
||||||
than chasing `latest`.
|
|
||||||
- BookStack officially supports MySQL/MariaDB only — no PostgreSQL.
|
|
||||||
|
|
||||||
## License
|
|
||||||
|
|
||||||
MIT
|
|
||||||
|
|
@ -1,85 +0,0 @@
|
||||||
#SPDX-License-Identifier: MIT-0
|
|
||||||
---
|
|
||||||
# defaults file for bookstack
|
|
||||||
|
|
||||||
# Base directory configuration (inherited from base role or defined here)
|
|
||||||
docker_compose_base_dir: /etc/docker/compose
|
|
||||||
docker_volume_base_dir: /srv/data
|
|
||||||
|
|
||||||
# bookstack-specific configuration
|
|
||||||
bookstack_service_name: bookstack
|
|
||||||
bookstack_docker_compose_dir: "{{ docker_compose_base_dir }}/{{ bookstack_service_name }}"
|
|
||||||
bookstack_docker_volume_dir: "{{ docker_volume_base_dir }}/{{ bookstack_service_name }}"
|
|
||||||
bookstack_appdata_dir: "{{ bookstack_docker_volume_dir }}/appdata"
|
|
||||||
bookstack_db_data_dir: "{{ bookstack_docker_volume_dir }}/db"
|
|
||||||
bookstack_backup_dir: "{{ bookstack_docker_volume_dir }}/backup"
|
|
||||||
|
|
||||||
# Service configuration
|
|
||||||
bookstack_domain: "wiki.local.test"
|
|
||||||
bookstack_base_url: "https://{{ bookstack_domain }}"
|
|
||||||
|
|
||||||
# Images — pin via inventory in production
|
|
||||||
bookstack_image: "lscr.io/linuxserver/bookstack:version-v26.03.3"
|
|
||||||
bookstack_db_image: "lscr.io/linuxserver/mariadb:11.4.9"
|
|
||||||
|
|
||||||
# Traefik configuration
|
|
||||||
bookstack_traefik_network: "proxy"
|
|
||||||
bookstack_traefik_certresolver: "le"
|
|
||||||
|
|
||||||
# Timezone / UID
|
|
||||||
bookstack_tz: "Europe/Zurich"
|
|
||||||
bookstack_puid: "1000"
|
|
||||||
bookstack_pgid: "1000"
|
|
||||||
|
|
||||||
# Database configuration
|
|
||||||
bookstack_db_name: "bookstack"
|
|
||||||
bookstack_db_user: "bookstack"
|
|
||||||
|
|
||||||
# REQUIRED SECRETS — empty defaults force `assert` to fail until set.
|
|
||||||
# Provide via OpenBao lookup, Ansible Vault, or extra-vars.
|
|
||||||
# Never commit real secrets to version control.
|
|
||||||
#
|
|
||||||
# Generate with:
|
|
||||||
# bookstack_db_root_password: openssl rand -base64 32 | tr -d '/+='
|
|
||||||
# bookstack_db_password: openssl rand -base64 32 | tr -d '/+='
|
|
||||||
# bookstack_admin_password: openssl rand -base64 24 | tr -d '/+='
|
|
||||||
bookstack_db_root_password: ""
|
|
||||||
bookstack_db_password: ""
|
|
||||||
bookstack_admin_password: ""
|
|
||||||
bookstack_oidc_client_secret: ""
|
|
||||||
|
|
||||||
# APP_KEY is generated automatically on first run and persisted on the host.
|
|
||||||
# Set explicitly only if restoring an existing instance.
|
|
||||||
bookstack_app_key: ""
|
|
||||||
|
|
||||||
# Initial local admin (fallback account, lives alongside OIDC)
|
|
||||||
bookstack_admin_name: "Admin"
|
|
||||||
bookstack_admin_email: "admin@local.test"
|
|
||||||
bookstack_artisan_path: "/app/www/artisan"
|
|
||||||
|
|
||||||
# Mail configuration
|
|
||||||
bookstack_mail_driver: "smtp"
|
|
||||||
bookstack_mail_host: "smtp.local.test"
|
|
||||||
bookstack_mail_port: 587
|
|
||||||
bookstack_mail_encryption: "tls"
|
|
||||||
bookstack_mail_from: "bookstack@local.test"
|
|
||||||
bookstack_mail_from_name: "BookStack"
|
|
||||||
bookstack_mail_username: ""
|
|
||||||
bookstack_mail_password: ""
|
|
||||||
|
|
||||||
# OIDC configuration (Entra ID by default; override `bookstack_oidc_issuer`
|
|
||||||
# for Keycloak or any other provider)
|
|
||||||
bookstack_oidc_enabled: false
|
|
||||||
bookstack_oidc_name: "SSO"
|
|
||||||
bookstack_entra_tenant_id: ""
|
|
||||||
bookstack_oidc_issuer: "https://login.microsoftonline.com/{{ bookstack_entra_tenant_id }}/v2.0"
|
|
||||||
bookstack_oidc_client_id: ""
|
|
||||||
bookstack_oidc_auto_initiate: false
|
|
||||||
bookstack_oidc_user_to_groups: false
|
|
||||||
bookstack_oidc_groups_claim: "groups"
|
|
||||||
bookstack_oidc_additional_scopes: "openid profile email"
|
|
||||||
|
|
||||||
# Backup configuration
|
|
||||||
bookstack_backup_enabled: true
|
|
||||||
bookstack_backup_retention_days: 14
|
|
||||||
bookstack_backup_schedule: "*-*-* 03:00:00"
|
|
||||||
|
|
@ -1,19 +0,0 @@
|
||||||
#SPDX-License-Identifier: MIT-0
|
|
||||||
---
|
|
||||||
# handlers file for bookstack
|
|
||||||
|
|
||||||
- name: stop bookstack
|
|
||||||
community.docker.docker_compose_v2:
|
|
||||||
project_src: "{{ bookstack_docker_compose_dir }}"
|
|
||||||
state: stopped
|
|
||||||
listen: restart bookstack
|
|
||||||
|
|
||||||
- name: start bookstack
|
|
||||||
community.docker.docker_compose_v2:
|
|
||||||
project_src: "{{ bookstack_docker_compose_dir }}"
|
|
||||||
state: present
|
|
||||||
listen: restart bookstack
|
|
||||||
|
|
||||||
- name: reload systemd
|
|
||||||
ansible.builtin.systemd:
|
|
||||||
daemon_reload: true
|
|
||||||
|
|
@ -1,194 +0,0 @@
|
||||||
---
|
|
||||||
argument_specs:
|
|
||||||
main:
|
|
||||||
short_description: Deploy BookStack (LSIO image + MariaDB) via Docker Compose.
|
|
||||||
description:
|
|
||||||
- Renders a Compose stack for the linuxserver.io BookStack image
|
|
||||||
with a sibling MariaDB container behind Traefik, then bootstraps
|
|
||||||
the initial admin user via C(php artisan bookstack:create-admin)
|
|
||||||
and optionally enables OIDC SSO (Entra ID by default).
|
|
||||||
- "Persists the Laravel C(APP_KEY) on the host so the same key is
|
|
||||||
re-used across deploys (a fresh key would orphan all encrypted
|
|
||||||
database values: 2FA secrets, API tokens, OIDC client_secret)."
|
|
||||||
- Ships an optional systemd timer that backs up the database dump,
|
|
||||||
uploads tarball and APP_KEY daily with configurable retention.
|
|
||||||
options:
|
|
||||||
docker_compose_base_dir:
|
|
||||||
type: path
|
|
||||||
default: /etc/docker/compose
|
|
||||||
docker_volume_base_dir:
|
|
||||||
type: path
|
|
||||||
default: /srv/data
|
|
||||||
bookstack_service_name:
|
|
||||||
type: str
|
|
||||||
default: bookstack
|
|
||||||
bookstack_docker_compose_dir:
|
|
||||||
type: path
|
|
||||||
bookstack_docker_volume_dir:
|
|
||||||
type: path
|
|
||||||
bookstack_appdata_dir:
|
|
||||||
type: path
|
|
||||||
bookstack_db_data_dir:
|
|
||||||
type: path
|
|
||||||
bookstack_backup_dir:
|
|
||||||
type: path
|
|
||||||
|
|
||||||
bookstack_domain:
|
|
||||||
type: str
|
|
||||||
default: wiki.local.test
|
|
||||||
description: Hostname used in the Traefik Host rule.
|
|
||||||
bookstack_base_url:
|
|
||||||
type: str
|
|
||||||
description: Defaults to C("https://{{ bookstack_domain }}").
|
|
||||||
|
|
||||||
bookstack_image:
|
|
||||||
type: str
|
|
||||||
default: "lscr.io/linuxserver/bookstack:version-v26.03.3"
|
|
||||||
bookstack_db_image:
|
|
||||||
type: str
|
|
||||||
default: "lscr.io/linuxserver/mariadb:11.4.9"
|
|
||||||
|
|
||||||
bookstack_traefik_network:
|
|
||||||
type: str
|
|
||||||
default: proxy
|
|
||||||
bookstack_traefik_certresolver:
|
|
||||||
type: str
|
|
||||||
default: le
|
|
||||||
|
|
||||||
bookstack_tz:
|
|
||||||
type: str
|
|
||||||
default: Europe/Zurich
|
|
||||||
bookstack_puid:
|
|
||||||
type: str
|
|
||||||
default: "1000"
|
|
||||||
bookstack_pgid:
|
|
||||||
type: str
|
|
||||||
default: "1000"
|
|
||||||
|
|
||||||
bookstack_db_name:
|
|
||||||
type: str
|
|
||||||
default: bookstack
|
|
||||||
bookstack_db_user:
|
|
||||||
type: str
|
|
||||||
default: bookstack
|
|
||||||
bookstack_db_root_password:
|
|
||||||
type: str
|
|
||||||
required: true
|
|
||||||
description: MariaDB C(root) password. Override per-inventory.
|
|
||||||
bookstack_db_password:
|
|
||||||
type: str
|
|
||||||
required: true
|
|
||||||
description: MariaDB C(bookstack_db_user) password. Override per-inventory.
|
|
||||||
|
|
||||||
bookstack_admin_password:
|
|
||||||
type: str
|
|
||||||
required: true
|
|
||||||
description:
|
|
||||||
- Password for the local admin user that the role creates via
|
|
||||||
C(bookstack:create-admin). Lives alongside any OIDC users.
|
|
||||||
|
|
||||||
bookstack_app_key:
|
|
||||||
type: str
|
|
||||||
default: ''
|
|
||||||
description:
|
|
||||||
- When empty the role generates a persistent C(APP_KEY) on first
|
|
||||||
run and stores it under C({{ bookstack_docker_volume_dir }}/.app_key).
|
|
||||||
Override only when restoring an existing instance — a mismatching
|
|
||||||
key orphans all encrypted database values.
|
|
||||||
|
|
||||||
bookstack_admin_name:
|
|
||||||
type: str
|
|
||||||
default: Admin
|
|
||||||
bookstack_admin_email:
|
|
||||||
type: str
|
|
||||||
default: admin@local.test
|
|
||||||
bookstack_artisan_path:
|
|
||||||
type: path
|
|
||||||
default: /app/www/artisan
|
|
||||||
description:
|
|
||||||
- Path to BookStack's C(artisan) script inside the container. The
|
|
||||||
LSIO image's C(WORKDIR) is not the app directory, so this must
|
|
||||||
be absolute.
|
|
||||||
|
|
||||||
bookstack_mail_driver:
|
|
||||||
type: str
|
|
||||||
choices: [smtp, log, sendmail, mailgun, ses, postmark]
|
|
||||||
default: smtp
|
|
||||||
bookstack_mail_host:
|
|
||||||
type: str
|
|
||||||
default: smtp.local.test
|
|
||||||
bookstack_mail_port:
|
|
||||||
type: int
|
|
||||||
default: 587
|
|
||||||
bookstack_mail_encryption:
|
|
||||||
type: str
|
|
||||||
choices: [tls, ssl, '']
|
|
||||||
default: tls
|
|
||||||
bookstack_mail_from:
|
|
||||||
type: str
|
|
||||||
default: bookstack@local.test
|
|
||||||
bookstack_mail_from_name:
|
|
||||||
type: str
|
|
||||||
default: BookStack
|
|
||||||
bookstack_mail_username:
|
|
||||||
type: str
|
|
||||||
default: ''
|
|
||||||
bookstack_mail_password:
|
|
||||||
type: str
|
|
||||||
default: ''
|
|
||||||
|
|
||||||
bookstack_oidc_enabled:
|
|
||||||
type: bool
|
|
||||||
default: false
|
|
||||||
bookstack_oidc_name:
|
|
||||||
type: str
|
|
||||||
default: SSO
|
|
||||||
description: Display name of the SSO button on the login page.
|
|
||||||
bookstack_entra_tenant_id:
|
|
||||||
type: str
|
|
||||||
default: ''
|
|
||||||
description: Entra tenant UUID. Required when C(bookstack_oidc_enabled=true).
|
|
||||||
bookstack_oidc_issuer:
|
|
||||||
type: str
|
|
||||||
description:
|
|
||||||
- OIDC issuer URL. Defaults to the Entra v2 issuer template
|
|
||||||
built from C(bookstack_entra_tenant_id). Override for
|
|
||||||
Keycloak or any other provider.
|
|
||||||
bookstack_oidc_client_id:
|
|
||||||
type: str
|
|
||||||
default: ''
|
|
||||||
description: Required when C(bookstack_oidc_enabled=true).
|
|
||||||
bookstack_oidc_client_secret:
|
|
||||||
type: str
|
|
||||||
default: ''
|
|
||||||
description: Required when C(bookstack_oidc_enabled=true).
|
|
||||||
bookstack_oidc_auto_initiate:
|
|
||||||
type: bool
|
|
||||||
default: false
|
|
||||||
description:
|
|
||||||
- When true users are redirected straight to the IdP and the
|
|
||||||
local login is reachable only via C(?email_login=1).
|
|
||||||
bookstack_oidc_user_to_groups:
|
|
||||||
type: bool
|
|
||||||
default: false
|
|
||||||
description:
|
|
||||||
- When true BookStack syncs roles from the IdP groups claim
|
|
||||||
on every login. Requires BookStack roles whose
|
|
||||||
C(External Auth ID) matches the IdP group's Object ID.
|
|
||||||
bookstack_oidc_groups_claim:
|
|
||||||
type: str
|
|
||||||
default: groups
|
|
||||||
bookstack_oidc_additional_scopes:
|
|
||||||
type: str
|
|
||||||
default: openid profile email
|
|
||||||
|
|
||||||
bookstack_backup_enabled:
|
|
||||||
type: bool
|
|
||||||
default: true
|
|
||||||
bookstack_backup_retention_days:
|
|
||||||
type: int
|
|
||||||
default: 14
|
|
||||||
bookstack_backup_schedule:
|
|
||||||
type: str
|
|
||||||
default: "*-*-* 03:00:00"
|
|
||||||
description: systemd C(OnCalendar) expression for the backup timer.
|
|
||||||
|
|
@ -1,25 +0,0 @@
|
||||||
galaxy_info:
|
|
||||||
author: digitalboard
|
|
||||||
description: Deploy BookStack as a self-contained Docker Compose stack behind Traefik
|
|
||||||
company: digitalboard
|
|
||||||
license: MIT
|
|
||||||
|
|
||||||
min_ansible_version: "2.14"
|
|
||||||
|
|
||||||
platforms:
|
|
||||||
- name: Debian
|
|
||||||
versions:
|
|
||||||
- bookworm
|
|
||||||
- name: Ubuntu
|
|
||||||
versions:
|
|
||||||
- jammy
|
|
||||||
- noble
|
|
||||||
|
|
||||||
galaxy_tags:
|
|
||||||
- docker
|
|
||||||
- bookstack
|
|
||||||
- wiki
|
|
||||||
- documentation
|
|
||||||
- digitalboard
|
|
||||||
|
|
||||||
dependencies: []
|
|
||||||
|
|
@ -1,223 +0,0 @@
|
||||||
#SPDX-License-Identifier: MIT-0
|
|
||||||
---
|
|
||||||
# tasks file for bookstack
|
|
||||||
|
|
||||||
# =====================================================================
|
|
||||||
# 1. VALIDATE REQUIRED SECRETS
|
|
||||||
# =====================================================================
|
|
||||||
|
|
||||||
- name: Assert required secrets are set
|
|
||||||
ansible.builtin.assert:
|
|
||||||
that:
|
|
||||||
- bookstack_db_root_password | length > 0
|
|
||||||
- bookstack_db_password | length > 0
|
|
||||||
- bookstack_admin_password | length > 0
|
|
||||||
- (not bookstack_oidc_enabled) or (bookstack_oidc_client_id | length > 0)
|
|
||||||
- (not bookstack_oidc_enabled) or (bookstack_oidc_client_secret | length > 0)
|
|
||||||
- (not bookstack_oidc_enabled) or (bookstack_entra_tenant_id | length > 0)
|
|
||||||
fail_msg: >-
|
|
||||||
One or more required secrets are unset. Provide them via OpenBao
|
|
||||||
lookup, Ansible Vault or --extra-vars. See README for the full list.
|
|
||||||
quiet: true
|
|
||||||
|
|
||||||
# =====================================================================
|
|
||||||
# 2. PREPARATION: Packages, directories, APP_KEY
|
|
||||||
# =====================================================================
|
|
||||||
|
|
||||||
- name: Ensure required packages are installed
|
|
||||||
ansible.builtin.package:
|
|
||||||
name:
|
|
||||||
- python3-docker
|
|
||||||
- python3-requests
|
|
||||||
state: present
|
|
||||||
|
|
||||||
- name: Create docker compose directory
|
|
||||||
ansible.builtin.file:
|
|
||||||
path: "{{ bookstack_docker_compose_dir }}"
|
|
||||||
state: directory
|
|
||||||
mode: '0755'
|
|
||||||
|
|
||||||
- name: Create BookStack data directories
|
|
||||||
ansible.builtin.file:
|
|
||||||
path: "{{ item }}"
|
|
||||||
state: directory
|
|
||||||
owner: "{{ bookstack_puid }}"
|
|
||||||
group: "{{ bookstack_pgid }}"
|
|
||||||
mode: '0755'
|
|
||||||
loop:
|
|
||||||
- "{{ bookstack_docker_volume_dir }}"
|
|
||||||
- "{{ bookstack_appdata_dir }}"
|
|
||||||
- "{{ bookstack_db_data_dir }}"
|
|
||||||
- "{{ bookstack_backup_dir }}"
|
|
||||||
|
|
||||||
- name: Verify Traefik network exists
|
|
||||||
community.docker.docker_network_info:
|
|
||||||
name: "{{ bookstack_traefik_network }}"
|
|
||||||
register: _traefik_net
|
|
||||||
failed_when: not _traefik_net.exists
|
|
||||||
|
|
||||||
- name: Check whether APP_KEY has been generated before
|
|
||||||
ansible.builtin.stat:
|
|
||||||
path: "{{ bookstack_docker_volume_dir }}/.app_key"
|
|
||||||
register: _app_key_file
|
|
||||||
|
|
||||||
- name: Generate persistent APP_KEY on first run
|
|
||||||
ansible.builtin.shell: |
|
|
||||||
set -o pipefail
|
|
||||||
umask 077
|
|
||||||
echo "base64:$(openssl rand -base64 32)" > {{ bookstack_docker_volume_dir }}/.app_key
|
|
||||||
args:
|
|
||||||
executable: /bin/bash
|
|
||||||
creates: "{{ bookstack_docker_volume_dir }}/.app_key"
|
|
||||||
when:
|
|
||||||
- not _app_key_file.stat.exists
|
|
||||||
- bookstack_app_key | length == 0
|
|
||||||
|
|
||||||
- name: Write inventory-provided APP_KEY
|
|
||||||
ansible.builtin.copy:
|
|
||||||
content: "{{ bookstack_app_key }}\n"
|
|
||||||
dest: "{{ bookstack_docker_volume_dir }}/.app_key"
|
|
||||||
mode: '0600'
|
|
||||||
when:
|
|
||||||
- not _app_key_file.stat.exists
|
|
||||||
- bookstack_app_key | length > 0
|
|
||||||
no_log: true
|
|
||||||
|
|
||||||
- name: Read APP_KEY back into a fact
|
|
||||||
ansible.builtin.slurp:
|
|
||||||
src: "{{ bookstack_docker_volume_dir }}/.app_key"
|
|
||||||
register: _app_key_slurp
|
|
||||||
no_log: true
|
|
||||||
|
|
||||||
- name: Register APP_KEY fact
|
|
||||||
ansible.builtin.set_fact:
|
|
||||||
bookstack_resolved_app_key: "{{ _app_key_slurp.content | b64decode | trim }}"
|
|
||||||
no_log: true
|
|
||||||
|
|
||||||
# =====================================================================
|
|
||||||
# 3. DEPLOY: Render compose, bring stack up
|
|
||||||
# =====================================================================
|
|
||||||
|
|
||||||
- name: Render docker-compose.yml for BookStack
|
|
||||||
ansible.builtin.template:
|
|
||||||
src: docker-compose.yml.j2
|
|
||||||
dest: "{{ bookstack_docker_compose_dir }}/docker-compose.yml"
|
|
||||||
mode: '0640'
|
|
||||||
notify: restart bookstack
|
|
||||||
|
|
||||||
- name: Start BookStack containers
|
|
||||||
community.docker.docker_compose_v2:
|
|
||||||
project_src: "{{ bookstack_docker_compose_dir }}"
|
|
||||||
state: present
|
|
||||||
pull: always
|
|
||||||
wait: true
|
|
||||||
|
|
||||||
# =====================================================================
|
|
||||||
# 4. CONFIGURE: Wait for app and seed initial admin user
|
|
||||||
# =====================================================================
|
|
||||||
|
|
||||||
- name: Wait for BookStack to be ready
|
|
||||||
ansible.builtin.command:
|
|
||||||
cmd: docker exec {{ bookstack_service_name }} curl -sf -o /dev/null -w "%{http_code}" http://localhost/login
|
|
||||||
register: _bookstack_health
|
|
||||||
retries: 30
|
|
||||||
delay: 5
|
|
||||||
until: _bookstack_health.stdout == "200"
|
|
||||||
changed_when: false
|
|
||||||
|
|
||||||
- name: Wait for BookStack migrations to be complete
|
|
||||||
community.docker.docker_container_exec:
|
|
||||||
container: "{{ bookstack_service_name }}-db"
|
|
||||||
argv:
|
|
||||||
- mariadb
|
|
||||||
- --protocol=tcp
|
|
||||||
- -h
|
|
||||||
- 127.0.0.1
|
|
||||||
- -u
|
|
||||||
- "{{ bookstack_db_user }}"
|
|
||||||
- "-p{{ bookstack_db_password }}"
|
|
||||||
- "{{ bookstack_db_name }}"
|
|
||||||
- -Nse
|
|
||||||
- "SHOW TABLES LIKE 'users';"
|
|
||||||
register: _users_table
|
|
||||||
retries: 30
|
|
||||||
delay: 5
|
|
||||||
until: _users_table.stdout | trim == 'users'
|
|
||||||
changed_when: false
|
|
||||||
no_log: true
|
|
||||||
|
|
||||||
- name: Check whether the initial admin already exists
|
|
||||||
community.docker.docker_container_exec:
|
|
||||||
container: "{{ bookstack_service_name }}-db"
|
|
||||||
argv:
|
|
||||||
- mariadb
|
|
||||||
- --protocol=tcp
|
|
||||||
- -h
|
|
||||||
- 127.0.0.1
|
|
||||||
- -u
|
|
||||||
- "{{ bookstack_db_user }}"
|
|
||||||
- "-p{{ bookstack_db_password }}"
|
|
||||||
- "{{ bookstack_db_name }}"
|
|
||||||
- -Nse
|
|
||||||
- "SELECT COUNT(*) FROM users WHERE email = '{{ bookstack_admin_email }}';"
|
|
||||||
register: _admin_exists
|
|
||||||
changed_when: false
|
|
||||||
no_log: true
|
|
||||||
|
|
||||||
- name: Create initial admin user
|
|
||||||
community.docker.docker_container_exec:
|
|
||||||
container: "{{ bookstack_service_name }}"
|
|
||||||
argv:
|
|
||||||
- php
|
|
||||||
- "{{ bookstack_artisan_path }}"
|
|
||||||
- bookstack:create-admin
|
|
||||||
- "--email={{ bookstack_admin_email }}"
|
|
||||||
- "--name={{ bookstack_admin_name }}"
|
|
||||||
- "--password={{ bookstack_admin_password }}"
|
|
||||||
when: (_admin_exists.stdout | trim | int) == 0
|
|
||||||
no_log: true
|
|
||||||
|
|
||||||
# =====================================================================
|
|
||||||
# 5. BACKUP: systemd timer for daily DB + uploads dump
|
|
||||||
# =====================================================================
|
|
||||||
|
|
||||||
- name: Render backup script
|
|
||||||
ansible.builtin.template:
|
|
||||||
src: backup.sh.j2
|
|
||||||
dest: /usr/local/bin/bookstack-backup.sh
|
|
||||||
owner: root
|
|
||||||
group: root
|
|
||||||
mode: '0750'
|
|
||||||
when: bookstack_backup_enabled | bool
|
|
||||||
|
|
||||||
- name: Render backup systemd service
|
|
||||||
ansible.builtin.template:
|
|
||||||
src: bookstack-backup.service.j2
|
|
||||||
dest: /etc/systemd/system/bookstack-backup.service
|
|
||||||
mode: '0644'
|
|
||||||
when: bookstack_backup_enabled | bool
|
|
||||||
notify: reload systemd
|
|
||||||
|
|
||||||
- name: Render backup systemd timer
|
|
||||||
ansible.builtin.template:
|
|
||||||
src: bookstack-backup.timer.j2
|
|
||||||
dest: /etc/systemd/system/bookstack-backup.timer
|
|
||||||
mode: '0644'
|
|
||||||
when: bookstack_backup_enabled | bool
|
|
||||||
notify: reload systemd
|
|
||||||
|
|
||||||
- name: Enable and start backup timer
|
|
||||||
ansible.builtin.systemd:
|
|
||||||
name: bookstack-backup.timer
|
|
||||||
enabled: true
|
|
||||||
state: started
|
|
||||||
daemon_reload: true
|
|
||||||
when: bookstack_backup_enabled | bool
|
|
||||||
|
|
||||||
- name: Disable backup timer when feature is off
|
|
||||||
ansible.builtin.systemd:
|
|
||||||
name: bookstack-backup.timer
|
|
||||||
enabled: false
|
|
||||||
state: stopped
|
|
||||||
when: not (bookstack_backup_enabled | bool)
|
|
||||||
failed_when: false
|
|
||||||
|
|
@ -1,41 +0,0 @@
|
||||||
#!/bin/bash
|
|
||||||
# {{ ansible_managed }}
|
|
||||||
set -euo pipefail
|
|
||||||
|
|
||||||
BACKUP_DIR="{{ bookstack_backup_dir }}"
|
|
||||||
RETENTION_DAYS={{ bookstack_backup_retention_days }}
|
|
||||||
APPDATA_DIR="{{ bookstack_appdata_dir }}"
|
|
||||||
STAMP="$(date +%Y%m%d-%H%M%S)"
|
|
||||||
|
|
||||||
mkdir -p "$BACKUP_DIR"
|
|
||||||
|
|
||||||
# --- DB dump (mariadb-dump from inside the DB container) ---
|
|
||||||
# Use the app user via TCP because root@localhost is unix_socket-auth only
|
|
||||||
# in the LSIO MariaDB image and root@% does not exist.
|
|
||||||
docker exec {{ bookstack_service_name }}-db \
|
|
||||||
mariadb-dump \
|
|
||||||
--protocol=tcp -h 127.0.0.1 \
|
|
||||||
-u "{{ bookstack_db_user }}" -p"{{ bookstack_db_password }}" \
|
|
||||||
--single-transaction --routines --triggers --quick \
|
|
||||||
"{{ bookstack_db_name }}" \
|
|
||||||
| gzip -9 > "$BACKUP_DIR/bookstack-db-$STAMP.sql.gz"
|
|
||||||
|
|
||||||
# --- File uploads (images, attachments) ---
|
|
||||||
# LSIO BookStack stores user uploads under /config/www/{uploads,storage/uploads,files}.
|
|
||||||
tar --warning=no-file-changed \
|
|
||||||
-czf "$BACKUP_DIR/bookstack-files-$STAMP.tar.gz" \
|
|
||||||
-C "$APPDATA_DIR/www" \
|
|
||||||
uploads storage/uploads files 2>/dev/null || true
|
|
||||||
|
|
||||||
# --- APP_KEY backup (critical for restore!) ---
|
|
||||||
install -m 0600 "{{ bookstack_docker_volume_dir }}/.app_key" \
|
|
||||||
"$BACKUP_DIR/bookstack-appkey-$STAMP.txt"
|
|
||||||
|
|
||||||
# --- Retention ---
|
|
||||||
find "$BACKUP_DIR" -type f \
|
|
||||||
\( -name 'bookstack-db-*.sql.gz' \
|
|
||||||
-o -name 'bookstack-files-*.tar.gz' \
|
|
||||||
-o -name 'bookstack-appkey-*.txt' \) \
|
|
||||||
-mtime +"$RETENTION_DAYS" -delete
|
|
||||||
|
|
||||||
echo "Backup complete: $STAMP"
|
|
||||||
|
|
@ -1,12 +0,0 @@
|
||||||
# {{ ansible_managed }}
|
|
||||||
[Unit]
|
|
||||||
Description=BookStack backup (DB + uploads)
|
|
||||||
Requires=docker.service
|
|
||||||
After=docker.service
|
|
||||||
|
|
||||||
[Service]
|
|
||||||
Type=oneshot
|
|
||||||
ExecStart=/usr/local/bin/bookstack-backup.sh
|
|
||||||
Nice=10
|
|
||||||
IOSchedulingClass=best-effort
|
|
||||||
IOSchedulingPriority=7
|
|
||||||
|
|
@ -1,11 +0,0 @@
|
||||||
# {{ ansible_managed }}
|
|
||||||
[Unit]
|
|
||||||
Description=Daily BookStack backup
|
|
||||||
|
|
||||||
[Timer]
|
|
||||||
OnCalendar={{ bookstack_backup_schedule }}
|
|
||||||
Persistent=true
|
|
||||||
RandomizedDelaySec=300
|
|
||||||
|
|
||||||
[Install]
|
|
||||||
WantedBy=timers.target
|
|
||||||
|
|
@ -1,87 +0,0 @@
|
||||||
#---------------------------------------------------------------------#
|
|
||||||
# BookStack - Self-hosted wiki / knowledge base. #
|
|
||||||
#---------------------------------------------------------------------#
|
|
||||||
---
|
|
||||||
services:
|
|
||||||
{{ bookstack_service_name }}:
|
|
||||||
image: {{ bookstack_image }}
|
|
||||||
container_name: {{ bookstack_service_name }}
|
|
||||||
restart: unless-stopped
|
|
||||||
environment:
|
|
||||||
PUID: "{{ bookstack_puid }}"
|
|
||||||
PGID: "{{ bookstack_pgid }}"
|
|
||||||
TZ: "{{ bookstack_tz }}"
|
|
||||||
APP_URL: "{{ bookstack_base_url }}"
|
|
||||||
APP_KEY: "{{ bookstack_resolved_app_key }}"
|
|
||||||
DB_HOST: "{{ bookstack_service_name }}-db"
|
|
||||||
DB_PORT: "3306"
|
|
||||||
DB_DATABASE: "{{ bookstack_db_name }}"
|
|
||||||
DB_USERNAME: "{{ bookstack_db_user }}"
|
|
||||||
DB_PASSWORD: "{{ bookstack_db_password }}"
|
|
||||||
MAIL_DRIVER: "{{ bookstack_mail_driver }}"
|
|
||||||
MAIL_HOST: "{{ bookstack_mail_host }}"
|
|
||||||
MAIL_PORT: "{{ bookstack_mail_port }}"
|
|
||||||
MAIL_USERNAME: "{{ bookstack_mail_username }}"
|
|
||||||
MAIL_PASSWORD: "{{ bookstack_mail_password }}"
|
|
||||||
MAIL_ENCRYPTION: "{{ bookstack_mail_encryption }}"
|
|
||||||
MAIL_FROM: "{{ bookstack_mail_from }}"
|
|
||||||
MAIL_FROM_NAME: "{{ bookstack_mail_from_name }}"
|
|
||||||
{% if bookstack_oidc_enabled %}
|
|
||||||
AUTH_METHOD: "oidc"
|
|
||||||
AUTH_AUTO_INITIATE: "{{ bookstack_oidc_auto_initiate | string | lower }}"
|
|
||||||
OIDC_NAME: "{{ bookstack_oidc_name }}"
|
|
||||||
OIDC_DISPLAY_NAME_CLAIMS: "name"
|
|
||||||
OIDC_CLIENT_ID: "{{ bookstack_oidc_client_id }}"
|
|
||||||
OIDC_CLIENT_SECRET: "{{ bookstack_oidc_client_secret }}"
|
|
||||||
OIDC_ISSUER: "{{ bookstack_oidc_issuer }}"
|
|
||||||
OIDC_ISSUER_DISCOVER: "true"
|
|
||||||
OIDC_END_SESSION_ENDPOINT: "true"
|
|
||||||
OIDC_ADDITIONAL_SCOPES: "{{ bookstack_oidc_additional_scopes }}"
|
|
||||||
OIDC_USER_TO_GROUPS: "{{ bookstack_oidc_user_to_groups | string | lower }}"
|
|
||||||
OIDC_GROUPS_CLAIM: "{{ bookstack_oidc_groups_claim }}"
|
|
||||||
{% endif %}
|
|
||||||
volumes:
|
|
||||||
- {{ bookstack_appdata_dir }}:/config
|
|
||||||
networks:
|
|
||||||
- {{ bookstack_traefik_network }}
|
|
||||||
- internal
|
|
||||||
depends_on:
|
|
||||||
{{ bookstack_service_name }}-db:
|
|
||||||
condition: service_healthy
|
|
||||||
labels:
|
|
||||||
- "traefik.enable=true"
|
|
||||||
- "traefik.docker.network={{ bookstack_traefik_network }}"
|
|
||||||
- "traefik.http.routers.{{ bookstack_service_name }}.rule=Host(`{{ bookstack_domain }}`)"
|
|
||||||
- "traefik.http.routers.{{ bookstack_service_name }}.entrypoints=websecure"
|
|
||||||
- "traefik.http.routers.{{ bookstack_service_name }}.tls=true"
|
|
||||||
- "traefik.http.routers.{{ bookstack_service_name }}.tls.certresolver={{ bookstack_traefik_certresolver }}"
|
|
||||||
- "traefik.http.services.{{ bookstack_service_name }}.loadbalancer.server.port=80"
|
|
||||||
|
|
||||||
{{ bookstack_service_name }}-db:
|
|
||||||
image: {{ bookstack_db_image }}
|
|
||||||
container_name: {{ bookstack_service_name }}-db
|
|
||||||
restart: unless-stopped
|
|
||||||
environment:
|
|
||||||
PUID: "{{ bookstack_puid }}"
|
|
||||||
PGID: "{{ bookstack_pgid }}"
|
|
||||||
TZ: "{{ bookstack_tz }}"
|
|
||||||
MYSQL_ROOT_PASSWORD: "{{ bookstack_db_root_password }}"
|
|
||||||
MYSQL_DATABASE: "{{ bookstack_db_name }}"
|
|
||||||
MYSQL_USER: "{{ bookstack_db_user }}"
|
|
||||||
MYSQL_PASSWORD: "{{ bookstack_db_password }}"
|
|
||||||
volumes:
|
|
||||||
- {{ bookstack_db_data_dir }}:/config
|
|
||||||
networks:
|
|
||||||
- internal
|
|
||||||
healthcheck:
|
|
||||||
test: ["CMD-SHELL", "mariadb-admin ping -h 127.0.0.1 -u root --password=\"$$MYSQL_ROOT_PASSWORD\" --silent"]
|
|
||||||
interval: 10s
|
|
||||||
timeout: 5s
|
|
||||||
retries: 12
|
|
||||||
start_period: 60s
|
|
||||||
|
|
||||||
networks:
|
|
||||||
{{ bookstack_traefik_network }}:
|
|
||||||
external: true
|
|
||||||
internal:
|
|
||||||
driver: bridge
|
|
||||||
|
|
@ -1 +0,0 @@
|
||||||
localhost
|
|
||||||
|
|
@ -1,5 +0,0 @@
|
||||||
---
|
|
||||||
- hosts: localhost
|
|
||||||
remote_user: root
|
|
||||||
roles:
|
|
||||||
- bookstack
|
|
||||||
|
|
@ -1,3 +0,0 @@
|
||||||
#SPDX-License-Identifier: MIT-0
|
|
||||||
---
|
|
||||||
# vars file for bookstack
|
|
||||||
|
|
@ -15,11 +15,7 @@ import sys
|
||||||
|
|
||||||
# Make the filter importable without having Ansible auto-discovery in
|
# Make the filter importable without having Ansible auto-discovery in
|
||||||
# the way (it would only run during a real `ansible-playbook` invocation).
|
# the way (it would only run during a real `ansible-playbook` invocation).
|
||||||
sys.path.insert(
|
sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..'))
|
||||||
0,
|
|
||||||
os.path.join(os.path.dirname(__file__), '..', '..', '..', '..',
|
|
||||||
'plugins', 'filter')
|
|
||||||
)
|
|
||||||
|
|
||||||
import pytest # noqa: E402
|
import pytest # noqa: E402
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -136,7 +136,7 @@
|
||||||
|
|
||||||
- name: Compute Homarr app layouts
|
- name: Compute Homarr app layouts
|
||||||
ansible.builtin.set_fact:
|
ansible.builtin.set_fact:
|
||||||
homarr_layout: "{{ homarr_apps | digitalboard.core.homarr_compute_layouts }}"
|
homarr_layout: "{{ homarr_apps | homarr_compute_layouts }}"
|
||||||
|
|
||||||
- name: Show computed app layouts
|
- name: Show computed app layouts
|
||||||
ansible.builtin.debug:
|
ansible.builtin.debug:
|
||||||
|
|
|
||||||
|
|
@ -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 }}"
|
|
||||||
```
|
|
||||||
|
|
@ -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
|
|
||||||
|
|
@ -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
|
|
||||||
|
|
@ -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
|
|
||||||
|
|
@ -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: []
|
|
||||||
|
|
@ -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 %}
|
|
||||||
|
|
@ -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
|
|
||||||
|
|
@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,2 +0,0 @@
|
||||||
#SPDX-License-Identifier: MIT-0
|
|
||||||
localhost
|
|
||||||
|
|
@ -1,6 +0,0 @@
|
||||||
#SPDX-License-Identifier: MIT-0
|
|
||||||
---
|
|
||||||
- hosts: localhost
|
|
||||||
remote_user: root
|
|
||||||
roles:
|
|
||||||
- opnform
|
|
||||||
|
|
@ -1,3 +0,0 @@
|
||||||
#SPDX-License-Identifier: MIT-0
|
|
||||||
---
|
|
||||||
# vars file for opnform
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue