# Website Runtime The portfolio website keeps Apache at the core while moving PHP execution to PHP-FPM. ## Runtime Shape - Base image: `php:8.5-fpm-alpine` - Web server: Alpine Apache, listening on container port `8080` - PHP runtime: PHP-FPM on `127.0.0.1:9000` - PHP cache: OPcache enabled, with timestamp validation disabled for immutable image deploys - Dynamic storage: `/var/www/localhost/htdocs/db` mounted from the retained `website-db` PVC Apache routes `*.php` through `proxy_fcgi` to PHP-FPM. The startup script starts PHP-FPM in the background and then runs Apache in the foreground. ## Static Render Path `render-static-pages.sh` runs during the image build and renders static HTML for the read-heavy pages: - `/` - `/cv.php` - `/blog.php` - `/demos.php` - `/homelab-tree.php` Static output is generated for the committed static languages under `/static-pages//`. Apache rewrites supported static-language requests to those generated HTML files. Requests for runtime-generated languages still fall through to PHP so the translation workflow continues to work. `blog.php` no longer renders visitor ideas directly into the HTML. Recent ideas are served by `visitor_ideas.php` and loaded by `visitor-ideas.js`, which keeps the blog page cacheable while preserving the write path through `save_idea.php`. ## Cache Path The website sets cache headers for pre-rendered HTML: ```text Cache-Control: public, max-age=300, s-maxage=3600 ``` Dynamic write and translation endpoints are marked `no-store`: - `save_idea.php` - `visitor_ideas.php` - `translate.php` - `save_lang.php` The OCI edge nginx layer handles gzip, cache HITs, HTTP/2, TLS session reuse, client keepalive, and upstream keepalive to the Traefik backend. CSS and JavaScript keep the edge's longer immutable asset policy. ## Translation Flow Unsupported browser languages call the same-origin `translate.php` endpoint. The endpoint sanitizes and batches text, checks Redis per string, calls the custom `website-translator` Ollama model only for cache misses, writes fresh translations back to Redis with a TTL, then returns results in the original order. The browser never calls the private Ollama address directly. n8n is deployed separately at `https://n8n.lab2025.duckdns.org` and receives `OLLAMA_BASE_URL=http://192.168.100.68:11434` so workflows can prewarm translations or run batch jobs against the Debian-host Ollama API. ## Translation Observability `translate.php` emits one structured JSON log event for each valid translation request. The event name is `website_translation` and the payload does not include source text or translated text. Useful fields: - `cache_hits` - `cache_misses` - `ollama_latency_ms` - `failed_json_parse_count` - `timeout_count` - `status` - `reason` In Loki, filter website logs for `"event":"website_translation"` to inspect cache behavior, Ollama latency, JSON parse failures, and timeout failures. ## Validation Do not build images locally. The image is built and deployed by the Debian homelab path and Gitea Actions. Local source validation that does not build the image: ```bash shellcheck apps/website/start-website.sh apps/website/render-static-pages.sh find apps/website -name "*.php" -print0 | xargs -0 -n1 php -l node --check apps/website/visitor-ideas.js kubectl kustomize apps/website >/tmp/website-kustomize.yaml ``` Static rendering can be checked without Docker by copying the app to a temporary directory and running: ```bash sh render-static-pages.sh /path/to/copied/apps/website ```