# Deploying This replaces the `news-backend` service (from `../news_triage`) in place: same host, same vhost, same ntfy topic. ## 1. Compose Replace the `news-backend` block in `~/compose.yml`: ```yaml news-backend: image: git.rcjohnstone.com/connor/news:latest restart: unless-stopped networks: - internal environment: - TZ=America/Louisville - NEWS_DB=/data/news.db - NEWS_CONFIG_DIR=/config - NTFY_URL=${NTFY_URL} # The old service defaulted to this topic (bin_support.rs:121) rather than # setting it, so reusing it keeps the existing phone subscription working. - NTFY_TOPIC=news-triage - NTFY_USER=${NTFY_USER} - NTFY_PASS=${NTFY_PASS} # REQUIRED for feedback buttons. ntfy runs http actions FROM THE PHONE, so # this must be the public URL, not a container address. Its absence is # exactly why the previous version's buttons never fired. - NEWS_API_BASE_URL=https://news.rcjohnstone.com - NEWS_FEEDBACK_SECRET=${NEWS_FEEDBACK_SECRET} # One LLM call per day, for the digest prose only. - NEWS_LITELLM_KEY=${NEWS_LITELLM_KEY} volumes: # `U` makes podman chown the volume to the container's uid. The image runs # as uid 10001, not root, so without it SQLite fails with # "attempt to write a readonly database". - ./data/news2:/data:U - ./config/news2:/config:ro healthcheck: # NOT curl: the v0.2 image shipped none, so its healthcheck failed forever # while the service was fine and `podman ps` reported it unhealthy for days. test: ["CMD", "python", "-c", "import urllib.request,sys; sys.exit(0 if urllib.request.urlopen('http://localhost:3000/healthz',timeout=4).status==200 else 1)"] interval: 60s timeout: 5s retries: 3 start_period: 20s ``` Copy `profile/*.toml` to `~/config/news2/`. A bind-mounted config edit needs `sudo podman restart news-backend`; `podman-compose up -d` will not restart a container when only a mounted file changed. An **environment** change does need the full `up -d`. ## 2. Caddy The existing `news.rcjohnstone.com` block serves a static SPA from `/srv/news/dist` and proxies only `/api*`. This app renders its own pages, so the block simplifies -- but it must also become reachable from the phone. ``` # ddns: public news.rcjohnstone.com { import secure_headers import accesslog # ntfy executes a notification's action buttons FROM THE PHONE, and a native # app cannot complete an interactive login -- the same reason ntfy, Immich # and Jellyfin are exempted from the identity gateway on this box. So this # one path bypasses Authelia. It is safe to: every request carries an HMAC # signed over (cluster_id, kind), so a leaked URL casts one vote on one # story rather than acting as a bearer token for the model. @feedback path /api/feedback/* handle @feedback { reverse_proxy news-backend:3000 } # Everything else is browser-facing, so it goes behind Authelia. Tapping the # digest notification opens a browser, where an interactive login works. handle { import authelia reverse_proxy news-backend:3000 } } ``` The change from the current block is deliberate and worth stating plainly: the vhost moves from **LAN-only** (`tls internal` + `import internal`, which aborts anything outside 10.0.0.0/24, 10.0.1.0/24, 10.89.0.0/16) to **public**. Without that, feedback buttons fail on cellular and the digest link is unopenable away from home -- and silent failure there is precisely how the previous version ended up with 32 labels in a week. `/srv/news/dist` becomes unused and the `news-update` systemd timer that populated it can be disabled. ## 3. Scheduling ``` */15 * * * * podman exec news-backend python -m news.cli poll */20 * * * * podman exec news-backend python -m news.cli notify 45 8 * * * podman exec news-backend python -m news.cli digest ``` ## 4. Verifying it actually works ```sh podman exec news-backend python -m news.cli poll # fetch, embed, cluster podman exec news-backend python -m news.cli simulate # what WOULD be sent podman exec news-backend python -m news.cli notify --dry-run ``` Then the one test that matters, because it is the loop that was broken before: send a real notification, **tap a feedback button on the phone**, and confirm a row lands in `feedback`: ```sh podman exec news-backend python -c \ "import sqlite3;print(sqlite3.connect('/data/news.db').execute('select * from feedback').fetchall())" ```