271 lines
10 KiB
Markdown
271 lines
10 KiB
Markdown
# 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 18
|
|
|
|
---
|
|
|
|
## Development setup
|
|
|
|
### Prerequisites
|
|
|
|
- [Rust](https://rustup.rs/) (stable toolchain)
|
|
- [Node.js](https://nodejs.org/) 20+ and npm
|
|
- PostgreSQL (locally, or via Docker — see below)
|
|
- The Diesel CLI: `cargo install diesel_cli --no-default-features --features postgres`
|
|
(requires `libpq`; on Debian/Ubuntu: `sudo apt install libpq-dev`)
|
|
|
|
### 1. Configure environment variables
|
|
|
|
Copy the example file and fill in your own values:
|
|
|
|
```sh
|
|
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:
|
|
|
|
```sh
|
|
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):
|
|
|
|
```sh
|
|
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
|
|
|
|
```sh
|
|
cargo run
|
|
```
|
|
|
|
The API listens on `http://0.0.0.0:8001`. Backend logs respect `RUST_LOG`.
|
|
|
|
### 5. Run the frontend
|
|
|
|
```sh
|
|
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`:
|
|
|
|
```sh
|
|
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
|
|
|
|
```sh
|
|
# Backend
|
|
cargo build # compile
|
|
cargo test # run the test suite (needs a reachable Postgres + DATABASE_URL)
|
|
cargo clippy # lint
|
|
cargo fmt # format
|
|
```
|
|
|
|
```sh
|
|
# Frontend (run from vue/)
|
|
npm run test # run Vitest component tests
|
|
npm run lint # eslint
|
|
npm run format # prettier
|
|
```
|
|
|
|
```sh
|
|
# 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
|
|
|
|
```sh
|
|
docker compose up --build -d
|
|
```
|
|
|
|
This builds three images and starts them on a shared network:
|
|
|
|
- **`postgres`** — PostgreSQL 18, data persisted in the `postgres_data` volume, reachable on `localhost:5432`
|
|
- **`backend`** — multi-stage build (compiles the Rust binary in a `rust:slim` builder, runs it in a slim `debian` runtime image); runs embedded Diesel migrations automatically on startup; listens on `0.0.0.0:8001`
|
|
- **`frontend`** — multi-stage build (compiles the Vue app with `node:20-alpine`, serves the static bundle with `nginx:alpine`); listens on `0.0.0.0:8080` and proxies `/api/` to the `backend` service 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
|
|
|
|
```sh
|
|
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: hardened deployment — isolated user + rootless Docker
|
|
|
|
Anyone who can run `docker` commands effectively has root on the host (container volume mounts can reach the whole filesystem) — being in the `docker` group is root-equivalent. For a production server, it's worth confining this stack to a dedicated, unprivileged system user running its own **rootless Docker** daemon, instead of using a system-wide install or adding the user to the `docker` group.
|
|
|
|
Rootless Docker runs as a completely independent daemon — separate socket (`/run/user/<uid>/docker.sock`) and storage (`~/.local/share/docker` vs. `/var/lib/docker`) — so it coexists fine with an existing system-wide Docker install on the same host. Just make sure `DOCKER_HOST`/`PATH` point at the rootless daemon when operating on this stack, and that the ports you publish in step 4 below aren't already in use elsewhere.
|
|
|
|
**1. Create the user and enable lingering** (so its services keep running without an active login session):
|
|
|
|
```sh
|
|
sudo adduser --disabled-password --gecos "" rss-svc
|
|
sudo loginctl enable-linger rss-svc
|
|
```
|
|
|
|
**2. Install rootless Docker for that user:**
|
|
|
|
```sh
|
|
sudo apt install -y uidmap dbus-user-session
|
|
sudo machinectl shell rss-svc@
|
|
curl -fsSL https://get.docker.com/rootless | sh
|
|
```
|
|
|
|
Add to `~/.bashrc` (as `rss-svc`):
|
|
|
|
```sh
|
|
export PATH=/home/rss-svc/bin:$PATH
|
|
export DOCKER_HOST=unix:///run/user/$(id -u)/docker.sock
|
|
```
|
|
|
|
Enable the daemon as a persistent user service, then re-login (or `exec $SHELL`) so the exports above take effect:
|
|
|
|
```sh
|
|
systemctl --user enable docker
|
|
systemctl --user start docker
|
|
```
|
|
|
|
**3. Deploy the stack as `rss-svc`:**
|
|
|
|
```sh
|
|
git clone <your-repo-url> ~/rss-reader
|
|
cd ~/rss-reader
|
|
cp .env.example .env
|
|
chmod 600 .env
|
|
```
|
|
|
|
Fill in `.env` with strong, unique secrets — `openssl rand -hex 32` is a convenient way to generate `JWT_SECRET`/`POSTGRES_PASSWORD`.
|
|
|
|
**4. Bind published ports to localhost only.** The only thing that needs to reach this stack from outside is the reverse proxy below, and it runs on the same host. Edit `docker-compose.yml`:
|
|
|
|
```yaml
|
|
postgres:
|
|
ports:
|
|
- "127.0.0.1:5432:5432"
|
|
backend:
|
|
ports:
|
|
- "127.0.0.1:8001:8001"
|
|
frontend:
|
|
ports:
|
|
- "127.0.0.1:8080:80"
|
|
```
|
|
|
|
**5. Bring it up:**
|
|
|
|
```sh
|
|
docker compose up --build -d
|
|
```
|
|
|
|
**6. Firewall** (run as your normal sudo-capable user — not `rss-svc`):
|
|
|
|
```sh
|
|
sudo ufw allow OpenSSH
|
|
sudo ufw allow 80/tcp
|
|
sudo ufw allow 443/tcp
|
|
sudo ufw enable
|
|
```
|
|
|
|
With this in place: only SSH and HTTPS are reachable from the network, the reverse proxy is the sole entry point into the app, the whole stack runs in its own user/container/network namespaces with no elevated privileges, and secrets live in a `chmod 600` `.env` owned by an account that can't do anything else on the system.
|
|
|
|
### 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:
|
|
|
|
```sh
|
|
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:
|
|
|
|
```apache
|
|
<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.com` in your root `.env` so the backend's CORS check allows the proxied origin, then `docker compose up --build -d backend`.
|
|
- You no longer need to publish port `8080` to the LAN — change the `frontend` service's port mapping in `docker-compose.yml` to `"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](https://certbot.eff.org/) Apache plugin), which can also write the vhost and set up auto-renewal for you.
|
|
|
|
### Notes
|
|
|
|
- Migrations run automatically at backend startup — no manual `diesel` step needed in production.
|
|
- Secrets (`POSTGRES_PASSWORD`, `JWT_SECRET`, `FRONTEND_ORIGIN`) come from the root `.env`, which is gitignored and interpolated into `docker-compose.yml` via `${VAR}` — never commit real secrets.
|
|
- If you need the app reachable from multiple origins (e.g. `localhost:8080` on desktop and `<lan-ip>:8080` on 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 in `src/main.rs` for a self-hosted, trusted-network setup.
|