--- date: 2026-03-22 topic: "StemAtlas — Public Deployment on sgeboers.nl" status: validated --- # StemAtlas Deployment Design ## Problem Statement The stemwijzer project has three user-facing products ready to publish: 1. **A blog post** explaining the political compass methodology and findings 2. **An interactive explorer** (political compass, party trajectories, motion search) 3. **The stemwijzer quiz** (vote on motions, see which parties match you) These need to be deployed publicly on sgeboers.nl using the existing VPS + Gitea + Drone + Docker stack. --- ## The Name: StemAtlas **`stematlas.sgeboers.nl`** Dutch wordplay: **stem** = *vote* AND *voice* (as in "the voice of parliament") + **atlas** = a comprehensive map of the world. Together: *an atlas of voices* — a map of how Dutch democracy sounds from the inside. It's broader than "stemwijzer" (which implies a voting guide) — it positions the site as a data exploration and journalism tool. --- ## Constraints - Existing VPS running Nginx, Gitea, Drone - Deployment pipeline: Docker build → push to registry → SSH `docker-compose up -d` - sgeboers.nl is a **raw HTML/CSS site** (not Hugo) hosted as a repo on git.sgeboers.nl - DuckDB file lives on the VPS — single writer (scheduler), multiple readers (Streamlit) - No new cloud services or hosting costs --- ## Architecture ``` Internet │ ├── sgeboers.nl (raw HTML/CSS site, existing repo on git.sgeboers.nl) │ └── blog/stematlas.html ← blog post with inline charts + link to subdomain │ └── stematlas.sgeboers.nl └── Nginx (reverse proxy) └── Streamlit multi-page app (port 8501) ├── Page 1: Stemwijzer Quiz (app.py) └── Page 2: Explorer (explorer.py) VPS filesystem: /srv/stematlas/ ├── data/motions.db ← DuckDB (shared, read-write by scheduler) └── docker-compose.yml ``` --- ## Components ### 1. Streamlit Multi-Page App Restructure entry point from `app.py` → `Home.py` with a `pages/` directory: ``` Home.py ← landing page / about pages/ 1_Stemwijzer.py ← quiz (app.py content) 2_Explorer.py ← explorer.py content ``` Streamlit's built-in multi-page routing handles navigation. One Docker container, one port (8501). **Why not two separate containers?** Single shared DuckDB file on VPS filesystem. Both pages open read-only connections (quiz opens read-write for session data, but that's the existing behaviour). One container = one volume mount = no coordination overhead. ### 2. Docker Compose The existing `.drone.yml` already calls `docker-compose up -d` on the VPS. We add/update `docker-compose.yml`: ``` Services: stematlas: image: registry/stematlas:latest ports: 8501 (internal only) volumes: - /srv/stematlas/data:/app/data ← persistent DB restart: unless-stopped scheduler: image: registry/stematlas:latest command: python scheduler.py volumes: - /srv/stematlas/data:/app/data ← same DB, write access restart: unless-stopped ``` **Scheduler as a sidecar**: runs in the same image but different container, keeps DB updated nightly. Streamlit container never writes to DB (except user sessions in the quiz). ### 3. Nginx Vhost New server block on the VPS: ``` stematlas.sgeboers.nl → proxy_pass http://127.0.0.1:8501 ``` Standard Streamlit proxy requirements: `proxy_http_version 1.1`, WebSocket upgrade headers for `/_stcore/stream`. Let's Encrypt cert via Certbot (standard pattern). ### 4. Drone CI Pipeline Update Existing `.drone.yml` steps remain identical — build, push, SSH deploy. The only change: `docker-compose.yml` in the repo now references both the `stematlas` and `scheduler` services, so `docker-compose up -d` picks them both up. No new Drone secrets needed if `DOCKER_REGISTRY`, `DEPLOY_HOST` etc. are already set. ### 5. Blog Post (Raw HTML page on sgeboers.nl) The blog post is a new `blog/stematlas.html` file added to the sgeboers.nl repo on git.sgeboers.nl. The Drone pipeline for that repo deploys it like any other static file — push to git, Drone copies to webroot, Nginx serves it. **Chart embedding strategy — inline Plotly divs:** Rather than iframes, we extract just the chart `
` + `