2026-07-20 16:43:47 +00:00
|
|
|
# Daily Quote
|
|
|
|
|
|
|
|
|
|
A small full-stack "quote of the day" app, built for the privatecloud VM `private-cloud-daily-quote`.
|
|
|
|
|
|
|
|
|
|
## Stack
|
|
|
|
|
|
|
|
|
|
- **Frontend**: Vite + React, styled with [MUI](https://mui.com) using a Material Design 3 theme (color roles, type scale, shape/elevation).
|
|
|
|
|
- **API**: Express (Node.js), serving both `/api/*` and the built SPA on port **3000**.
|
|
|
|
|
- **Database**: Postgres 16, run via Docker Compose (`quotes`, `users`, `favorites` tables — see `db/init.sql`).
|
|
|
|
|
- **Platform auth**: [Zitadel](https://zitadel.com), self-hosted via Docker, OIDC Authorization Code + PKCE. Gates "submit a quote" and "favorites"; browsing is public.
|
|
|
|
|
- **Git hosting**: [Gitea](https://gitea.io), self-hosted via Docker (this repo's `origin`).
|
|
|
|
|
- **CI/CD**: Gitea Actions, using a local `act_runner` container. See `.gitea/workflows/ci.yml`.
|
2026-07-20 17:05:22 +00:00
|
|
|
Verified working end-to-end (checkout → build → syntax check, all green), but the
|
|
|
|
|
runner has occasionally been slow to pick up a freshly-pushed task on this VM; a
|
|
|
|
|
`docker restart quoteapp-gitea-runner` reliably kicks it back into gear if a run
|
|
|
|
|
seems stuck in "queued".
|
2026-07-20 16:43:47 +00:00
|
|
|
- **Observability**: `prom-client` metrics at `/api/metrics`, scraped by Prometheus, visualized in a provisioned Grafana dashboard.
|
|
|
|
|
- **Backup**: nightly `pg_dump` via a systemd timer (`quoteapp-backup.timer`), 7-day retention, dumps to `~/backups`.
|
|
|
|
|
|
|
|
|
|
## Local services (Docker)
|
|
|
|
|
|
|
|
|
|
| Service | Internal port | Host port | Notes |
|
|
|
|
|
|-------------|---------------|-----------|-------|
|
|
|
|
|
| Postgres | 5432 | 5432 (loopback only) | app + zitadel databases |
|
|
|
|
|
| Zitadel | 8080 | 8080 | public via `https://<host>:8080` |
|
|
|
|
|
| Gitea | 3000 | 3001 | public via `https://<host>:3001` |
|
|
|
|
|
| Gitea SSH | 22 | 2222 (loopback only) | |
|
|
|
|
|
| act_runner | - | - | registered against Gitea Actions |
|
|
|
|
|
| Prometheus | 9090 | 9090 (loopback only) | |
|
|
|
|
|
| Grafana | 3000 | 3002 | public via `https://<host>:3002`, `admin`/`admin` |
|
|
|
|
|
|
|
|
|
|
Bring the stack up with:
|
|
|
|
|
|
|
|
|
|
```sh
|
|
|
|
|
docker compose up -d
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
## Running the app
|
|
|
|
|
|
|
|
|
|
The app itself runs directly on the VM (not in Docker) as a systemd service on port 3000:
|
|
|
|
|
|
|
|
|
|
```sh
|
|
|
|
|
sudo systemctl status quote-app
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
To rebuild after a frontend change:
|
|
|
|
|
|
|
|
|
|
```sh
|
|
|
|
|
cd client && npm run build
|
|
|
|
|
sudo systemctl restart quote-app
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
## Auth notes
|
|
|
|
|
|
|
|
|
|
Zitadel is reachable at `https://<vm-host>:8080`. Since exe.dev's proxy only marks one
|
|
|
|
|
port ("3000", the app) fully public, signing in requires being an authorized user of
|
|
|
|
|
this workspace to reach the Zitadel login page on port 8080 — browsing and reading
|
|
|
|
|
quotes on the main app is public regardless. A demo user (`demo`) was created for
|
|
|
|
|
testing; see `.zitadel-demo-user` (not committed) for its password.
|
|
|
|
|
|
2026-07-20 17:10:39 +00:00
|
|
|
Zitadel defaults to "Login V2", a separate UI app we don't deploy here, so the
|
|
|
|
|
instance's `loginV2.required` feature flag was switched off in favor of the classic
|
|
|
|
|
login UI bundled in the main server image (`ZITADEL_DEFAULTINSTANCE_FEATURES_LOGINV2_REQUIRED`
|
|
|
|
|
in `docker-compose.yml`, plus a one-time `PUT /v2/features/instance` call against the
|
|
|
|
|
already-running instance — re-apply that call if the Zitadel volume is ever recreated
|
|
|
|
|
and the env var doesn't take retroactively).
|
|
|
|
|
|
|
|
|
|
The full authorize → login → callback → session chain was verified as far as it can be
|
|
|
|
|
from inside the VM: OIDC discovery, the PKCE authorize redirect, and the login page
|
|
|
|
|
itself (200, not 404) all check out. The very last mile — an actual browser completing
|
|
|
|
|
login — couldn't be driven from here, since this VM's own DNS resolves its public
|
|
|
|
|
hostname to a private, TLS-less address (bypassing exe.dev's real edge proxy), and
|
|
|
|
|
Zitadel's login cookie is `__Host-`-prefixed, which browsers only accept over genuine
|
|
|
|
|
HTTPS. Worth a real sign-in test from an actual browser to confirm the last step.
|
|
|
|
|
|
2026-07-20 16:43:47 +00:00
|
|
|
## Backups
|
|
|
|
|
|
|
|
|
|
```sh
|
|
|
|
|
./backup/backup.sh # run manually
|
|
|
|
|
systemctl list-timers quoteapp-backup.timer
|
|
|
|
|
```
|