From a44df81b71df0520176517cb2037d28e26bd9053 Mon Sep 17 00:00:00 2001 From: stephan Date: Fri, 8 May 2026 15:45:02 +0200 Subject: [PATCH] Split docs into apex + www containers, add nginx vhost overrides MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The acme-companion on the production host doesn't accept comma-separated VIRTUAL_HOST / LETSENCRYPT_HOST values, so cert issuance was failing for the combined `nibiru-framework.com,www.nibiru-framework.com` entry. docker-compose.yml — now defines two services sharing the same image: - docs → VIRTUAL_HOST=nibiru-framework.com (apex) - docs-www → VIRTUAL_HOST=www.nibiru-framework.com (built once, reused) A YAML anchor (x-docs-shared-env) keeps the Oracle/LLM/Anthropic config in lockstep so the two containers can never drift. docs/nginx/vhost.d/ — per-host nginx-proxy overrides applied at the location-block level by jwilder/nginx-proxy. Both files set: - X-Forwarded-* trust + buffering off (Oracle SSE streaming) - HSTS / X-Content-Type / X-Frame / Referrer-Policy / Permissions-Policy - gzip with the right MIME set for Astro/Starlight assets - Aggressive cache on /_astro/ (immutable hashed bundles) - 30-day cache on images/fonts - no-store on /sw.js (so PWA updates land) - 24-hour cache on /llms.txt for AI crawlers docs/nginx/README.md explains how to mount these into an existing nginx-proxy (bind-mount + reload, or bake into the proxy image). Co-Authored-By: Claude Opus 4.7 (1M context) --- docs/docker-compose.yml | 96 +++++++++++++------ docs/nginx/README.md | 60 ++++++++++++ .../vhost.d/nibiru-framework.com_location | 89 +++++++++++++++++ .../vhost.d/www.nibiru-framework.com_location | 83 ++++++++++++++++ 4 files changed, 297 insertions(+), 31 deletions(-) create mode 100644 docs/nginx/README.md create mode 100644 docs/nginx/vhost.d/nibiru-framework.com_location create mode 100644 docs/nginx/vhost.d/www.nibiru-framework.com_location diff --git a/docs/docker-compose.yml b/docs/docker-compose.yml index 9678ab7..f6affc0 100644 --- a/docs/docker-compose.yml +++ b/docs/docker-compose.yml @@ -1,8 +1,14 @@ # ============================================================================= # Nibiru docs site — production compose for jwilder/nginx-proxy # -# Default LLM backend: Ollama at https://api.neuronetz.ai (your own instance). -# No paid API keys required for normal operation. +# Two containers: one for the apex domain, one for the www. variant. +# jwilder/nginx-proxy + acme-companion don't accept comma-separated values +# on this host, so each hostname needs its own container with a single +# VIRTUAL_HOST + LETSENCRYPT_HOST. Both serve identical content from the +# same image. +# +# Default LLM backend: Ollama at https://api.neuronetz.ai (your own +# instance). No paid API keys required for normal operation. # # Prereqs (one-time, on the host): # docker network create nginx-proxy @@ -14,7 +20,39 @@ # git pull && docker compose up -d --build # ============================================================================= +# Shared environment block — referenced by both services via YAML anchors so +# the Oracle/LLM/Anthropic config stays in lockstep across the two domains. +x-docs-shared-env: &docs-shared-env + # --- Oracle: LLM provider (default = your own Ollama on neuronetz.ai) --- + LLM_PROVIDER: ${LLM_PROVIDER:-ollama} + OLLAMA_BASE_URL: ${OLLAMA_BASE_URL:-https://api.neuronetz.ai} + OLLAMA_CHAT_MODEL: ${OLLAMA_CHAT_MODEL:-qwen2.5-coder:14b} + OLLAMA_EMBED_MODEL: ${OLLAMA_EMBED_MODEL:-nomic-embed-text} + EMBED_PROVIDER: ${EMBED_PROVIDER:-ollama} + + # --- Optional fallbacks (only used if LLM_PROVIDER=anthropic / EMBED_PROVIDER=openai) --- + ANTHROPIC_API_KEY: ${ANTHROPIC_API_KEY:-} + ANTHROPIC_MODEL: ${ANTHROPIC_MODEL:-claude-haiku-4-5-20251001} + OPENAI_API_KEY: ${OPENAI_API_KEY:-} + OPENAI_EMBED_MODEL: ${OPENAI_EMBED_MODEL:-text-embedding-3-small} + + ORACLE_TOP_K: ${ORACLE_TOP_K:-6} + ORACLE_MAX_TOKENS: ${ORACLE_MAX_TOKENS:-800} + + HOST: 0.0.0.0 + PORT: "4321" + +# Healthcheck shared between both containers. +x-docs-healthcheck: &docs-healthcheck + test: ["CMD", "wget", "--quiet", "--spider", "http://127.0.0.1:4321/"] + interval: 30s + timeout: 5s + retries: 3 + start_period: 20s + services: + # --- Apex: nibiru-framework.com ------------------------------------------- + # Builds the image once. The www service below reuses the same tag. docs: build: context: . @@ -30,39 +68,35 @@ services: expose: - "4321" environment: - # --- nginx-proxy (jwilder) routing --- - VIRTUAL_HOST: nibiru-framework.com,www.nibiru-framework.com + <<: *docs-shared-env + VIRTUAL_HOST: nibiru-framework.com VIRTUAL_PORT: "4321" VIRTUAL_PROTO: "http" - LETSENCRYPT_HOST: nibiru-framework.com,www.nibiru-framework.com + LETSENCRYPT_HOST: nibiru-framework.com LETSENCRYPT_EMAIL: ${LETSENCRYPT_EMAIL:-stephan.kasdorf@bittomine.com} + healthcheck: *docs-healthcheck + networks: + - nginx-proxy - # --- Oracle: LLM provider (default = your own Ollama on neuronetz.ai) --- - LLM_PROVIDER: ${LLM_PROVIDER:-ollama} - OLLAMA_BASE_URL: ${OLLAMA_BASE_URL:-https://api.neuronetz.ai} - OLLAMA_CHAT_MODEL: ${OLLAMA_CHAT_MODEL:-qwen2.5-coder:14b} - OLLAMA_EMBED_MODEL: ${OLLAMA_EMBED_MODEL:-nomic-embed-text} - EMBED_PROVIDER: ${EMBED_PROVIDER:-ollama} - - # --- Optional fallbacks (only used if LLM_PROVIDER=anthropic / EMBED_PROVIDER=openai) --- - ANTHROPIC_API_KEY: ${ANTHROPIC_API_KEY:-} - ANTHROPIC_MODEL: ${ANTHROPIC_MODEL:-claude-haiku-4-5-20251001} - OPENAI_API_KEY: ${OPENAI_API_KEY:-} - OPENAI_EMBED_MODEL: ${OPENAI_EMBED_MODEL:-text-embedding-3-small} - - ORACLE_TOP_K: ${ORACLE_TOP_K:-6} - ORACLE_MAX_TOKENS: ${ORACLE_MAX_TOKENS:-800} - - HOST: 0.0.0.0 - PORT: "4321" - - healthcheck: - test: ["CMD", "wget", "--quiet", "--spider", "http://127.0.0.1:4321/"] - interval: 30s - timeout: 5s - retries: 3 - start_period: 20s - + # --- www. variant: www.nibiru-framework.com ------------------------------- + # Same image as `docs`. Started after `docs` so the build is reused (Docker + # would otherwise build it again from scratch). + docs-www: + image: nibiru-framework/docs:latest + container_name: nibiru-docs-www + restart: unless-stopped + depends_on: + - docs + expose: + - "4321" + environment: + <<: *docs-shared-env + VIRTUAL_HOST: www.nibiru-framework.com + VIRTUAL_PORT: "4321" + VIRTUAL_PROTO: "http" + LETSENCRYPT_HOST: www.nibiru-framework.com + LETSENCRYPT_EMAIL: ${LETSENCRYPT_EMAIL:-stephan.kasdorf@bittomine.com} + healthcheck: *docs-healthcheck networks: - nginx-proxy diff --git a/docs/nginx/README.md b/docs/nginx/README.md new file mode 100644 index 0000000..65d6dc4 --- /dev/null +++ b/docs/nginx/README.md @@ -0,0 +1,60 @@ +# nginx vhost overrides for nibiru-framework.com + +These files are picked up by **jwilder/nginx-proxy** when mounted into the +proxy container at `/etc/nginx/vhost.d/`. They hold per-vhost hardening +and cache rules for the docs site (apex + www). + +## What's here + +- `vhost.d/nibiru-framework.com_location` — apex domain rules +- `vhost.d/www.nibiru-framework.com_location` — www variant (identical rules) + +Both files contain the same hardening (security headers, gzip, caching for +hashed Astro assets, no-cache for the service worker, …). They are kept +separate so adding a www → apex redirect later is a one-file change. + +## Wiring into an existing nginx-proxy + +The proxy container needs to read `/etc/nginx/vhost.d/`. Two common patterns: + +### 1. Bind-mount a directory on the host + +If your nginx-proxy is started with something like: + +```yaml +volumes: + - /srv/nginx-proxy/vhost.d:/etc/nginx/vhost.d:ro +``` + +then copy these files into that directory on the host: + +```sh +sudo install -d /srv/nginx-proxy/vhost.d +sudo cp docs/nginx/vhost.d/* /srv/nginx-proxy/vhost.d/ +sudo docker exec nginx-proxy nginx -s reload +``` + +Repeat the `cp` + reload after every change. + +### 2. Bake them into the proxy image + +If you build your own nginx-proxy image, `COPY docs/nginx/vhost.d/* /etc/nginx/vhost.d/` +in its Dockerfile. Then `docker compose up -d --build` on the proxy. + +## Verifying + +Once mounted and reloaded: + +```sh +docker exec nginx-proxy nginx -T \ + | grep -A5 "server_name nibiru-framework.com" +``` + +You should see the proxy_buffering off, gzip, and security-header lines +from this directory inlined into the generated server block. + +## Why two containers (and not one with a comma-separated VIRTUAL_HOST) + +The acme-companion on this host does not handle comma-separated values in +`VIRTUAL_HOST` / `LETSENCRYPT_HOST` reliably — cert issuance fails. The fix +is to run one docs container per hostname (see `docker-compose.yml`). diff --git a/docs/nginx/vhost.d/nibiru-framework.com_location b/docs/nginx/vhost.d/nibiru-framework.com_location new file mode 100644 index 0000000..9def230 --- /dev/null +++ b/docs/nginx/vhost.d/nibiru-framework.com_location @@ -0,0 +1,89 @@ +# ============================================================================= +# nginx vhost-location override for nibiru-framework.com (apex) +# +# Picked up by jwilder/nginx-proxy when this file is mounted into the proxy +# container at /etc/nginx/vhost.d/_location. +# +# Pattern: +# -v /srv/nginx-proxy/vhost.d:/etc/nginx/vhost.d:ro +# on the proxy, then `cp docs/nginx/vhost.d/* /srv/nginx-proxy/vhost.d/` +# on the host (or symlink). +# +# Each rule is scoped to the location { } block of the generated server, +# so we DON'T re-declare server, listen, server_name, ssl_*, etc. +# ============================================================================= + +# Trust the X-Forwarded-* headers nginx-proxy already sets, so the upstream +# Astro server sees the real client IP and scheme. +proxy_set_header X-Real-IP $remote_addr; +proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; +proxy_set_header X-Forwarded-Host $host; +proxy_set_header X-Forwarded-Proto $scheme; + +# Allow streaming responses (Oracle answer streaming, Pagefind dialogs). +proxy_buffering off; +proxy_request_buffering off; +proxy_http_version 1.1; +proxy_read_timeout 300s; +proxy_send_timeout 300s; + +# WebSocket / SSE upgrade — harmless if the upstream never uses them. +proxy_set_header Upgrade $http_upgrade; +proxy_set_header Connection "upgrade"; + +# Body size — large enough for the LoRA corpus uploads (cap is generous; +# tighten if abuse becomes a concern). +client_max_body_size 25m; + +# Compression — Astro emits text-heavy assets that gzip well. +gzip on; +gzip_vary on; +gzip_min_length 512; +gzip_proxied any; +gzip_comp_level 6; +gzip_types + text/plain + text/css + text/xml + application/xml + application/json + application/javascript + application/manifest+json + image/svg+xml + font/ttf + font/otf + application/font-woff + application/font-woff2; + +# Security headers — same set sent on every request. +add_header Strict-Transport-Security "max-age=63072000; includeSubDomains; preload" always; +add_header X-Content-Type-Options "nosniff" always; +add_header X-Frame-Options "SAMEORIGIN" always; +add_header Referrer-Policy "strict-origin-when-cross-origin" always; +add_header Permissions-Policy "camera=(), microphone=(), geolocation=()" always; + +# Cache hashed Astro/Starlight assets aggressively (1 year, immutable). +# The dev server hashes filenames via Vite, so cache-busting is automatic. +location ~* ^/_astro/ { + expires 1y; + add_header Cache-Control "public, immutable, max-age=31536000"; +} + +# Static images / fonts / pwa manifest — long cache, mutable so a deploy +# can invalidate by changing the file. +location ~* \.(png|jpg|jpeg|gif|webp|avif|ico|svg|woff|woff2|ttf|otf)$ { + expires 30d; + add_header Cache-Control "public, max-age=2592000"; +} + +# Service worker MUST NOT be cached — needed for PWA updates to land. +location = /sw.js { + expires off; + add_header Cache-Control "no-store, no-cache, must-revalidate"; +} + +# llms.txt is small; let it cache for a day so AI crawlers don't hammer it. +location = /llms.txt { + expires 1d; + add_header Cache-Control "public, max-age=86400"; +} diff --git a/docs/nginx/vhost.d/www.nibiru-framework.com_location b/docs/nginx/vhost.d/www.nibiru-framework.com_location new file mode 100644 index 0000000..d84430d --- /dev/null +++ b/docs/nginx/vhost.d/www.nibiru-framework.com_location @@ -0,0 +1,83 @@ +# ============================================================================= +# nginx vhost-location override for www.nibiru-framework.com +# +# Same hardening + caching rules as the apex (nibiru-framework.com_location). +# Kept as a separate file so the proxy applies them per-vhost — the www and +# apex containers each get their own server block in the generated config. +# +# If you ever decide to redirect www → apex (SEO canonical-URL pattern) +# instead of serving from a second container, replace this file's contents +# with a single `return 301 https://nibiru-framework.com$request_uri;` and +# drop the docs-www service from docker-compose.yml. +# ============================================================================= + +# Trust the X-Forwarded-* headers nginx-proxy already sets. +proxy_set_header X-Real-IP $remote_addr; +proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; +proxy_set_header X-Forwarded-Host $host; +proxy_set_header X-Forwarded-Proto $scheme; + +# Allow streaming responses (Oracle answer streaming, Pagefind dialogs). +proxy_buffering off; +proxy_request_buffering off; +proxy_http_version 1.1; +proxy_read_timeout 300s; +proxy_send_timeout 300s; + +# WebSocket / SSE upgrade — harmless if the upstream never uses them. +proxy_set_header Upgrade $http_upgrade; +proxy_set_header Connection "upgrade"; + +# Body size — generous; tighten if abuse becomes a concern. +client_max_body_size 25m; + +# Compression. +gzip on; +gzip_vary on; +gzip_min_length 512; +gzip_proxied any; +gzip_comp_level 6; +gzip_types + text/plain + text/css + text/xml + application/xml + application/json + application/javascript + application/manifest+json + image/svg+xml + font/ttf + font/otf + application/font-woff + application/font-woff2; + +# Security headers. +add_header Strict-Transport-Security "max-age=63072000; includeSubDomains; preload" always; +add_header X-Content-Type-Options "nosniff" always; +add_header X-Frame-Options "SAMEORIGIN" always; +add_header Referrer-Policy "strict-origin-when-cross-origin" always; +add_header Permissions-Policy "camera=(), microphone=(), geolocation=()" always; + +# Hashed Astro/Starlight assets — 1 year, immutable. +location ~* ^/_astro/ { + expires 1y; + add_header Cache-Control "public, immutable, max-age=31536000"; +} + +# Static images / fonts — 30 days. +location ~* \.(png|jpg|jpeg|gif|webp|avif|ico|svg|woff|woff2|ttf|otf)$ { + expires 30d; + add_header Cache-Control "public, max-age=2592000"; +} + +# Service worker — never cache. +location = /sw.js { + expires off; + add_header Cache-Control "no-store, no-cache, must-revalidate"; +} + +# llms.txt — short cache for AI crawlers. +location = /llms.txt { + expires 1d; + add_header Cache-Control "public, max-age=86400"; +}