Move the deployment runbook out of the repository into CTXD #137

8 changed files with 50 additions and 1520 deletions
+9 -55
View File
@@ -11,61 +11,14 @@ single Go service with the React PWA embedded in the binary.
> `/health`, embedded PWA skeleton, Makefile, Dockerfile, CI). The planned > `/health`, embedded PWA skeleton, Makefile, Dockerfile, CI). The planned
> application is described in [docs/ARCHITECTURE.md](docs/ARCHITECTURE.md). > application is described in [docs/ARCHITECTURE.md](docs/ARCHITECTURE.md).
## Quick start ## Running it locally
Requirements: Go 1.25 and Node.js `^22.22.2 || ^24.15.0 || >=26.0.0` — that The developer setup (prerequisites, Authentik and Google clients, the
is, 22.22.2 or later on the 22 line, 24.15.0 or later on the 24 line, or 26.0.0 `MEALQUEST_*` environment, `make dev` and the optional SearXNG sidecar) is
or later. Node.js 23, 24.0–24.14 and 25 are not supported: this is the runbook information and lives in CTXD: project `mealquest`, file
`engines` range of jsdom in the committed lockfile, the strictest of all the `RUNBOOKS.MD`, section "Local development". Configuration is read only from
web dependencies. the environment, and the service refuses to start, naming every missing or
invalid variable.
```sh
(cd web && npm ci)
mkdir -p ~/.mealquest-dev # outside the repository: it holds a secret
chmod 700 ~/.mealquest-dev
head -c 32 /dev/urandom | base64 > ~/.mealquest-dev/secret_key
chmod 600 ~/.mealquest-dev/secret_key
export MEALQUEST_HTTP_ADDR=:8080
export MEALQUEST_PUBLIC_URL=http://localhost:5173
export MEALQUEST_DATA_DIR="$HOME/.mealquest-dev"
export MEALQUEST_SHUTDOWN_TIMEOUT=10s
export MEALQUEST_SECRET_KEY_FILE="$HOME/.mealquest-dev/secret_key"
# Authentik OAuth2/OpenID provider for MealQuest (redirect URI:
# $MEALQUEST_PUBLIC_URL/auth/callback). Save its client secret to the file.
export MEALQUEST_OIDC_ISSUER=https://auth.example.com/application/o/mealquest/
export MEALQUEST_OIDC_CLIENT_ID=mealquest
export MEALQUEST_OIDC_CLIENT_SECRET_FILE="$HOME/.mealquest-dev/oidc_client_secret"
export MEALQUEST_OIDC_ALLOWED_GROUPS=mealquest-admin
# Optional: sessions end after MEALQUEST_SESSION_IDLE unused (default 720h)
# and always MEALQUEST_SESSION_MAX after sign-in (default 4320h).
# export MEALQUEST_SESSION_IDLE=720h
# export MEALQUEST_SESSION_MAX=4320h
# Optional: comma-separated IPs/CIDRs of reverse proxies whose
# X-Forwarded-For/-Proto headers are honoured (production: Traefik). Unset
# trusts no peer, which is right for local development.
# export MEALQUEST_TRUSTED_PROXY=10.60.1.33
# Optional: a SearXNG instance the Settings screen suggests as the recipe
# search URL until one is saved.
# export MEALQUEST_SEARXNG_URL=http://localhost:8888
export MEALQUEST_DEFAULT_TZ=America/New_York
make dev
```
`make dev` runs the Go server on `:8080` and the Vite dev server on `:5173`
(which proxies `/api`, `/auth` and `/media` to `:8080`). Open
http://localhost:5173. Configuration is environment-only and fails fast:
every variable above is required except the commented-out session
lifetimes (when set they must be positive, with MAX at least IDLE),
trusted proxy list (every entry must be an IP or CIDR), the SearXNG URL
(an absolute `http://` or `https://` URL without credentials, a query or a
fragment) and `MEALQUEST_DEFAULT_TZ`, and
secrets are read only from files named by `*_FILE` variables. `MEALQUEST_PUBLIC_URL`, `MEALQUEST_OIDC_ISSUER` and the
endpoints the issuer advertises must be `https://`; plain `http://` is
accepted only for `localhost`, `127.0.0.1` and `[::1]`.
`MEALQUEST_DEFAULT_TZ` is the IANA zone the household settings are created
with on first boot: until those settings exist, boot fails unless it is a
valid IANA zone. Later boots ignore it, set or not (change the zone in
household settings instead).
## Common commands ## Common commands
@@ -78,7 +31,8 @@ household settings instead).
## Where to read next ## Where to read next
- [AGENTS.md](AGENTS.md) — house rules, approved dependencies, conventions. - [AGENTS.md](AGENTS.md) — house rules, approved dependencies, the quality
floor every change must pass, repository layout, conventions.
- [docs/ARCHITECTURE.md](docs/ARCHITECTURE.md) — architecture of record. - [docs/ARCHITECTURE.md](docs/ARCHITECTURE.md) — architecture of record.
- [design/MealQuest Requirements.md](design/MealQuest%20Requirements.md) — product requirements. - [design/MealQuest Requirements.md](design/MealQuest%20Requirements.md) — product requirements.
- [design/MealQuest UI Designs/design_handoff_mealquest/README.md](design/MealQuest%20UI%20Designs/design_handoff_mealquest/README.md) — UI handoff. - [design/MealQuest UI Designs/design_handoff_mealquest/README.md](design/MealQuest%20UI%20Designs/design_handoff_mealquest/README.md) — UI handoff.
+18 -23
View File
@@ -1,20 +1,15 @@
# MealQuest production environment. Copy to deploy/.env (never committed) # MealQuest production environment: the template for deploy/.env (never
# and replace the placeholders. The service fails fast: a missing or invalid # committed). The service fails fast: a missing or invalid required variable
# required variable stops it at boot with the variable named. # stops it at boot with the variable named. Creating deploy/.env: CTXD
# project mealquest, RUNBOOKS.MD "Stack and first deploy" > "3. Environment
# file".
# #
# Secrets are never set here. They live in deploy/secrets/ (never committed), # Secrets are never set here. They live in deploy/secrets/ (never committed),
# one value per file, owned by the container user and readable only by it: # one value per file, owned by the container user 10001:10001 with mode
# # 0600: secret_key, oidc_client_secret and google_client_secret. SearXNG's
# mkdir -p deploy/secrets && chmod 700 deploy/secrets # searxng.env is the exception: compose reads it itself, so it is owned by
# head -c 32 /dev/urandom | base64 > deploy/secrets/secret_key # the host account that runs compose, mode 0600. Creating them: CTXD project
# printf '%s\n' '<Authentik client secret>' > deploy/secrets/oidc_client_secret # mealquest, RUNBOOKS.MD "Stack and first deploy" > "4. Secrets".
# printf '%s\n' '<Google OAuth client secret>' > deploy/secrets/google_client_secret
# sudo chown 10001:10001 deploy/secrets/* && sudo chmod 600 deploy/secrets/*
#
# SearXNG's secret key is the exception: compose reads it itself, so it
# stays owned by you (mode 0600). Create it after the chown above:
#
# (umask 077; printf 'SEARXNG_SECRET=%s\n' "$(openssl rand -hex 32)" > deploy/secrets/searxng.env)
# #
# deploy/compose.yaml mounts them at /run/secrets/<name> and points the # deploy/compose.yaml mounts them at /run/secrets/<name> and points the
# *_FILE variables at them. # *_FILE variables at them.
@@ -42,8 +37,7 @@ MEALQUEST_TRUSTED_PROXY=10.60.1.33
# IANA zone the household settings are created with on first boot. Required # IANA zone the household settings are created with on first boot. Required
# until those settings exist; ignored afterwards (change the zone in # until those settings exist; ignored afterwards (change the zone in
# household settings instead). Confirm the household's zone before the first # household settings instead).
# deploy.
MEALQUEST_DEFAULT_TZ=America/New_York MEALQUEST_DEFAULT_TZ=America/New_York
# --- Authentik OIDC ---------------------------------------------------------- # --- Authentik OIDC ----------------------------------------------------------
@@ -57,10 +51,10 @@ MEALQUEST_OIDC_ALLOWED_GROUPS=mealquest-admin
# --- Google Calendar (Push to Calendar) ------------------------------------ # --- Google Calendar (Push to Calendar) ------------------------------------
# Client ID of the Google OAuth web client (deploy/RUNBOOK.md "Google # Client ID of the Google OAuth web client (CTXD project mealquest,
# Calendar OAuth client"). Required: the service will not start without it # RUNBOOKS.MD "Google Calendar OAuth client"). Required: the service will
# and the client secret file. Its authorized redirect URI is # not start without it and the client secret file. Its authorized redirect
# <MEALQUEST_PUBLIC_URL origin>/api/v1/integrations/google/callback. # URI is <MEALQUEST_PUBLIC_URL origin>/api/v1/integrations/google/callback.
MEALQUEST_GOOGLE_CLIENT_ID=<client id>.apps.googleusercontent.com MEALQUEST_GOOGLE_CLIENT_ID=<client id>.apps.googleusercontent.com
# Optional session lifetimes. A session ends after IDLE unused and always MAX # Optional session lifetimes. A session ends after IDLE unused and always MAX
@@ -82,13 +76,14 @@ MEALQUEST_SESSION_MAX=4320h
# The SearXNG recipe-search sidecar on the compose network. The Settings # The SearXNG recipe-search sidecar on the compose network. The Settings
# screen pre-fills it as the SearXNG URL until one is saved; the saved # screen pre-fills it as the SearXNG URL until one is saved; the saved
# provider and base URL come from PATCH /api/v1/settings/recipe-search. # provider and base URL come from PATCH /api/v1/settings/recipe-search.
# See deploy/RUNBOOK.md "Recipe search (SearXNG)". # See CTXD project mealquest, RUNBOOKS.MD "Recipe search (SearXNG)".
# --- Planned; not read by the service yet ------------------------------------- # --- Planned; not read by the service yet -------------------------------------
# #
# Added to deploy/compose.yaml and this file by the features that use them. # Added to deploy/compose.yaml and this file by the features that use them.
# #
# Web Push (Feature 19). Generate the key pair with cmd/vapidgen. # Web Push (Feature 19). Key pair: CTXD project mealquest, RUNBOOKS.MD
# "VAPID keys (Web Push)".
# MEALQUEST_VAPID_PUBLIC_KEY=<base64url VAPID public key> # MEALQUEST_VAPID_PUBLIC_KEY=<base64url VAPID public key>
# MEALQUEST_VAPID_PRIVATE_KEY_FILE=/run/secrets/vapid_private_key # MEALQUEST_VAPID_PRIVATE_KEY_FILE=/run/secrets/vapid_private_key
# MEALQUEST_VAPID_SUBJECT=mailto:<owner address> # MEALQUEST_VAPID_SUBJECT=mailto:<owner address>
-1417
View File
File diff suppressed because it is too large Load Diff
+4 -5
View File
@@ -1,8 +1,7 @@
# Development override: publishes SearXNG on the dev machine's loopback so # Development override layered on deploy/compose.yaml: publishes SearXNG
# it can be queried directly, e.g. # on the dev machine's loopback (127.0.0.1:8888) so it can be queried
# # directly. Using it: CTXD project mealquest, RUNBOOKS.MD "Local
# docker compose -f deploy/compose.yaml -f deploy/compose.dev.yaml up -d searxng # development".
# curl 'http://127.0.0.1:8888/search?q=chicken+tikka+masala+recipe&format=json'
# #
# Loopback only, never a LAN address. Never used in production: the # Loopback only, never a LAN address. Never used in production: the
# systemd unit starts deploy/compose.yaml alone. # systemd unit starts deploy/compose.yaml alone.
+10 -12
View File
@@ -1,18 +1,16 @@
# Production stack for MealQuest on 10.60.1.14 (D-018). Build on the host, # Production stack for MealQuest on 10.60.1.14 (D-018). Building, deploying
# then start through systemd: # and operating it: CTXD project mealquest, RUNBOOKS.MD "Stack unit" and
# # "Routine deploy and rollback".
# docker compose -f deploy/compose.yaml build
# sudo systemctl restart mealquest
# #
# The services use restart: "no" on purpose: Docker must not start them # The services use restart: "no" on purpose: Docker must not start them
# before the DOCKER-USER firewall is applied. deploy/systemd/mealquest.service # before the DOCKER-USER firewall is applied. deploy/systemd/mealquest.service
# owns start and stop and requires mealquest-firewall.service first. # owns start and stop and requires mealquest-firewall.service first.
# #
# Configuration comes from deploy/.env (copy deploy/.env.example) and secrets # Configuration comes from deploy/.env (template: deploy/.env.example) and
# from deploy/secrets/; neither is committed. Compose bind-mounts file secrets # secrets from deploy/secrets/; neither is committed. Compose bind-mounts
# with their host owner and mode, so each file in deploy/secrets/ must be # file secrets with their host owner and mode, so each file in
# owned by the container user 10001:10001 with mode 0600 (except # deploy/secrets/ must be owned by the container user 10001:10001 with mode
# searxng.env, read by compose itself; see the searxng service). # 0600 (except searxng.env, read by compose itself; see the searxng service).
name: mealquest name: mealquest
services: services:
@@ -65,8 +63,8 @@ services:
# the app's own user (owner of the volume) with a read-only root # the app's own user (owner of the volume) with a read-only root
# filesystem, no privilege escalation and no Linux capabilities. # filesystem, no privilege escalation and no Linux capabilities.
# #
# Restore: follow "Restore the database" in deploy/RUNBOOK.md (stop the # Restoring the database: CTXD project mealquest, RUNBOOKS.MD "Restore
# stack, move the database with its -wal and -shm aside, restore, check). # the database".
litestream: litestream:
image: litestream/litestream:0.5.17 image: litestream/litestream:0.5.17
user: "10001:10001" user: "10001:10001"
+3 -2
View File
@@ -5,8 +5,9 @@
# are one-way: a failure here fails this unit visibly and never fails # are one-way: a failure here fails this unit visibly and never fails
# docker.service. # docker.service.
# #
# Start MealQuest only after this unit has succeeded: # mealquest.service requires this unit and starts the stack only after it
# systemctl start mealquest-firewall && docker compose up -d # has succeeded. Installing and operating both: CTXD project mealquest,
# RUNBOOKS.MD "Host firewall" and "Stack unit".
[Unit] [Unit]
Description=Restrict MealQuest port 8090 to Traefik (DOCKER-USER) Description=Restrict MealQuest port 8090 to Traefik (DOCKER-USER)
After=docker.service After=docker.service
@@ -1,8 +1,7 @@
# Runs the nightly MealQuest media backup (/usr/local/sbin/mealquest-media-backup). # Runs the nightly MealQuest media backup (/usr/local/sbin/mealquest-media-backup).
# Triggered by mealquest-media-backup.timer; run by hand with # Triggered by mealquest-media-backup.timer; output goes to the journal.
# systemctl start mealquest-media-backup # Installing it and running it by hand: CTXD project mealquest,
# and read the result with # RUNBOOKS.MD "Media — nightly rsync".
# journalctl -u mealquest-media-backup
# #
# The script runs as the app's user (10001, host user "mealquest") with no # The script runs as the app's user (10001, host user "mealquest") with no
# capabilities. systemd, as root, only bind-mounts the data volume's root # capabilities. systemd, as root, only bind-mounts the data volume's root
+3 -2
View File
@@ -15,8 +15,9 @@
# swapping the path afterwards cannot redirect it. Symlinks inside the # swapping the path afterwards cannot redirect it. Symlinks inside the
# directory are copied as links (-a), never followed. # directory are copied as links (-a), never followed.
# #
# The media directory is created at deploy time (deploy/RUNBOOK.md). If it # The media directory is created at deploy time (CTXD project mealquest,
# is missing the job fails; it is never treated as "nothing to back up". # RUNBOOKS.MD "Media — nightly rsync"). If it is missing the job fails; it
# is never treated as "nothing to back up".
# #
# rsync mirrors into .../mealquest-backups/media/ only: the target ends in # rsync mirrors into .../mealquest-backups/media/ only: the target ends in
# a slash, so --delete removes files from that directory and nothing # a slash, so --delete removes files from that directory and nothing