Self-hosted algorithmic RSS feed on k3s
  • HTML 58.3%
  • Python 41%
  • Dockerfile 0.7%
Find a file
ignohr c61d4ca351
All checks were successful
Build and push RSS feed images / build-scorer (push) Successful in 2m25s
Build and push RSS feed images / build-collector (push) Successful in 2m37s
Build and push RSS feed images / build-frontend (push) Successful in 2m39s
Build and push RSS feed images / deploy (push) Successful in 44s
ci: add workflow file path to trigger filter so workflow changes self-apply
2026-06-24 00:21:46 -04:00
.forgejo/workflows ci: add workflow file path to trigger filter so workflow changes self-apply 2026-06-24 00:21:46 -04:00
android Add Android TWA build, assetlinks.json, and deterministic language detection 2026-06-23 21:07:40 -04:00
base Initial commit — algorithmic RSS feed on k3s 2026-06-13 17:27:50 -04:00
feedback Fix registry URLs: gitea.ignohr.com → forgejo.ignohr.com throughout 2026-06-23 23:05:56 -04:00
ingestion Initial commit — algorithmic RSS feed on k3s 2026-06-13 17:27:50 -04:00
presentation Fix registry URLs: gitea.ignohr.com → forgejo.ignohr.com throughout 2026-06-23 23:05:56 -04:00
scoring Fix registry URLs: gitea.ignohr.com → forgejo.ignohr.com throughout 2026-06-23 23:05:56 -04:00
users/eric Fix registry URLs: gitea.ignohr.com → forgejo.ignohr.com throughout 2026-06-23 23:05:56 -04:00
.gitignore Initial commit — algorithmic RSS feed on k3s 2026-06-13 17:27:50 -04:00
README.md Initial commit — algorithmic RSS feed on k3s 2026-06-13 17:27:50 -04:00

RSS Algorithmic Feed

Self-hosted, algorithmic RSS feed running on k3s. Scores articles by cosine similarity to your reading history + LLM relevance judgement against an editable interest profile + implicit dwell/scroll signals.

Configuration

Setting Value
Namespace rss
Ollama On-cluster, CPU only
TLS cert-manager + Let's Encrypt (letsencrypt-prod)
Miniflux https://rss.ignohr.com
Frontend https://feed.ignohr.com
Storage CephRBD (ceph-rbd storageClass)
RSS sources → Miniflux (rss.ignohr.com)
            → RSSHub (non-native RSS)
            → Mercury Parser (full-text)
                     ↓
            Scorer service (Python)
            ↕              ↕
         Ollama          Postgres+pgvector
      (embeddings)      (scores + vectors)
                     ↓
            Feedback collector (FastAPI)
              ↕ events
            Frontend SPA (feed.ignohr.com)

Deployment order

1. Secrets (manual, don't commit)

# Edit base/secrets.yaml with real values first
kubectl apply -f base/namespace.yaml
kubectl apply -f base/secrets.yaml

2. Ingestion layer

kubectl apply -f ingestion/postgres.yaml
# Wait for postgres to be ready
kubectl rollout status statefulset/miniflux-postgres -n rss
kubectl apply -f ingestion/miniflux.yaml
kubectl apply -f ingestion/rsshub.yaml
kubectl apply -f ingestion/mercury-parser.yaml

Open rss.ignohr.com, log in, and:

  • Add your RSS feeds
  • Go to Settings → API Keys → Create an API key
  • Update scorer-secrets with the key: kubectl edit secret scorer-secrets -n rss

3. Scoring layer

# Option A: Ollama on cluster (CPU)
kubectl apply -f scoring/ollama.yaml
# Wait for init container to pull nomic-embed-text (~270MB)
kubectl logs -f deployment/ollama -n rss -c pull-models

# Option B: Ollama on workstation — edit scoring/ollama.yaml,
# delete the StatefulSet/Deployment and uncomment the ExternalName Service

# Build and push scorer image
cd scoring/scorer-app
docker build -t gitea.ignohr.com/isaac/rss-scorer:latest .
docker push gitea.ignohr.com/isaac/rss-scorer:latest
cd ../..

kubectl apply -f scoring/scorer.yaml
kubectl logs -f deployment/scorer -n rss

4. Feedback layer

cd feedback
docker build -t gitea.ignohr.com/isaac/rss-feedback-collector:latest .
docker push gitea.ignohr.com/isaac/rss-feedback-collector:latest
cd ..

kubectl apply -f feedback/collector.yaml

5. Frontend

cd presentation
docker build -t gitea.ignohr.com/isaac/rss-frontend:latest .
docker push gitea.ignohr.com/isaac/rss-frontend:latest
cd ..

kubectl apply -f presentation/frontend.yaml

Open feed.ignohr.com. Articles will appear after the first scorer run (~2 min).

First-run bootstrap

Before you have liked articles, scoring falls back to the interest profile. Open feed.ignohr.com → "Interest profile" → describe what you care about. Example:

Self-hosted infrastructure, Kubernetes, Linux kernel development, AI/ML research (especially local inference), homelab, open source tooling. Not interested in crypto, celebrity news, marketing.

After thumbing 10+ articles, the embedding centroid kicks in and the profile becomes less important.

Tuning

Edit JUDGE_MODEL in scoring/scorer.yaml:

  • "" — LLM judge disabled; pure embedding similarity + profile (fastest)
  • "qwen2.5:1.5b" — fast, decent quality
  • "qwen2.5:7b" — better quality, ~10s/article on CPU

Edit score weights in scoring/scorer-app/main.pycompute_final_score().

pgvector schema

All data lives in the rss namespace Postgres instance:

  • article_scores — entry embeddings + scores
  • preference_vectors — liked/disliked centroids
  • interest_profile — editable profile text
  • reading_events — raw implicit feedback events
# Connect to postgres directly
kubectl exec -it statefulset/miniflux-postgres -n rss -- psql -U miniflux miniflux

Resource footprint (approximate)

Component CPU req RAM req
miniflux-postgres 100m 256Mi
miniflux 50m 64Mi
rsshub 50m 128Mi
mercury-parser 50m 64Mi
ollama (CPU) 200m 512Mi
scorer 50m 128Mi
feedback-collector 25m 64Mi
rss-frontend 10m 16Mi
Total 535m 1.2Gi

.gitignore

base/secrets.yaml