Docs: production deployment recipe + Arch gotchas
- README: expand the production section with a full systemd recipe (useradd, chown, uv sync --project, install -d for ReadWritePaths, .env perms 640), a Caddy block, and backup note. - AGENTS.md: add the Arch python-uv package-name note and the root-rooted venv caveat, plus a new "Deploy / production" section documenting the volundr unit, data location, .env perms, and forward-proxy expectations.
This commit is contained in:
@@ -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,
|
There is **no `main.py` at the repo root** — the app is the `app` package,
|
||||||
entry point `app.main:app`.
|
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
|
## Configuration
|
||||||
|
|
||||||
All settings come from env vars (or a `.env` file) via `pydantic-settings`,
|
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,
|
lead time, default 60), `BIKE_ADMIN_RENEWAL_MONTHS` (MOT/insurance validity,
|
||||||
default 12).
|
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
|
## Architecture / layout
|
||||||
|
|
||||||
- `app/config.py` – `Settings` (pydantic-settings) + `get_settings()` (cached).
|
- `app/config.py` – `Settings` (pydantic-settings) + `get_settings()` (cached).
|
||||||
|
|||||||
@@ -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
|
Or copy `.env.example` to `.env` and fill in the values (env vars and the local
|
||||||
`.env` file are both read automatically).
|
`.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 —
|
||||||
|
`<data_dir>/bike_admin.db` (SQLite, WAL; `data/bike_admin.db` by default).
|
||||||
|
|
||||||
## Tests / lint
|
## Tests / lint
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
@@ -49,13 +98,3 @@ uv run pytest -q
|
|||||||
uv run ruff check app tests
|
uv run ruff check app tests
|
||||||
uv run ruff format --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).
|
|
||||||
|
|||||||
Reference in New Issue
Block a user