diff --git a/AGENTS.md b/AGENTS.md index 5cbb4b4..94e15ae 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -28,6 +28,12 @@ uv run ruff format app tests # apply formatting There is **no `main.py` at the repo root** — the app is the `app` package, entry point `app.main:app`. +**Arch Linux note:** `uv` ships as `python-uv` (not `uv` or `python3-uv`) so +`sudo pacman -S --needed python-uv` is the install command. If you create the +venv with `sudo`, the interpreter will provision under `/root/.local/share/uv` +and a non-root service user can never execute it; build the venv as the +service user (or against a system Python — see README "systemd recipe"). + ## Configuration All settings come from env vars (or a `.env` file) via `pydantic-settings`, @@ -45,6 +51,30 @@ All settings come from env vars (or a `.env` file) via `pydantic-settings`, lead time, default 60), `BIKE_ADMIN_RENEWAL_MONTHS` (MOT/insurance validity, default 12). +## Deploy / production + +`deploy/bike_admin.service` is the systemd unit for the production server +(volundr). Key points: + +- Runs as the unprivileged `bike` system user (no home dir needed beyond the + uv-provisioned interpreter in `/home/bike`.local/share/uv/...). +- App data lives next to the checkout: `/opt/bike_admin/data`. The app creates + `data/bike_admin.db` on startup, but systemd requires the `ReadWritePaths=` + target to exist before start — hence the one-time + `sudo install -d -o bike -g bike /opt/bike_admin/data` step (see README). +- `.env` at `/opt/bike_admin/.env`, `chown bike:bike` and `chmod 640`. + (systemd reads it at ExecStart.) +- Hardening kept: `NoNewPrivileges`, `PrivateTmp`, `ProtectSystem=full`, + `ProtectHome=true`. Do not remove `ProtectHome` unless the interpreter path + moves back to `/home`; `StateDirectory=` is the cleaner alternative for + data, but we chose `ReadWritePaths=` to keep data next to the app. + +Caddy (hermodr, on a separate host) reverse-proxies +`volundr.local.jakes-asgard.uk` to the app's IP:8000. The app trusts +`X-Forwarded-For` (BIKE_ADMIN_TRUST_PROXY defaults true) and uvicorn's +`--forwarded-allow-ips` whitelist in the unit governs which proxies it'll +accept. + ## Architecture / layout - `app/config.py` – `Settings` (pydantic-settings) + `get_settings()` (cached). diff --git a/README.md b/README.md index b9e9233..b92472c 100644 --- a/README.md +++ b/README.md @@ -42,6 +42,55 @@ uv run uvicorn app.main:app --host 127.0.0.1 --port 8000 Or copy `.env.example` to `.env` and fill in the values (env vars and the local `.env` file are both read automatically). +## Running behind a reverse proxy (production) + +TLS terminated at the proxy (Caddy/nginx), app on a private host behind it. +`deploy/bike_admin.service` is the systemd unit used in production; +`deploy/env.production.example` is the production `.env` template. + +- Set `BIKE_ADMIN_IS_PRODUCTION=true` so the session cookie is `Secure`. +- Bind as needed for the proxy to reach you: + - proxy on the **same** host → `--host 127.0.0.1` + - proxy on **another** host (e.g. a single fronting Caddy) → `--host 0.0.0.0` + or bind to a specific LAN interface; never the public interface directly, since + there is no app-level TLS. +- Proxy forwards to the app and sets `X-Forwarded-For` / `X-Forwarded-Proto`; + the app trusts these by default (`BIKE_ADMIN_TRUST_PROXY`). uvicorn's + `--forwarded-allow-ips 192.168.0.0/16 --proxy-headers` accepts them + (adjust to your LAN range). + +### systemd (production recipe) + +```bash +# one-time: deps + service account [on Arch: uv ships as python-uv] +sudo pacman -S --needed git python-uv +sudo useradd --system bike + +# one-time: app +sudo chown -R bike:bike /opt/bike_admin +sudo -u bike uv sync --all-extras --project /opt/bike_admin +sudo install -d -o bike -g bike /opt/bike_admin/data # systemd needs it to exist + +# one-time: .env (see deploy/env.production.example) +sudo chown bike:bike /opt/bike_admin/.env +sudo chmod 640 /opt/bike_admin/.env # owner rw, group r, others none + +sudo cp deploy/bike_admin.service /etc/systemd/system/bike-admin.service +sudo systemctl daemon-reload +sudo systemctl enable --now bike-admin +``` + +Caddy block for the fronting proxy: + +``` +volundr.local.jakes-asgard.uk { + reverse_proxy 192.168.0.52:8000 +} +``` + +Back up your data by copying the single file in the data dir — +`/bike_admin.db` (SQLite, WAL; `data/bike_admin.db` by default). + ## Tests / lint ```bash @@ -49,13 +98,3 @@ uv run pytest -q uv run ruff check app tests uv run ruff format --check app tests ``` - -## Running behind a reverse proxy (production) - -- Terminate TLS at the proxy (nginx/apache/caddy) and forward to the app. -- Set `BIKE_ADMIN_IS_PRODUCTION=true` so the session cookie is `Secure`. -- Bind to the private address: `uvicorn app.main:app --host 127.0.0.1 --port 8000` -- Pass `X-Forwarded-For` / `X-Forwarded-Proto` so throttling + secure-cookie logic - see the real client IP/protocol (the app trusts these by default). - -Back up your data by copying the single file `data/bike_admin.db` (SQLite, WAL).