Register dta_blog for node-static deploy (closes #29) (#30)
Sync runner checkout / sync (push) Successful in 7s

## Summary
- Closes [#29](#29).
- Register `dta_blog` in `app_catalog` (`type: node-static`, repo `Ditch_The_Agent/dta_blog`, default branch `main`, webroot `/var/www/{env}.blog.realpath.app/html`). Reuses `roles/app-deploy/tasks/node_static.yml` (`npm ci` then `npm run build:<env>`); the blog's `package.json` copies `dist/` to that webroot after `python3 build.py --env <env>`.
- Add prod **8086** / beta **8087** `host_apps` on adama, roslin, starbuck, apollo, and ai-server-4080 (those ports were free vs 8080–8085 / 8088).
- Optional nginx `error_page 404 /404.html` for this app only (`error_page_404` catalog field); other static apps keep the SPA `try_files` fallback.
- Document NPM/DNS (`blog.realpath.app` → `:8086`, `beta.blog.realpath.app` → `:8087`), no `realpath.app/blog` mount, and UFW staying LAN/NPM-only.

## Test plan
- [ ] Confirm 8086/8087 unused on app hosts before first deploy.
- [ ] `./scripts/deploy.sh --app dta_blog --env beta --ref main` publishes `/var/www/beta.blog.realpath.app/html`.
- [ ] `./scripts/deploy.sh --app dta_blog --env prod --ref main` publishes `/var/www/prod.blog.realpath.app/html`.
- [ ] **Until [dta_blog#1](Ditch_The_Agent/dta_blog#1) is on `main`**, use `--ref issue-1-static-blog` so the SSG (not the stub README) is built.
- [ ] NPM + DNS + TLS: `blog.realpath.app` → `:8086`, `beta.blog.realpath.app` → `:8087`. Do not reverse-proxy onto `realpath.app/blog`.
- [ ] Prod HTML has article text, Tianji id `cmtvvmf562afjzqumwt1yh2y8`, links to `https://realpath.app/`.
- [ ] Beta HTML has Tianji id `cmtvvn6z62agdzqumtk8xijpy`, links to `https://beta.realpath.app/`, demo posts present.
- [ ] `https://blog.realpath.app/sitemap.xml`, `robots.txt`, `llms.txt` return 200.
- [ ] Unknown slug returns 404.html (not the index SPA fallback).

Reviewed-on: #30
This commit was merged in pull request #30.
This commit is contained in:
2026-09-10 18:34:25 -07:00
parent 2abcdd7c58
commit 1988def1a6
9 changed files with 56 additions and 4 deletions
+28 -3
View File
@@ -211,6 +211,7 @@ After Docker install, re-SSH so the `docker` group membership takes effect.
| `company_site` | django (docker) | all webservers | prod | active/active behind NPM; beta port reserved | | `company_site` | django (docker) | all webservers | prod | active/active behind NPM; beta port reserved |
| `dta_service` | django (docker) | all webservers | beta + prod | active/active behind NPM | | `dta_service` | django (docker) | all webservers | beta + prod | active/active behind NPM |
| `dta_webapp` | node/vite static | all webservers | beta + prod | active/active; built to `/var/www/<env>.realpath.app/html`, served by web-static nginx | | `dta_webapp` | node/vite static | all webservers | beta + prod | active/active; built to `/var/www/<env>.realpath.app/html`, served by web-static nginx |
| `dta_blog` | node-static (Python SSG) | all webservers | beta + prod | active/active; built to `/var/www/<env>.blog.realpath.app/html`; **own hosts** (`blog.realpath.app` / `beta.blog.realpath.app`), not `realpath.app/blog`; Tianji ids baked at build |
| `scha` | django (docker) | all webservers | prod | active/active behind NPM; beta port reserved | | `scha` | django (docker) | all webservers | prod | active/active behind NPM; beta port reserved |
| `chat_web_app` | node-static (CRA) | all webservers | beta + prod | active/active; built to `/var/www/<env>.chat.aimloperations/html`, served by web-static nginx | | `chat_web_app` | node-static (CRA) | all webservers | beta + prod | active/active; built to `/var/www/<env>.chat.aimloperations/html`, served by web-static nginx |
| `chat_backend` | django (docker) | all webservers | beta + prod | active/active behind NPM; Ollama `http://10.0.0.128:11434`; SearxNG `http://10.0.0.128:8088` (`SEARXNG_BASE_URL`) | | `chat_backend` | django (docker) | all webservers | beta + prod | active/active behind NPM; Ollama `http://10.0.0.128:11434`; SearxNG `http://10.0.0.128:8088` (`SEARXNG_BASE_URL`) |
@@ -256,6 +257,7 @@ future beta replica.
| dta_webapp (nginx) | 8081 | 8080 | all webservers | | dta_webapp (nginx) | 8081 | 8080 | all webservers |
| chat_web_app (nginx) | 8083 | 8082 | all webservers | | chat_web_app (nginx) | 8083 | 8082 | all webservers |
| abc_fe (nginx) | **8085** | 8084 (*not deployed*) | all webservers | | abc_fe (nginx) | **8085** | 8084 (*not deployed*) | all webservers |
| dta_blog (nginx) | **8087** | **8086** | all webservers |
| SearxNG (LAN only) | — | **8088** | ai-server-4080 only (`searxng_stack`); not an NPM upstream | | SearxNG (LAN only) | — | **8088** | ai-server-4080 only (`searxng_stack`); not an NPM upstream |
Host-local services on ai-server-4080 (not balanced by NPM): Host-local services on ai-server-4080 (not balanced by NPM):
@@ -293,8 +295,10 @@ chat_backend secrets must use `SEARXNG_BASE_URL=http://10.0.0.128:8088`.
`compose_profiles: [worker]` so deploy starts dj-queue with web. Other hosts `compose_profiles: [worker]` so deploy starts dj-queue with web. Other hosts
omit profiles (web only). omit profiles (web only).
- **node-static**: git checkout at ref → `npm ci` → `npm run build:<env>` - **node-static**: git checkout at ref → `npm ci` → `npm run build:<env>`
(writes to the app's `webroot_pattern`, e.g. `/var/www/{env}.realpath.app/html` (writes to the app's `webroot_pattern`, e.g. `/var/www/{env}.realpath.app/html`,
or `/var/www/{env}.chat.aimloperations/html`). `/var/www/{env}.blog.realpath.app/html`, or `/var/www/{env}.chat.aimloperations/html`).
Optional catalog `error_page_404` (used by `dta_blog`) serves that file instead
of the SPA `/index.html` fallback.
- **web-static** role: one nginx container per app host serving the static roots - **web-static** role: one nginx container per app host serving the static roots
on their ports (from `host_apps`); NPM balances across hosts. Before `compose up`, on their ports (from `host_apps`); NPM balances across hosts. Before `compose up`,
removes any container currently publishing those host ports (`docker ps --filter removes any container currently publishing those host ports (`docker ps --filter
@@ -315,6 +319,7 @@ point each domain at the backend(s):
| company_site | aimloperations.com (+ www) | `adama:8000` + `roslin:8000` + `starbuck:8000` + `apollo:8000` | | company_site | aimloperations.com (+ www) | `adama:8000` + `roslin:8000` + `starbuck:8000` + `apollo:8000` |
| dta_service | (see DTA NPM hosts) | `adama:8001` / `8011` + same on roslin / starbuck / apollo / ai-server-4080 | | dta_service | (see DTA NPM hosts) | `adama:8001` / `8011` + same on roslin / starbuck / apollo / ai-server-4080 |
| dta_webapp | (see DTA NPM hosts) | `adama:8080` / `8081` + same on roslin / starbuck / apollo | | dta_webapp | (see DTA NPM hosts) | `adama:8080` / `8081` + same on roslin / starbuck / apollo |
| dta_blog | `blog.realpath.app` (prod); `beta.blog.realpath.app` (beta). **Do not** reverse-proxy onto `realpath.app/blog` | `adama:8086` / `8087` + same on roslin / starbuck / apollo / ai-server-4080 |
| scha | `schawheaton.aimloperations.com`, `schawheaton.com` (+ www) | `adama:8002` + `roslin:8002` + `starbuck:8002` + `apollo:8002` (+ `ai-server-4080:8002`) | | scha | `schawheaton.aimloperations.com`, `schawheaton.com` (+ www) | `adama:8002` + `roslin:8002` + `starbuck:8002` + `apollo:8002` (+ `ai-server-4080:8002`) |
| chat_web_app | `chat.aimloperations.com` (+ www); `beta.chat.aimloperations.com` | `adama:8082` / `8083` + same on roslin / starbuck / apollo / ai-server-4080 | | chat_web_app | `chat.aimloperations.com` (+ www); `beta.chat.aimloperations.com` | `adama:8082` / `8083` + same on roslin / starbuck / apollo / ai-server-4080 |
| chat_backend | `chatbackend.aimloperations.com`; `beta.chatbackend.aimloperations.com` | `adama:8003` / `8013` + same on roslin / starbuck / apollo / ai-server-4080 | | chat_backend | `chatbackend.aimloperations.com`; `beta.chatbackend.aimloperations.com` | `adama:8003` / `8013` + same on roslin / starbuck / apollo / ai-server-4080 |
@@ -337,6 +342,9 @@ point each domain at the backend(s):
(keep the test/docker jobs). (keep the test/docker jobs).
- [ ] `dta_webapp`: `npm run build:beta` / `build:prod` output to - [ ] `dta_webapp`: `npm run build:beta` / `build:prod` output to
`/var/www/beta.realpath.app/html` / `/var/www/prod.realpath.app/html`. `/var/www/beta.realpath.app/html` / `/var/www/prod.realpath.app/html`.
- [ ] `dta_blog`: `npm run build:beta` / `build:prod` output to
`/var/www/beta.blog.realpath.app/html` / `/var/www/prod.blog.realpath.app/html`
(`python3 build.py --env <env>` copies `dist/`). Default branch is `main`.
- [ ] `chat_web_app`: `npm run build:beta` / `build:prod` output to - [ ] `chat_web_app`: `npm run build:beta` / `build:prod` output to
`/var/www/beta.chat.aimloperations/html` / `/var/www/prod.chat.aimloperations/html`. `/var/www/beta.chat.aimloperations/html` / `/var/www/prod.chat.aimloperations/html`.
@@ -432,6 +440,22 @@ Companion workflows: [abc_be#22](https://git.aimloperations.com/GIS/abc_be/issue
[abc_worker#27](https://git.aimloperations.com/GIS/abc_worker/issues/27), [abc_worker#27](https://git.aimloperations.com/GIS/abc_worker/issues/27),
[abc_fe#27](https://git.aimloperations.com/GIS/abc_fe/issues/27). [abc_fe#27](https://git.aimloperations.com/GIS/abc_fe/issues/27).
`dta_blog` ([#29](https://git.aimloperations.com/ai_ml_operations/server-infra/issues/29)) —
Python SSG, **no secrets file**. `npm run build:<env>` writes
`/var/www/<env>.blog.realpath.app/html`. Prod (`:8086`) omits `demo: true` seed
posts; beta (`:8087`) includes them. Header/footer links are baked at build
(`https://realpath.app` vs `https://beta.realpath.app`); Tianji website ids
are likewise baked (`cmtvvmf562afjzqumwt1yh2y8` / `cmtvvn6z62agdzqumtk8xijpy`).
Default branch is `main`.
NPM / DNS / Cloudflare (Ansible does not manage these): `blog.realpath.app` →
`:8086`, `beta.blog.realpath.app` → `:8087`, TLS like the other RealPath names.
Do **not** mount this on `realpath.app/blog`. Static ports stay LAN / NPM-only
(UFW does not world-open 8086/8087).
Companion app ticket: [dta_blog#1](https://git.aimloperations.com/Ditch_The_Agent/dta_blog/issues/1).
Gitea deploy can later call `./scripts/deploy.sh --app dta_blog --env beta|prod`.
### One-time host bootstrap (per target) ### One-time host bootstrap (per target)
- [x] Gitea SSH key: the `gitea-key` role (in `site.yml`) generates a key per - [x] Gitea SSH key: the `gitea-key` role (in `site.yml`) generates a key per
@@ -510,7 +534,8 @@ Store vault password for CI in a file readable only by the Act runner (e.g. `~/.
| 10g | Register + deploy `url_shortening_service` (django, ports 8005/8015) | Done ([#22](https://git.aimloperations.com/ai_ml_operations/server-infra/issues/22)) | | 10g | Register + deploy `url_shortening_service` (django, ports 8005/8015) | Done ([#22](https://git.aimloperations.com/ai_ml_operations/server-infra/issues/22)) |
| 10h | Register + deploy `college_craft` (django, ports 8006/8016) | Done ([#24](https://git.aimloperations.com/ai_ml_operations/server-infra/issues/24)) | | 10h | Register + deploy `college_craft` (django, ports 8006/8016) | Done ([#24](https://git.aimloperations.com/ai_ml_operations/server-infra/issues/24)) |
| 10i | Register + deploy ABC beta (`abc_be` 8017, `abc_worker` 8018, `abc_fe` 8085) | Done ([#26](https://git.aimloperations.com/ai_ml_operations/server-infra/issues/26)) | | 10i | Register + deploy ABC beta (`abc_be` 8017, `abc_worker` 8018, `abc_fe` 8085) | Done ([#26](https://git.aimloperations.com/ai_ml_operations/server-infra/issues/26)) |
| 10j | Register + deploy `print_forge` (django, ports 8007/8019) | This PR ([#27](https://git.aimloperations.com/ai_ml_operations/server-infra/issues/27)) | | 10j | Register + deploy `print_forge` (django, ports 8007/8019) | Done ([#27](https://git.aimloperations.com/ai_ml_operations/server-infra/issues/27)) |
| 10k | Register + deploy `dta_blog` (node-static, ports 8086/8087) | This PR ([#29](https://git.aimloperations.com/ai_ml_operations/server-infra/issues/29)) |
| 11 | Gitea container registry (optional) | Future | | 11 | Gitea container registry (optional) | Future |
## Open Decisions ## Open Decisions
+9
View File
@@ -80,6 +80,15 @@ app_catalog:
webroot_pattern: "/var/www/{env}.realpath.app/html" webroot_pattern: "/var/www/{env}.realpath.app/html"
# deploy runs `npm ci` then `npm run build:<env>`; that script writes to # deploy runs `npm ci` then `npm run build:<env>`; that script writes to
# {{ web_static_root }}/<env>_dta_webapp (beta/prod), served by web-static. # {{ web_static_root }}/<env>_dta_webapp (beta/prod), served by web-static.
dta_blog:
type: node-static
repo: "{{ git_base_url }}/Ditch_The_Agent/dta_blog.git"
default_branch: main
# Python SSG (stdlib). package.json build:<env> copies dist/ to this webroot
# so the existing node-static path (`npm ci` then `npm run build:<env>`) works.
webroot_pattern: "/var/www/{env}.blog.realpath.app/html"
# Real 404 page (not SPA index fallback). See roles/web-static nginx.conf.j2.
error_page_404: /404.html
scha: scha:
type: django type: django
repo: "{{ git_base_url }}/ai_ml_operations/scha.git" repo: "{{ git_base_url }}/ai_ml_operations/scha.git"
+2
View File
@@ -11,6 +11,8 @@ host_apps:
- { name: dta_service, env: beta, port: 8011 } - { name: dta_service, env: beta, port: 8011 }
- { name: dta_webapp, env: prod, port: 8080 } - { name: dta_webapp, env: prod, port: 8080 }
- { name: dta_webapp, env: beta, port: 8081 } - { name: dta_webapp, env: beta, port: 8081 }
- { name: dta_blog, env: prod, port: 8086 }
- { name: dta_blog, env: beta, port: 8087 }
- { name: scha, env: prod, port: 8002 } - { name: scha, env: prod, port: 8002 }
# optional: - { name: scha, env: beta, port: 8012 } # optional: - { name: scha, env: beta, port: 8012 }
- { name: chat_web_app, env: prod, port: 8082 } - { name: chat_web_app, env: prod, port: 8082 }
+2
View File
@@ -27,6 +27,8 @@ host_apps:
- { name: company_site, env: prod, port: 8000 } - { name: company_site, env: prod, port: 8000 }
- { name: dta_webapp, env: prod, port: 8080 } - { name: dta_webapp, env: prod, port: 8080 }
- { name: dta_webapp, env: beta, port: 8081 } - { name: dta_webapp, env: beta, port: 8081 }
- { name: dta_blog, env: prod, port: 8086 }
- { name: dta_blog, env: beta, port: 8087 }
- { name: dta_service, env: prod, port: 8001 } - { name: dta_service, env: prod, port: 8001 }
- { name: dta_service, env: beta, port: 8011 } - { name: dta_service, env: beta, port: 8011 }
- { name: scha, env: prod, port: 8002 } - { name: scha, env: prod, port: 8002 }
+2
View File
@@ -7,6 +7,8 @@ host_apps:
- { name: dta_service, env: beta, port: 8011 } - { name: dta_service, env: beta, port: 8011 }
- { name: dta_webapp, env: prod, port: 8080 } - { name: dta_webapp, env: prod, port: 8080 }
- { name: dta_webapp, env: beta, port: 8081 } - { name: dta_webapp, env: beta, port: 8081 }
- { name: dta_blog, env: prod, port: 8086 }
- { name: dta_blog, env: beta, port: 8087 }
- { name: scha, env: prod, port: 8002 } - { name: scha, env: prod, port: 8002 }
# optional: - { name: scha, env: beta, port: 8012 } # optional: - { name: scha, env: beta, port: 8012 }
- { name: chat_web_app, env: prod, port: 8082 } - { name: chat_web_app, env: prod, port: 8082 }
+2
View File
@@ -7,6 +7,8 @@ host_apps:
- { name: dta_service, env: beta, port: 8011 } - { name: dta_service, env: beta, port: 8011 }
- { name: dta_webapp, env: prod, port: 8080 } - { name: dta_webapp, env: prod, port: 8080 }
- { name: dta_webapp, env: beta, port: 8081 } - { name: dta_webapp, env: beta, port: 8081 }
- { name: dta_blog, env: prod, port: 8086 }
- { name: dta_blog, env: beta, port: 8087 }
- { name: scha, env: prod, port: 8002 } - { name: scha, env: prod, port: 8002 }
# optional: - { name: scha, env: beta, port: 8012 } # optional: - { name: scha, env: beta, port: 8012 }
- { name: chat_web_app, env: prod, port: 8082 } - { name: chat_web_app, env: prod, port: 8082 }
+2
View File
@@ -7,6 +7,8 @@ host_apps:
- { name: dta_service, env: beta, port: 8011 } - { name: dta_service, env: beta, port: 8011 }
- { name: dta_webapp, env: prod, port: 8080 } - { name: dta_webapp, env: prod, port: 8080 }
- { name: dta_webapp, env: beta, port: 8081 } - { name: dta_webapp, env: beta, port: 8081 }
- { name: dta_blog, env: prod, port: 8086 }
- { name: dta_blog, env: beta, port: 8087 }
- { name: scha, env: prod, port: 8002 } - { name: scha, env: prod, port: 8002 }
# optional: - { name: scha, env: beta, port: 8012 } # optional: - { name: scha, env: beta, port: 8012 }
- { name: chat_web_app, env: prod, port: 8082 } - { name: chat_web_app, env: prod, port: 8082 }
+8
View File
@@ -30,9 +30,17 @@ server {
root {{ (app_catalog[a.name].webroot_pattern | default(web_static_root ~ '/{env}_' ~ a.name)) | replace('{env}', a.env) }}; root {{ (app_catalog[a.name].webroot_pattern | default(web_static_root ~ '/{env}_' ~ a.name)) | replace('{env}', a.env) }};
index index.html; index index.html;
{% if app_catalog[a.name].error_page_404 | default('') %}
error_page 404 {{ app_catalog[a.name].error_page_404 }};
location / {
try_files $uri $uri/ =404;
}
{% else %}
location / { location / {
try_files $uri $uri/ /index.html; try_files $uri $uri/ /index.html;
} }
{% endif %}
location ~* \.(?:js|css|woff2?|png|jpg|jpeg|gif|svg|ico)$ { location ~* \.(?:js|css|woff2?|png|jpg|jpeg|gif|svg|ico)$ {
expires 7d; expires 7d;
+1 -1
View File
@@ -17,7 +17,7 @@ Deploy applications with deploy-apps.yml.
HOST Optional. Limit to one host: adama, roslin, starbuck, apollo, or ai-server-4080. HOST Optional. Limit to one host: adama, roslin, starbuck, apollo, or ai-server-4080.
Options: Options:
--app NAME App to deploy (company_site, dta_service, dta_webapp, scha, chat_web_app, chat_backend, monica_site, url_shortening_service, college_craft, print_forge, abc_be, abc_worker, abc_fe) --app NAME App to deploy (company_site, dta_service, dta_webapp, dta_blog, scha, chat_web_app, chat_backend, monica_site, url_shortening_service, college_craft, print_forge, abc_be, abc_worker, abc_fe)
--env ENV Environment: beta or prod --env ENV Environment: beta or prod
--ref REF Git ref/sha to deploy (default: master) --ref REF Git ref/sha to deploy (default: master)
--check Dry run --check Dry run