release.yml's first real run failed: docker/login-action against 192.168.0.3:3000 hit "server gave HTTP response to HTTPS client" — Docker refuses any non-localhost registry over plain HTTP by default, so this was never actually a workflow bug. Rejected insecure-registries in daemon.json after reading this Unraid host's own rc.docker script: applying it needs a full dockerd restart, and with Live Restore disabled here, that stops every one of the ~40 other containers on the box first. Also rejected a real Let's Encrypt cert on a public bbergle.com subdomain — this host's other subdomains are Cloudflare-proxied, which would terminate TLS at Cloudflare's edge and never reach our own cert at all. Chosen instead, scoped to touch nothing already working: a self-signed cert for registry.bbergle.com behind a new NPMplus proxy host (found its real HTTPS port, 9537, by reading `docker port NPMplus` rather than assuming 443, which is a different nginx process on this box entirely); an /etc/hosts entry on the Unraid host so only that host needs to resolve the name (no DNS record, no router/NAT dependency); and its CA dropped into /etc/docker/certs.d, which Docker's own docs confirm is read per-connection with no daemon restart required. Also pins buildx to driver: docker instead of setup-buildx-action's default docker-container driver, which runs an isolated builder that doesn't see /etc/docker/certs.d and would have quietly defeated all of the above. Full record, including what was rejected and why, in docs/DECISIONS.md D17. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01R2ZKeWkZV7ehf7fivrAkkG
93 lines
4.7 KiB
Markdown
93 lines
4.7 KiB
Markdown
# deploy/
|
|
|
|
Everything needed to run Velodrome as **one container**. See `docs/DECISIONS.md` D15 (why SQLite
|
|
and one container) and D16 (why migrations run from the entrypoint, and the Caddy/tini setup)
|
|
before changing anything here.
|
|
|
|
```
|
|
../Dockerfile Multi-stage build: web SPA + API venv + Caddy, into one runtime image
|
|
Caddyfile Serves the static SPA, proxies /api/* to uvicorn on loopback
|
|
entrypoint.sh Runs migrations, then supervises uvicorn + Caddy as PID 1's children
|
|
unraid-template.xml Unraid Community Applications template — turns the env vars below into
|
|
fillable web UI fields instead of a .env file
|
|
```
|
|
|
|
There is no docker-compose here, deliberately — the app is one container, not a set of services
|
|
that need orchestrating together. (Later phases may add genuinely separate containers — a
|
|
tileserver, a local LLM — see `docs/PLAN.md`'s "Service topology"; those would get their own
|
|
compose file or Unraid templates when a phase actually needs one, not speculatively now.)
|
|
|
|
## Build
|
|
|
|
From the repo root (the build context — the Dockerfile needs both `apps/api` and `apps/web`):
|
|
|
|
```sh
|
|
docker build -t velodrome .
|
|
```
|
|
|
|
## Run
|
|
|
|
```sh
|
|
docker run -d \
|
|
--name velodrome \
|
|
-p 8080:8080 \
|
|
-v velodrome-data:/data \
|
|
-e VELODROME_PUBLIC_URL=https://bikes.example.com \
|
|
-e VELODROME_SECRET_KEY=$(openssl rand -hex 32) \
|
|
-e VELODROME_ENVIRONMENT=production \
|
|
velodrome
|
|
```
|
|
|
|
Put a TLS-terminating reverse proxy (whatever's already fronting other services on the host) in
|
|
front of port 8080 — this container only ever serves plain HTTP itself.
|
|
|
|
`GET http://<host>:8080/api/v1/healthz` should return `{"status": "ok"}` once it's up.
|
|
|
|
## Environment variables
|
|
|
|
All read by `apps/api/velodrome/config.py` (prefix `VELODROME_`) — the app and Alembic both read
|
|
the same values, there's no separate migration-time config anymore (docs/DECISIONS.md D15).
|
|
|
|
| Variable | Required | Default (baked into the image) | Notes |
|
|
|---|---|---|---|
|
|
| `VELODROME_DATABASE_URL` | No — don't override | `sqlite+aiosqlite:////data/velodrome.db` | Fixed to the `/data` volume mount. Change the volume mapping, not this. |
|
|
| `VELODROME_PUBLIC_URL` | **Yes** | none | The externally-visible URL. Checked against `Origin` on cookie-authenticated mutations — get this wrong and every logged-in write silently 403s. |
|
|
| `VELODROME_SECRET_KEY` | **Yes** | insecure dev placeholder | `openssl rand -hex 32`. Not yet used for anything reachable (arrives with Bryton credential encryption in a later phase) — set a real value now anyway. |
|
|
| `VELODROME_ENVIRONMENT` | **Yes** | `development` | `development` \| `test` \| `production`. Gates the session cookie's `Secure` flag — always `production` behind real HTTPS. |
|
|
| `VELODROME_SESSION_TTL_DAYS` | No | `90` | Login session lifetime. |
|
|
| `VELODROME_SESSION_COOKIE_NAME` | No | `vd_session` | Only matters if it collides with another app on the same domain. |
|
|
|
|
## Volumes
|
|
|
|
| Path | Contents |
|
|
|---|---|
|
|
| `/data` | The SQLite database file. Will also hold the content-addressed blob store once Phase 1 builds ingestion. This is the only thing that needs backing up. |
|
|
|
|
## Unraid
|
|
|
|
Import `unraid-template.xml` from the Docker tab's "Add Container" template picker — it exposes
|
|
the Port, Data path, and the env vars above as fillable web UI fields, matching the earlier
|
|
decision to keep configuration in Unraid's own UI rather than a `.env` file on disk. Every field
|
|
stays editable by hand afterward regardless of what the template pre-fills.
|
|
|
|
## Publishing the image
|
|
|
|
`.gitea/workflows/release.yml` builds this Dockerfile and pushes it to the Gitea container
|
|
registry at `registry.bbergle.com:9537/bbergle/bike-app` on a `v*` tag push, or on manual
|
|
`workflow_dispatch`. Not `192.168.0.3:3000` (Gitea's own plain-HTTP address) directly — Docker
|
|
refuses any non-localhost registry over plain HTTP by default, so `registry.bbergle.com:9537` is
|
|
an NPMplus proxy host in front of Gitea's registry that terminates TLS with a self-signed cert.
|
|
See `docs/DECISIONS.md` D17 for the full setup (cert, NPMplus proxy host, `certs.d` trust, and the
|
|
buildx driver change this required) — none of it is committed here, since it's host-local trust
|
|
material and NPMplus config, not something this repo can or should own.
|
|
|
|
It does **not** SSH into the host and recreate the running container — rolling out a new image on
|
|
Unraid (pulling it and clicking "Apply" on the container, or via Unraid's own update-checking) is
|
|
left as a manual/Unraid-side step, not something CI does unattended.
|
|
|
|
## What's not here yet
|
|
|
|
Backups (`docs/PLAN.md` calls for a systemd timer running `restic` against `/data`, independent of
|
|
CI) and the `import_inbox` USB-watch bind mount are both Phase 1+ concerns — nothing in the schema
|
|
uses them yet.
|