RSS Reader
A self-hosted RSS reader: a Rust/actix-web + Diesel/PostgreSQL backend with a Vue 3 single-page-app frontend. Sync feeds, read articles, and mark them as read — from your desktop or your phone.
Stack
- Backend: Rust, actix-web, Diesel ORM, PostgreSQL, JWT auth
- Frontend: Vue 3, Vite, axios
- Database: PostgreSQL 15
Development setup
Prerequisites
- Rust (stable toolchain)
- Node.js 20+ and npm
- PostgreSQL (locally, or via Docker — see below)
- The Diesel CLI:
cargo install diesel_cli --no-default-features --features postgres(requireslibpq; on Debian/Ubuntu:sudo apt install libpq-dev)
1. Configure environment variables
Copy the example file and fill in your own values:
cp .env.example .env
.env is read by both the backend (via dotenv) and the diesel CLI, and is gitignored — never commit it.
| Variable | Purpose |
|---|---|
DATABASE_URL |
Postgres connection string, e.g. postgres://admin:changeme@localhost/rss |
JWT_SECRET |
Secret used to sign/verify auth tokens — use a long random string |
FRONTEND_ORIGIN |
Origin allowed by CORS, e.g. http://localhost:5173 for the Vite dev server |
RUST_LOG |
Log level for env_logger, e.g. info |
POSTGRES_PASSWORD |
Password for the admin Postgres user (used by docker-compose.yml) |
2. Start PostgreSQL
The simplest way during development is to run just the database container:
docker compose up postgres -d
This starts Postgres on localhost:5432 with the credentials from .env. Alternatively, point DATABASE_URL at any Postgres instance you already have running.
3. Run database migrations
Migrations are embedded in the backend binary and also run automatically on startup, but you can apply them manually with the Diesel CLI (useful when generating new migrations during development):
diesel setup # creates the database and runs existing migrations
diesel migration run # applies any pending migrations
diesel migration generate <name> # scaffolds a new up.sql/down.sql pair
4. Run the backend
cargo run
The API listens on http://0.0.0.0:8001. Backend logs respect RUST_LOG.
5. Run the frontend
cd vue
npm install
npm run dev -- --host
The Vite dev server runs on http://localhost:5173 (the --host flag also exposes it on your LAN so you can test from a phone) and proxies /api requests to http://localhost:8001 (configured in vue/vite.config.js).
6. Try it out
Create a user, then log in through the UI at http://localhost:5173:
curl -X POST -H "Content-Type: application/json" \
-d '{"name": "mace", "email": "you@example.com", "password": "secret"}' \
http://localhost:8001/api/v1/user/create
Useful commands during development
# Backend
cargo build # compile
cargo test # run the test suite (needs a reachable Postgres + DATABASE_URL)
cargo clippy # lint
cargo fmt # format
# Frontend (run from vue/)
npm run test # run Vitest component tests
npm run lint # eslint
npm run format # prettier
# Inspect the dockerized database directly
docker exec -it rss-postgres psql -d rss -U admin
Production setup (Docker)
The whole stack — Postgres, backend, and frontend — runs via Docker Compose. The frontend is built as a static Vue bundle and served by nginx, which also reverse-proxies /api/ to the backend container.
1. Configure environment variables
Same as in development — create a root-level .env from .env.example and fill in strong, unique values for POSTGRES_PASSWORD and JWT_SECRET. Set FRONTEND_ORIGIN to the URL the frontend will actually be served from (e.g. http://<host-ip>:8080 or your domain), since the backend's CORS policy only allows that origin.
2. Build and start everything
docker compose up --build -d
This builds three images and starts them on a shared network:
postgres— PostgreSQL 15, data persisted in thepostgres_datavolume, reachable onlocalhost:5432backend— multi-stage build (compiles the Rust binary in arust:slimbuilder, runs it in a slimdebianruntime image); runs embedded Diesel migrations automatically on startup; listens on0.0.0.0:8001frontend— multi-stage build (compiles the Vue app withnode:20-alpine, serves the static bundle withnginx:alpine); listens on0.0.0.0:8080and proxies/api/to thebackendservice over Docker's internal network
3. Use it
- From the host machine:
http://localhost:8080 - From another device on the same network (e.g. a phone):
http://<host-LAN-IP>:8080
Operating the stack
docker compose ps # check container status
docker compose logs -f backend # follow backend logs
docker compose down # stop everything (keeps the postgres_data volume)
docker compose down -v # stop and wipe all data — careful!
docker compose up --build -d # rebuild after pulling code changes
Optional: Apache reverse proxy (TLS termination)
If you want to expose the app under a domain with HTTPS, put Apache in front of the frontend container (which keeps listening on localhost:8080) and let Apache handle TLS. Enable the required modules first:
sudo a2enmod proxy proxy_http proxy_wstunnel ssl headers
Then a vhost like this proxies everything — including the WebSocket-capable Vite/axios traffic and the /api/ calls the frontend's nginx already forwards to the backend — to the container:
<VirtualHost *:443>
ServerName rss.example.com
SSLEngine on
SSLCertificateFile /etc/letsencrypt/live/rss.example.com/fullchain.pem
SSLCertificateKeyFile /etc/letsencrypt/live/rss.example.com/privkey.pem
ProxyPreserveHost On
ProxyPass / http://127.0.0.1:8080/
ProxyPassReverse / http://127.0.0.1:8080/
RequestHeader set X-Forwarded-Proto "https"
ErrorLog ${APACHE_LOG_DIR}/rss-error.log
CustomLog ${APACHE_LOG_DIR}/rss-access.log combined
</VirtualHost>
# Redirect plain HTTP to HTTPS
<VirtualHost *:80>
ServerName rss.example.com
Redirect permanent / https://rss.example.com/
</VirtualHost>
Notes for this setup:
- Set
FRONTEND_ORIGIN=https://rss.example.comin your root.envso the backend's CORS check allows the proxied origin, thendocker compose up --build -d backend. - You no longer need to publish port
8080to the LAN — change thefrontendservice's port mapping indocker-compose.ymlto"127.0.0.1:8080:80"so only Apache (on the same host) can reach it. - Obtain the certificate with
certbot --apache -d rss.example.com(via the Certbot Apache plugin), which can also write the vhost and set up auto-renewal for you.
Notes
- Migrations run automatically at backend startup — no manual
dieselstep needed in production. - Secrets (
POSTGRES_PASSWORD,JWT_SECRET,FRONTEND_ORIGIN) come from the root.env, which is gitignored and interpolated intodocker-compose.ymlvia${VAR}— never commit real secrets. - If you need the app reachable from multiple origins (e.g.
localhost:8080on desktop and<lan-ip>:8080on a phone), the current single-origin CORS check (FRONTEND_ORIGIN) won't allow both — pick the one you'll actually use, or relax the CORS policy insrc/main.rsfor a self-hosted, trusted-network setup.