diff --git a/CLAUDE.md b/CLAUDE.md index 13c3446..f81e631 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -34,8 +34,10 @@ own Postgres container, reachable only from vps1 itself (`127.0.0.1:8137` — reach it either; use an SSH tunnel): no public DNS record, no Caddy site, no TLS. It is deployed by CI like beta and prod. The desktop hosts no Thermograph environment at all; -`make dev-up` there is a laptop convenience for running the stack locally, not -a deployment target. +the root `Makefile` (`make install` once, then `make up` / `make down`, wrapping +`infra`'s `dev-up`/`dev-down`) is a laptop convenience for running the stack +locally, not a deployment target. `install` is what builds the `:local` image +tags those compose files default to — nothing publishes them. **One workflow deploys everything.** `deploy.yml` handles both services and all three environments: the branch selects the environment (`dev` → vps1, `main` → diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..c834372 --- /dev/null +++ b/Makefile @@ -0,0 +1,81 @@ +# Laptop-local entry point: build the two service images, run the dev stack. +# +# No fleet host uses this file. dev, beta and prod all deploy through +# infra/deploy/deploy.sh, which pulls registry images by their sha-<12hex> tag. +# A laptop has no registry login, so `install` BUILDS both images under the +# `:local` tag that infra/docker-compose.yml already falls back to +# (BACKEND_IMAGE_TAG / FRONTEND_IMAGE_TAG default to `local`). Without that +# build step `up` would try to PULL `:local`, which is published nowhere and +# fails with a 403 -- so `up` checks for the images first and says what to run. +# +# This wraps infra's own compose targets; `make -C infra dev-up` remains +# equivalent to `make up` here, same project name, same overlay, same stack. + +SHELL := /bin/bash + +# Keeps this stack's volumes off the prod-shaped `thermograph` project that +# infra/docker-compose.yml pins by default (the env var always wins over the +# file's `name:` key). +COMPOSE_PROJECT_NAME ?= thermograph-dev +POSTGRES_PASSWORD ?= thermograph-dev +# The daemon refuses to start without this, and the backend answers 404 on the +# whole /internal/* surface while it is unset -- both fail closed. A throwaway +# value is all a laptop needs; nothing outside this stack accepts it. +THERMOGRAPH_INTERNAL_TOKEN ?= dev-token +export COMPOSE_PROJECT_NAME POSTGRES_PASSWORD THERMOGRAPH_INTERNAL_TOKEN + +# The dev overlay publishes backend on $DEV_BIND_ADDR, default loopback. Set +# DEV_BIND_ADDR=0.0.0.0 to reach it from phones on your own Wi-Fi -- safe on a +# laptop LAN, never on a fleet host (see infra/docker-compose.dev.yml). +COMPOSE := docker compose -f infra/docker-compose.yml -f infra/docker-compose.dev.yml + +# Ask compose what it will run rather than restating the image names here -- +# REGISTRY_HOST / *_IMAGE_PATH / *_IMAGE_TAG all have defaults in +# infra/docker-compose.yml, and a second copy of them in this file is a rename +# away from silently building a tag nothing runs. `--images ` also +# lists that service's dependencies, hence the per-service filter. Override by +# passing BACKEND_IMAGE=... / FRONTEND_IMAGE=... if you need a one-off tag. +# POSTGRES_PASSWORD is inlined because `config` refuses to interpolate without +# it, and $(shell) does not see make's `export` directives. +CONFIG_ENV := POSTGRES_PASSWORD=$(POSTGRES_PASSWORD) +ifndef BACKEND_IMAGE +BACKEND_IMAGE := $(shell $(CONFIG_ENV) $(COMPOSE) config --images backend 2>/dev/null | grep -m1 '/backend:') +endif +ifndef FRONTEND_IMAGE +FRONTEND_IMAGE := $(shell $(CONFIG_ENV) $(COMPOSE) config --images frontend 2>/dev/null | grep -m1 '/frontend:') +endif + +.DEFAULT_GOAL := help +.PHONY: help install up down logs ps check-images-resolved + +help: ## List targets + @grep -hE '^[a-z-]+:.*?## ' $(MAKEFILE_LIST) \ + | awk -F':.*?## ' '{printf " \033[36m%-8s\033[0m %s\n", $$1, $$2}' + +# Empty means `docker compose config` itself failed -- a broken compose file or +# no docker at all. Fail here rather than running `docker build -t ""`. +check-images-resolved: + @[[ -n "$(BACKEND_IMAGE)" && -n "$(FRONTEND_IMAGE)" ]] || { \ + echo "could not resolve image names from infra/docker-compose.yml;" >&2; \ + echo "check 'docker compose -f infra/docker-compose.yml config'" >&2; exit 1; } + +install: check-images-resolved ## Build backend + frontend images as :local (run once before `up`) + docker build -t $(BACKEND_IMAGE) backend + docker build -t $(FRONTEND_IMAGE) frontend + +up: check-images-resolved ## Start the local dev stack, uncapped, backend on 127.0.0.1:8137 + @for img in $(BACKEND_IMAGE) $(FRONTEND_IMAGE); do \ + docker image inspect "$$img" >/dev/null 2>&1 || { \ + echo "missing image $$img -- run 'make install' first" >&2; exit 1; }; \ + done + $(COMPOSE) up -d + @echo "backend: http://$${DEV_BIND_ADDR:-127.0.0.1}:8137" + +down: ## Stop the stack (named volumes persist) + $(COMPOSE) down + +logs: ## Follow the stack's logs (SERVICE=backend to narrow) + $(COMPOSE) logs -f $(SERVICE) + +ps: ## Show the stack's containers + $(COMPOSE) ps diff --git a/docs/onboarding/02-setup.md b/docs/onboarding/02-setup.md index 3ea84ee..59a715f 100644 --- a/docs/onboarding/02-setup.md +++ b/docs/onboarding/02-setup.md @@ -169,21 +169,37 @@ ends fail closed. With Discord unconfigured it logs once and runs cron-only. ## The full stack, locally +From the repo root: + ```bash -cd infra -make dev-up # docker-compose.yml + docker-compose.dev.yml overlay: - # uncapped CPU, backend published on 0.0.0.0:8137 for your LAN -make dev-down +make install # build both images as :local -- required once, see below +make up # docker-compose.yml + docker-compose.dev.yml overlay: + # uncapped CPU, backend on 127.0.0.1:8137 +make down ``` +`make install` is not optional on a fresh checkout. Neither compose file +carries a `build:` for backend or frontend — each ships as its own registry +image — so with no image present compose tries to **pull** +`git.thermograph.org/emi/thermograph/backend:local`, a tag published nowhere, +and fails with `403 Forbidden` against a registry you have no login for. +`install` builds that tag locally from `backend/Dockerfile` and +`frontend/Dockerfile`. `make up` checks both images exist and points you here +if they don't. + +`make -C infra dev-up` / `dev-down` remain equivalent to `up` / `down` — same +overlay, same project name, same stack. + +Backend publishes on `${DEV_BIND_ADDR}`, **loopback by default**. Pass +`DEV_BIND_ADDR=0.0.0.0 make up` to reach it from phones on your own Wi-Fi — +fine on a laptop LAN, never on a fleet host. + This is a **laptop convenience**, not the hosted dev environment: the actual `dev` environment now runs on vps1 with its own Postgres, deployed by CI, and bound only to the WireGuard mesh (`10.10.0.2:8137`) — never `0.0.0.0`, since -vps1 is a public box. See [Infra and secrets](08-infra-secrets.md). A local -`make dev-up` is fine on `0.0.0.0` because it's your own machine's LAN, not the -public internet. +vps1 is a public box. See [Infra and secrets](08-infra-secrets.md). -The dev overlay exports `COMPOSE_PROJECT_NAME=thermograph-dev` so this stack +Both entry points set `COMPOSE_PROJECT_NAME=thermograph-dev` so this stack keeps volumes separate from anything else. **Do not remove either half of the project-name pinning** — `infra/docker-compose.yml` pins `name: thermograph`, and without it running compose from `infra/` derives the project name `infra`, diff --git a/infra/Makefile b/infra/Makefile index 2feaa42..aa4080e 100644 --- a/infra/Makefile +++ b/infra/Makefile @@ -30,20 +30,30 @@ db-up: db-down: docker compose stop db -# The dev overlay: uncapped (no CPU limits). Default publish is 0.0.0.0:8137, -# right for a laptop-local rehearsal (e.g. phones on the same Wi-Fi) -- this is -# also the *fleet* dev environment's own overlay, but that environment now runs -# on vps1 (a public VPS, not a LAN box) and deploys via deploy/deploy-dev.sh, -# which overrides the bind address to the WireGuard mesh IP only -# (DEV_BIND_ADDR, see deploy/env-topology.sh) -- never invoke `make dev-up` -# directly on a fleet host, or it publishes on every interface. (The base file -# is prod/beta's shape: capped + loopback, fronted by their own Caddy LB.) +# The dev overlay: uncapped (no CPU limits), backend published on +# ${DEV_BIND_ADDR}, which docker-compose.dev.yml defaults to loopback. Set +# DEV_BIND_ADDR=0.0.0.0 for a laptop-local rehearsal reachable from phones on +# the same Wi-Fi. This is also the *fleet* dev environment's own overlay, but +# that environment runs on vps1 (a public VPS, not a LAN box) and deploys via +# deploy/deploy-dev.sh, which pins the bind address to the WireGuard mesh IP +# (see deploy/env-topology.sh) -- never invoke `make dev-up` directly on a +# fleet host. (The base file is prod/beta's shape: capped + loopback, fronted +# by their own Caddy LB.) +# +# COMPOSE_PROJECT_NAME matches deploy/deploy-dev.sh, so a laptop's dev stack +# keeps its volumes off the `thermograph` project that docker-compose.yml pins +# by default. No `--build`: neither compose file carries a `build:` for backend +# or frontend, so the images must exist first -- the root Makefile's `install` +# builds them under the `:local` tag these files default to. DEV_COMPOSE = docker compose -f docker-compose.yml -f docker-compose.dev.yml dev-up: - POSTGRES_PASSWORD=$${POSTGRES_PASSWORD:-thermograph-dev} $(DEV_COMPOSE) up -d --build + COMPOSE_PROJECT_NAME=$${COMPOSE_PROJECT_NAME:-thermograph-dev} \ + POSTGRES_PASSWORD=$${POSTGRES_PASSWORD:-thermograph-dev} \ + THERMOGRAPH_INTERNAL_TOKEN=$${THERMOGRAPH_INTERNAL_TOKEN:-dev-token} \ + $(DEV_COMPOSE) up -d dev-down: - $(DEV_COMPOSE) down + COMPOSE_PROJECT_NAME=$${COMPOSE_PROJECT_NAME:-thermograph-dev} $(DEV_COMPOSE) down # Self-hosted Open-Meteo (prod-only overlay). Serves the ERA5 archive from object # storage so the app is off the public archive API. See deploy/openmeteo/README.md