make: add root install/up/down for the local stack
All checks were successful
secrets-guard / encrypted (pull_request) Successful in 5s
shell-lint / shellcheck (pull_request) Successful in 7s
PR build (required check) / changes (pull_request) Successful in 19s
PR build (required check) / build-backend (pull_request) Has been skipped
PR build (required check) / build-frontend (pull_request) Has been skipped
PR build (required check) / validate-observability (pull_request) Has been skipped
PR build (required check) / gate (pull_request) Successful in 1s
Sync infra to hosts / sync-beta (push) Has been skipped
Sync infra to hosts / sync-prod (push) Has been skipped
Sync infra to hosts / sync-centralis (push) Has been skipped
secrets-guard / encrypted (push) Successful in 6s
Sync infra to hosts / sync-dev (push) Successful in 12s
shell-lint / shellcheck (push) Successful in 8s

A fresh checkout had no working path to the stack locally. Neither compose
file carries a `build:` for backend or frontend -- each ships as its own
registry image -- so with no image present `make dev-up` fell through to
pulling the `:local` tag, which nothing publishes, and failed with 403
against a registry a laptop has no login for.

`install` builds both images from backend/Dockerfile and frontend/Dockerfile
under exactly the tag the compose files default to; `up` verifies they exist
and says what to run instead of retrying the pull. Image names are read back
from `docker compose config --images` rather than restated here, so an org
rename cannot leave this file building a tag nothing runs.

infra's dev-up/dev-down now set COMPOSE_PROJECT_NAME=thermograph-dev, which
matches deploy/deploy-dev.sh and what the root CLAUDE.md already described;
a local run previously landed in the prod-shaped `thermograph` project. Drop
dev-up's `--build`, a no-op since the Dockerfiles moved out of infra, and
correct the comment claiming a 0.0.0.0 default -- the overlay has published
on loopback since dev moved to a public VPS.
This commit is contained in:
Emi Griffith 2026-08-01 13:14:32 -07:00
parent 9f9c436424
commit 4dcd6775c4
4 changed files with 129 additions and 20 deletions

View file

@ -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`

81
Makefile Normal file
View file

@ -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 <service>` 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

View file

@ -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`,

View file

@ -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