diff --git a/IMPLEMENTATION.md b/IMPLEMENTATION.md index 2dfa831..8055a8b 100644 --- a/IMPLEMENTATION.md +++ b/IMPLEMENTATION.md @@ -58,12 +58,13 @@ server-infra/ │ ├── hosts.yml │ ├── group_vars/ │ │ └── all.yml # vars + app_catalog -│ └── host_vars/ -│ ├── adama.yml # host_apps (django + static; monica worker) -│ ├── roslin.yml # host_apps (mirrors adama, no worker) -│ ├── starbuck.yml # host_apps (mirrors roslin) -│ ├── apollo.yml # host_apps (mirrors roslin) -│ └── ai-server-4080.yml # control node / act runner / SearxNG / observability +│ ├── host_vars/ +│ │ ├── adama.yml # host_apps (django + static; monica worker) +│ │ ├── roslin.yml # host_apps (mirrors adama, no worker) +│ │ ├── starbuck.yml # host_apps (mirrors roslin) +│ │ ├── apollo.yml # host_apps (mirrors roslin) +│ │ └── ai-server-4080.yml # control node / act runner / SearxNG / observability +│ └── wordpress_sites.yml # WordPress NFS mounts + generated compose inputs ├── playbooks/ │ ├── site.yml # Phase 1: provision │ ├── deploy-apps.yml # Phase 2: CI deploy @@ -79,7 +80,8 @@ server-infra/ │ ├── searxng/ # SearxNG JSON API for chat_backend (#10) │ ├── alloy/ # log/metrics shipper │ ├── app-deploy/ # django (docker) + node-static deploy -│ └── web-static/ # nginx container serving /var/www builds +│ ├── web-static/ # nginx container serving /var/www builds +│ └── wordpress/ # NFS wp-content + compose rendered per site └── scripts/ ├── provision.sh # Wrapper with --limit support ├── deploy.sh # Wrapper for deploy playbook @@ -197,7 +199,7 @@ Applies roles in order to the `webservers` group: | Role | Purpose | |------|---------| -| `common` | apt update, git, python3, pip, curl, ca-certificates | +| `common` | apt update, git, python3, pip, curl, ca-certificates, nfs-common | | `ufw` | Firewall: SSH from LAN only, HTTP/HTTPS public | | `docker` | Docker CE, compose plugin, add `westfarn` to `docker` group | | `nodejs` | Node.js + npm + npx (NodeSource) for `dta_webapp` builds | diff --git a/README.md b/README.md index 4fea127..ee27a44 100644 --- a/README.md +++ b/README.md @@ -43,6 +43,11 @@ See [IMPLEMENTATION.md](IMPLEMENTATION.md) for full architecture, CI/CD plan, an Observability (Alloy → Loki / Prometheus → Grafana): [docs/OBSERVABILITY.md](docs/OBSERVABILITY.md) · [docs/GRAFANA_USAGE.md](docs/GRAFANA_USAGE.md) +WordPress (MariaDB on TrueNAS): [docs/WORDPRESS.md](docs/WORDPRESS.md). +Provision mounts each site's `wp-content` and writes its compose file from +[inventory/wordpress_sites.yml](inventory/wordpress_sites.yml). Start the +containers by hand (adama first). + Provision installs **Alloy** on every host. On **ai-server-4080** it also starts the central **Loki / Prometheus / Grafana** stack (`observability_stack: true`). diff --git a/docs/WORDPRESS.md b/docs/WORDPRESS.md new file mode 100644 index 0000000..e305947 --- /dev/null +++ b/docs/WORDPRESS.md @@ -0,0 +1,392 @@ +# Adding a WordPress site + +Manual procedure for a WordPress site on all five web servers, one MariaDB on +TrueNAS, and Nginx Proxy Manager (NPM) in front. Ansible does **not** start +the WordPress containers. When you run `scripts/provision.sh` it mounts each +site's `wp-content` and, once that entry has a public `url`, writes its +compose file from +[inventory/wordpress_sites.yml](../inventory/wordpress_sites.yml). Repeat +[Per site](#per-site) for every new site. Do not install another MariaDB. + +## Where things live + +| Piece | Where | +|-------|--------| +| MariaDB | TrueNAS `10.0.0.230`, port **3306**, LAN only | +| MariaDB login | `westfarn` @ `%` (the host the TrueNAS app created) | +| NPM | `10.0.0.230` (not managed by Ansible) | +| App hosts | adama `10.0.0.77`, roslin `10.0.0.176`, starbuck `10.0.0.44`, apollo `10.0.0.7`, ai-server-4080 `10.0.0.128` | +| Secrets | `~/Documents/secrets/wordpress/` on the control node. Never commit these. | +| Shared files | one NFS dataset per site per env, mounted on every app host | + +Prod and beta never share a database, an NFS dataset, a port, or a domain. +The five hosts of one env do share that env's database and `wp-content`. + +## Already done + +Leave this alone when adding the next site. + +- MariaDB app is running. Its host-path dataset is owned by uid **999** (`netdata`). Without that, the container crash-loops with `Errcode: 13 "Permission denied"` on `/var/lib/mysql`. +- Login `westfarn` exists at host `%`, not at `10.0.0.%`. `GRANT` to `'westfarn'@'10.0.0.%'` fails with `ERROR 1133` because that row is not in `mysql.user`. +- Databases `relawfirm` and `relawfirm_beta` already exist. + +Confirm from **Apps → mariadb → Shell**: + +```bash +mariadb -u root -p +``` + +```sql +SELECT user, host FROM mysql.user WHERE user = 'westfarn'; +SHOW DATABASES; +SHOW GRANTS FOR 'westfarn'@'%'; +``` + +`relawfirm` still needs the grants below if `SHOW GRANTS` does not list both databases. New sites use the same `GRANT` shape with their own database names. + +```sql +GRANT ALL PRIVILEGES ON relawfirm.* TO 'westfarn'@'%'; +GRANT ALL PRIVILEGES ON relawfirm_beta.* TO 'westfarn'@'%'; +FLUSH PRIVILEGES; +``` + +## Ports + +Same host port on every app server for a given site and env. NPM balances +`adama:PORT` + `roslin:PORT` + `starbuck:PORT` + `apollo:PORT` + `ai-server-4080:PORT`. + +These are not in the Ansible `host_apps` tables. Do not reuse a port from +[IMPLEMENTATION.md](../IMPLEMENTATION.md) (8000–8020 and 8080–8088 are taken; +**8020** is `abc_voice`). + +| Site | prod port | beta port | prod database | beta database | +|------|-----------|-----------|---------------|---------------| +| relawfirm | **8021** | **8022** | `relawfirm` | `relawfirm_beta` | +| *(next site)* | next free | next free | `` | `_beta` | + +--- + +## Per site + +Fill this in before touching a host. `relawfirm` is the first site. + +| | prod | beta | +|--|------|------| +| Public URL | `https://` | `https://` | +| Database | `relawfirm` | `relawfirm_beta` | +| Host port | `8021` | `8022` | +| NFS dataset | `/mnt/AIMLOperationsNAS/wordpress/relawfirm` | `/mnt/AIMLOperationsNAS/wordpress/relawfirm_beta` | +| Mount on app hosts | `/mnt/wordpress/relawfirm/wp-content` | `/mnt/wordpress/relawfirm_beta/wp-content` | +| Env file | `relawfirm_prod.env` | `relawfirm_beta.env` | +| Compose project | `relawfirm_prod` | `relawfirm_beta` | + +`` below means that short name (`relawfirm`, then whatever the next site is). Use lowercase letters, numbers, and underscore only. + +### 1. Databases + +Skip this for `relawfirm` / `relawfirm_beta`. They already exist. + +In the MariaDB shell (`mariadb -u root -p`): + +```sql +CREATE DATABASE CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +CREATE DATABASE _beta CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +GRANT ALL PRIVILEGES ON .* TO 'westfarn'@'%'; +GRANT ALL PRIVILEGES ON _beta.* TO 'westfarn'@'%'; +FLUSH PRIVILEGES; +``` + +The account in `TO` must match `SELECT user, host FROM mysql.user`. Today that is `'westfarn'@'%'`. Do not create another MariaDB user unless you want a separate login; this matches the shared Postgres user. + +### 2. Salts and env files + +On the control node, generate salts **once per env**: + +```bash +curl -fsS https://api.wordpress.org/secret-key/1.1/salt/ +``` + +Copy the same eight values into the env file on every app host for that env. A different salt on one host logs people out as soon as NPM sends them to another host. Prod and beta should not share salts. + +Create the file locally (mode `600`). Do not commit it. + +`~/Documents/secrets/wordpress/_prod.env`: + +```bash +WORDPRESS_DB_HOST=10.0.0.230:3306 +WORDPRESS_DB_USER=westfarn +WORDPRESS_DB_PASSWORD= +WORDPRESS_DB_NAME= +WORDPRESS_AUTH_KEY= +WORDPRESS_SECURE_AUTH_KEY= +WORDPRESS_LOGGED_IN_KEY= +WORDPRESS_NONCE_KEY= +WORDPRESS_AUTH_SALT= +WORDPRESS_SECURE_AUTH_SALT= +WORDPRESS_LOGGED_IN_SALT= +WORDPRESS_NONCE_SALT= +``` + +`_beta.env` is the same except `WORDPRESS_DB_NAME=_beta` and a second set of salts. + +`WORDPRESS_DB_PASSWORD` is the MariaDB password. The WordPress admin password is chosen later in the browser, on the first host only. + +### 3. Shared `wp-content` (TrueNAS NFS) + +Uploads, plugins, and themes are files. With five hosts, those files have to live on one share. A private Docker volume on each host 404s media as soon as NPM moves the next request. + +Create **one dataset per site per env**. Preset **Generic** (this dataset is an NFS share, not the MariaDB host path). Leave record size at the default. Case sensitive. Do not add an SMB share. + +Example paths: + +```text +/mnt/AIMLOperationsNAS/wordpress/relawfirm +/mnt/AIMLOperationsNAS/wordpress/relawfirm_beta +``` + +**Shares → Unix Shares (NFS) → Add**, one share per dataset: + +| Setting | Value | +|---------|--------| +| Path | the dataset | +| Authorized Networks | `10.0.0.0/24` | +| Maproot User | `root` | +| Maproot Group | `root` | + +Maproot root lets the WordPress entrypoint create files on an empty share. The export is limited to the LAN. Do not forward NFS to the internet. + +Do not edit `/etc/fstab` by hand. Add one entry per env to +[inventory/wordpress_sites.yml](../inventory/wordpress_sites.yml), then mount +every host: + +```bash +./scripts/provision.sh +``` + +`export` is the TrueNAS dataset. `mount` is the path on the app hosts. Prod +and beta are separate entries (beta export ends in `_beta`, not `-beta`). + +```yaml +wordpress_sites: + - name: + port: 8021 + url: https:// + export: /mnt/AIMLOperationsNAS/wordpress/ + mount: /mnt/wordpress//wp-content + - name: _beta + port: 8022 + url: https:// + export: /mnt/AIMLOperationsNAS/wordpress/_beta + mount: /mnt/wordpress/_beta/wp-content +``` + +`url` is the public NPM URL. Leave it empty and provision still mounts NFS, +but it does not write a compose file. Set it, then provision again. + +One host is enough to check the new lines: + +```bash +./scripts/provision.sh adama +mount | grep wordpress +touch /mnt/wordpress//wp-content/.write-test && rm /mnt/wordpress//wp-content/.write-test +``` + +The first container to start copies the stock themes and plugins into this directory. Mount it on all five hosts **before** that start, and start only one host while the directory is still empty. Beta uses its own dataset and its own yaml entry. + +### 4. Compose file + +Do not copy a compose file by hand. The template is +[roles/wordpress/templates/docker-compose.yml.j2](../roles/wordpress/templates/docker-compose.yml.j2). +`scripts/provision.sh` renders one file per inventory entry on every app host. +Secrets stay in the env file. `WP_HOME` and `WP_SITEURL` come from that +entry's `url`, not from `http://10.0.0.77:8021`. + +| `name` | Compose file | Project (`name:` in the file) | Env file | +|--------|--------------|--------------------------------|----------| +| `` | `/opt/apps/wordpress//docker-compose.yml` | `_prod` | `/opt/apps/env/_prod.env` | +| `_beta` | `/opt/apps/wordpress/-beta/docker-compose.yml` | `_beta` | `/opt/apps/env/_beta.env` | + +Prod and beta do not share a directory. Image default is `wordpress:6-apache` +(`wordpress_image` in the role). Override with `image:` on one entry. The +bind mount is that entry's `mount`. + +Copy the env file onto each host (mode `600`) before the first start. +Provision creates `/opt/apps/env` (mode `700`) but does not copy secrets. + +### 5. Install in the browser, one host only + +Start **adama** first. Leave the other four stopped until the installer has finished. Five installers against one database will fight. + +On adama: + +```bash +cd /opt/apps/wordpress/ +docker compose -p _prod up -d +docker compose -p _prod logs -f +``` + +Wait until Apache is up. The log should not show a database connection error. + +In NPM, add a normal Proxy Host (single forward target) **before** opening the installer: + +| Field | Value | +|-------|--------| +| Domain | prod domain | +| Scheme | `http` | +| Forward Hostname | `10.0.0.77` | +| Forward Port | `8021` | +| SSL | request a certificate, force SSL | +| Websockets | off | + +Open `https://` and finish the WordPress installer (site title, admin user, admin password). That admin password is only in WordPress. It is not the MariaDB password. + +Then confirm a page refresh stays on `https://` and does not bounce to `http://10.0.0.77:8021`. + +### 6. Start the other four hosts + +Copy the same compose file and the same env file to roslin, starbuck, apollo, and ai-server-4080. NFS must already be mounted. Then, on each: + +```bash +cd /opt/apps/wordpress/ +docker compose -p _prod up -d +``` + +There is no second browser install. These containers attach to the database adama already populated. + +### 7. Load balance in NPM + +NPM's proxy host form accepts one forward address. The upstream list has to live in NPM's custom **http** config, which is included outside the server block. An `upstream` directive in the proxy host Advanced tab is rejected (`upstream directive is not allowed here`). + +On the NPM host (TrueNAS shell, or the NPM app shell), edit the file the container sees as `/data/nginx/custom/http.conf`. On a TrueNAS install that path is under the NPM app's data dataset, usually `.../npm/data/nginx/custom/http.conf`. Create `custom/` if it is missing. + +Append one block per site per env. Name the upstream after the compose project. + +```nginx +upstream relawfirm_prod { + least_conn; + server 10.0.0.77:8021 max_fails=3 fail_timeout=10s; + server 10.0.0.176:8021 max_fails=3 fail_timeout=10s; + server 10.0.0.44:8021 max_fails=3 fail_timeout=10s; + server 10.0.0.7:8021 max_fails=3 fail_timeout=10s; + server 10.0.0.128:8021 max_fails=3 fail_timeout=10s; +} +``` + +In the proxy host **Advanced** tab, set: + +```nginx +location ^~ / { + proxy_pass http://relawfirm_prod; + proxy_http_version 1.1; + proxy_set_header Host $host; + 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; + client_max_body_size 64m; +} +``` + +`location ^~ /` is what sends traffic at the upstream. The Details tab can keep `10.0.0.77:8021` as the saved forward target; the `^~` location is the one that matches. + +Reload and check the config from the NPM container: + +```bash +docker exec nginx -t +docker exec nginx -s reload +``` + +`nginx -t` must succeed. A duplicate-location error means this Advanced snippet was pasted twice. + +`client_max_body_size 64m` is the upload limit. Raise it in this block if the site needs larger media. A 413 from NPM is this limit, not MariaDB. + +Details tab: SSL on, Force SSL on, HTTP/2 on. Block Common Exploits can stay on. + +### 8. Cron, on adama only + +`DISABLE_WP_CRON` is set, so page views do not run scheduled jobs. One host runs them. Same idea as the dj-queue worker that stays on adama. + +On adama, `/etc/cron.d/wordpress-`: + +```cron +* * * * * westfarn curl -fsS https:///wp-cron.php?doing_wp_cron >/dev/null +``` + +Beta gets its own cron file hitting the beta URL. Do not add this cron on the other four hosts. + +### 9. Plugins, themes, and upgrades + +Plugins and themes installed from the dashboard are written to the NFS share, so every host sees them. Run one update at a time. + +WordPress core in this setup comes from the `wordpress:6-apache` image, not from the dashboard updater. The dashboard updater writes core into that container only. To move core, change `wordpress_image` (or that entry's `image`) and run `./scripts/provision.sh` so every host gets the new tag. Then, on each host: + +```bash +cd /opt/apps/wordpress/ +docker compose pull +docker compose up -d +``` + +The project name is already in the compose file, so `-p` is optional. The database and `wp-content` stay on MariaDB and NFS. + +--- + +## Beta + +Repeat sections 2–8 with the beta column of the table: + +- database `_beta` +- port **8022** for relawfirm (next free port for the next site) +- its own env file, salts, NFS dataset, compose project, NPM host, and adama cron + +Do not point beta `WORDPRESS_DB_NAME` at the prod database. + +--- + +## Next site + +MariaDB is already the shared server. For the next site, start at [Per site](#per-site) and do not redo [Already done](#already-done). + +1. Pick a short name, a prod domain, a beta domain, and the next two free host ports. Add a row to the [Ports](#ports) table in this file. +2. `CREATE DATABASE` / `GRANT` to `'westfarn'@'%'` for `` and `_beta`. +3. New env files and a new salt set per env. +4. New NFS datasets, then two entries in [inventory/wordpress_sites.yml](../inventory/wordpress_sites.yml) (`name`, `port`, `url`, `export`, `mount`). Run `./scripts/provision.sh` so every host mounts them and writes compose. Do not reuse another site's `wp-content`. +5. Start adama first, browser install once, then the other four. +6. New `upstream` block in `http.conf` and a new NPM proxy host. +7. New cron file on adama only. + +--- + +## Checks + +On each app host: + +```bash +docker compose -p _prod ps +curl -fsS -o /dev/null -w '%{http_code}\n' http://127.0.0.1:8021/ +mount | grep wordpress +``` + +From the control node, each backend should answer: + +```bash +for ip in 10.0.0.77 10.0.0.176 10.0.0.44 10.0.0.7 10.0.0.128; do + curl -fsS -o /dev/null -w "$ip %{http_code}\n" "http://$ip:8021/" +done +``` + +Then load `https://` several times. A media file uploaded while you are on the site should still load after a refresh (that request may have been served by a different host). + +## When something fails + +| Symptom | Cause | +|---------|--------| +| MariaDB `Errcode: 13` on `/var/lib/mysql` | Dataset not writable by uid 999. See [Already done](#already-done). | +| `ERROR 1133` on `GRANT` | User host does not match. Use the host from `SELECT user, host FROM mysql.user`. | +| `Can't create database; database exists` | Database was created by the TrueNAS wizard or a previous run. Continue at `GRANT`. | +| Container exits, cannot connect to MySQL | `WORDPRESS_DB_*` host, user, password, or database name. Port is `10.0.0.230:3306`. | +| Installer or login redirects to `http://10.0.0.x:8021` | `WP_HOME` / `WP_SITEURL` are not the public URL, or NPM is not sending `X-Forwarded-Proto`. | +| Logged out on refresh | Salts differ between hosts. Recreate with one shared env file. | +| Upload succeeds, image 404s on refresh | That host does not have the NFS mount, or the mount is not the one in the compose file. | +| `413 Request Entity Too Large` | `client_max_body_size` in the NPM `location ^~ /` block. | +| Scheduled posts never publish | Cron is missing on adama, or `wp-cron.php` is not reachable at the public URL. | +| Plugin shows on one host only | It was installed onto a local disk instead of the NFS `wp-content`. | diff --git a/inventory/group_vars/all.yml b/inventory/group_vars/all.yml index ab64c70..fa5db9b 100644 --- a/inventory/group_vars/all.yml +++ b/inventory/group_vars/all.yml @@ -14,9 +14,13 @@ ufw_allowed_tcp_ports: # LiveKit on webservers. Signaling stays LAN (NPM → :7880). UDP must be # reachable from the internet via router DNAT to the NPM upstream host. +# 8020 is abc_voice. That process uses host networking, so Docker does not +# publish the port and UFW drops it unless it is listed here. NPM +# (abc.voice.aimloperations.com) reaches it from the LAN. ufw_lan_tcp_ports: - 7880 - 7881 + - 8020 ufw_extra_udp_ports: - 3478 - "50000:60000" diff --git a/inventory/wordpress_sites.yml b/inventory/wordpress_sites.yml new file mode 100644 index 0000000..b6c24cd --- /dev/null +++ b/inventory/wordpress_sites.yml @@ -0,0 +1,32 @@ +--- +# WordPress sites on every webserver. scripts/provision.sh mounts wp-content +# and writes one compose file per entry. Do not hand-edit /etc/fstab or the +# generated compose file on the hosts. +# +# name: site key. Prod is the short name (relawfirm). Beta is that name +# plus _beta (relawfirm_beta). Names use lowercase, numbers, underscore. +# export: dataset path on the TrueNAS NFS server +# mount: local path compose bind-mounts to /var/www/html/wp-content +# port: host port published to container port 80 +# url: public https URL (WP_HOME and WP_SITEURL). Empty skips compose. +# +# From name, provision derives: +# relawfirm -> /opt/apps/wordpress/relawfirm/docker-compose.yml +# project relawfirm_prod, env /opt/apps/env/relawfirm_prod.env +# relawfirm_beta -> /opt/apps/wordpress/relawfirm-beta/docker-compose.yml +# project relawfirm_beta, env /opt/apps/env/relawfirm_beta.env + +wordpress_nfs_server: 10.0.0.230 +wordpress_nfs_opts: "nfsvers=4,tcp,_netdev" + +wordpress_sites: + - name: relawfirm + port: 8021 + url: "" + export: /mnt/AIMLOperationsNAS/wordpress/relawfirm + mount: /mnt/wordpress/relawfirm/wp-content + - name: relawfirm_beta + port: 8022 + url: "" + export: /mnt/AIMLOperationsNAS/wordpress/relawfirm_beta + mount: /mnt/wordpress/relawfirm_beta/wp-content diff --git a/playbooks/site.yml b/playbooks/site.yml index 52a7620..d6e8c87 100644 --- a/playbooks/site.yml +++ b/playbooks/site.yml @@ -13,6 +13,9 @@ - nodejs - gitea-key - tianji + # NFS wp-content and generated compose for inventory/wordpress_sites.yml. + - role: wordpress + tags: [wordpress] - name: Provision central observability stack hosts: ai-server-4080 diff --git a/requirements.yml b/requirements.yml index 3c194cf..3834074 100644 --- a/requirements.yml +++ b/requirements.yml @@ -2,3 +2,5 @@ collections: - name: community.general version: ">=8.0.0" + - name: ansible.posix + version: ">=1.5.0" diff --git a/roles/common/tasks/main.yml b/roles/common/tasks/main.yml index 01eab35..244a567 100644 --- a/roles/common/tasks/main.yml +++ b/roles/common/tasks/main.yml @@ -23,4 +23,5 @@ - gnupg - apt-transport-https - software-properties-common + - nfs-common state: present diff --git a/roles/wordpress/defaults/main.yml b/roles/wordpress/defaults/main.yml new file mode 100644 index 0000000..c7f6809 --- /dev/null +++ b/roles/wordpress/defaults/main.yml @@ -0,0 +1,3 @@ +--- +wordpress_image: "wordpress:6-apache" +wordpress_root: "{{ apps_base_dir }}/wordpress" diff --git a/roles/wordpress/tasks/main.yml b/roles/wordpress/tasks/main.yml new file mode 100644 index 0000000..d93a3d8 --- /dev/null +++ b/roles/wordpress/tasks/main.yml @@ -0,0 +1,77 @@ +--- +# Mount each site's shared wp-content and write its compose file. +# List lives in inventory/wordpress_sites.yml. Containers are not started +# here: the first browser install must run on one host only. + +- name: Load WordPress site list + ansible.builtin.include_vars: + file: "{{ inventory_dir }}/wordpress_sites.yml" + +- name: Install NFS client + ansible.builtin.apt: + name: nfs-common + state: present + +- name: Create WordPress mount points + ansible.builtin.file: + path: "{{ item.mount }}" + state: directory + mode: "0755" + loop: "{{ wordpress_sites }}" + loop_control: + label: "{{ item.name }}" + +- name: Mount WordPress wp-content + ansible.posix.mount: + src: "{{ wordpress_nfs_server }}:{{ item.export }}" + path: "{{ item.mount }}" + fstype: nfs + opts: "{{ wordpress_nfs_opts }}" + dump: "0" + passno: "0" + state: mounted + loop: "{{ wordpress_sites }}" + loop_control: + label: "{{ item.name }}" + +- name: Ensure WordPress env directory + ansible.builtin.file: + path: "{{ apps_env_dir }}" + state: directory + owner: "{{ admin_user }}" + group: "{{ admin_user }}" + mode: "0700" + +- name: Ensure WordPress compose directories + ansible.builtin.file: + path: "{{ wordpress_root }}/{{ item.name | regex_replace('_beta$', '-beta') }}" + state: directory + owner: "{{ admin_user }}" + group: "{{ admin_user }}" + mode: "0755" + loop: "{{ wordpress_sites }}" + when: (item.url | default('') | length) > 0 + loop_control: + label: "{{ item.name }}" + +- name: Note WordPress sites waiting on a public URL + ansible.builtin.debug: + msg: >- + WordPress {{ item.name }} has no url. NFS is mounted. + Compose is not written until url is set in inventory/wordpress_sites.yml. + loop: "{{ wordpress_sites }}" + when: (item.url | default('') | length) == 0 + loop_control: + label: "{{ item.name }}" + +- name: Write WordPress compose file + ansible.builtin.template: + src: docker-compose.yml.j2 + dest: "{{ wordpress_root }}/{{ item.name | regex_replace('_beta$', '-beta') }}/docker-compose.yml" + owner: "{{ admin_user }}" + group: "{{ admin_user }}" + mode: "0644" + loop: "{{ wordpress_sites }}" + when: (item.url | default('') | length) > 0 + loop_control: + label: "{{ item.name }}" diff --git a/roles/wordpress/templates/docker-compose.yml.j2 b/roles/wordpress/templates/docker-compose.yml.j2 new file mode 100644 index 0000000..0b6c607 --- /dev/null +++ b/roles/wordpress/templates/docker-compose.yml.j2 @@ -0,0 +1,21 @@ +# Managed by Ansible (roles/wordpress). Do not edit by hand. +name: {{ item.name if (item.name is match('.*_beta$')) else (item.name ~ '_prod') }} +services: + wordpress: + image: {{ item.image | default(wordpress_image) }} + restart: unless-stopped + env_file: + - {{ apps_env_dir }}/{{ item.name if (item.name is match('.*_beta$')) else (item.name ~ '_prod') }}.env + ports: + - "{{ item.port }}:80" + environment: + WORDPRESS_CONFIG_EXTRA: | + define('WP_HOME', '{{ item.url }}'); + define('WP_SITEURL', '{{ item.url }}'); + define('DISABLE_WP_CRON', true); + if (isset($_SERVER['HTTP_X_FORWARDED_PROTO']) && strpos($_SERVER['HTTP_X_FORWARDED_PROTO'], 'https') !== false) { + $_SERVER['HTTPS'] = 'on'; + } + define('FORCE_SSL_ADMIN', true); + volumes: + - {{ item.mount }}:/var/www/html/wp-content diff --git a/scripts/provision.sh b/scripts/provision.sh index 69adef6..1e68753 100755 --- a/scripts/provision.sh +++ b/scripts/provision.sh @@ -14,6 +14,11 @@ Usage: $(basename "$0") [HOST] [OPTIONS] Provision server(s) with site.yml. Applies: common, ufw, docker, nodejs, gitea-key, tianji, alloy (every host). +common installs nfs-common with the other base packages. +Also mounts WordPress wp-content (NFS) and, when that entry has a public +url, writes its compose file. Site list is inventory/wordpress_sites.yml. +Add a site there; the next provision mounts it on every webserver. +Containers are not started. On ai-server-4080 also: observability (Loki + Prometheus + Grafana) when observability_stack is true, and SearxNG when searxng_stack is true (chat_backend grounded search on :8088).