From 1fe712e7fce1b149455ce24780c1a983b8f70700 Mon Sep 17 00:00:00 2001 From: stroblme Date: Sat, 22 Aug 2026 09:25:02 +0200 Subject: [PATCH] Add make dev-lan: the app on a host port, API proxied on the same origin --- Makefile | 11 ++++++++++- NOTEPAD.md | 8 ++++++++ docker/compose.lan.yml | 19 +++++++++++++++++++ docker/nginx-api-proxy.conf | 32 ++++++++++++++++++++++++++++++++ 4 files changed, 69 insertions(+), 1 deletion(-) create mode 100644 docker/compose.lan.yml create mode 100644 docker/nginx-api-proxy.conf diff --git a/Makefile b/Makefile index 2e7cd8b..6b4414e 100644 --- a/Makefile +++ b/Makefile @@ -2,7 +2,7 @@ # Convenience targets for development, testing, linting, and deployment. # The workspace root delegates to these (see ../Makefile). -.PHONY: dev-utils dev dev-local up down update install dev-backend dev-frontend \ +.PHONY: dev-utils dev dev-local dev-lan up down update install dev-backend dev-frontend \ generate-client seed-example seed-demo seed-house seed-aircon seed-hosted-demo test test-backend test-frontend soak bench-startup lint lint-backend \ lint-frontend format-frontend umami build docs docs-serve clean help @@ -17,9 +17,14 @@ COMPOSE_PROD := $(COMPOSE) -f docker/compose.yml # Production also runs autoheal, which restarts the backend when its deep # health check fails. It is profile-gated because it mounts the Docker socket. COMPOSE_PROD_RUN := $(COMPOSE_PROD) --profile autoheal +# Host port `make dev-lan` publishes the frontend on. +APP_PORT ?= 8080 COMPOSE_DEV := $(COMPOSE_PROD) -f docker/compose.dev.yml # Integrated local stack: dev stack wired onto the shared `proxy` network. COMPOSE_LOCAL := $(COMPOSE_DEV) -f docker/compose.local.yml +# Same stack, with the frontend also published on APP_PORT and proxying the API +# there, for clients that cannot resolve app.$(DOMAIN). +COMPOSE_LAN := $(COMPOSE_LOCAL) -f docker/compose.lan.yml help: ## Show available targets @awk 'BEGIN{FS=":.*?## "} /^[a-zA-Z_-]+:.*?##/ {printf " \033[36m%-18s\033[0m %s\n", $$1, $$2}' $(MAKEFILE_LIST) @@ -36,6 +41,10 @@ dev-local: ## Start the integrated local stack (called by the root `make dev`) DOMAIN=$${DOMAIN:-localhost} ENVIRONMENT=$${ENVIRONMENT:-local} \ $(COMPOSE_LOCAL) up --build -d proxy backend frontend mailcatcher +dev-lan: ## Same, plus the app on http://:$(APP_PORT) (no DNS needed) + DOMAIN=$${DOMAIN:-localhost} ENVIRONMENT=$${ENVIRONMENT:-local} APP_PORT=$(APP_PORT) \ + $(COMPOSE_LAN) up --build -d proxy backend frontend mailcatcher + up: ## Start the production stack $(COMPOSE_PROD_RUN) up --build -d diff --git a/NOTEPAD.md b/NOTEPAD.md index 1f55a6d..827b36e 100644 --- a/NOTEPAD.md +++ b/NOTEPAD.md @@ -289,3 +289,11 @@ Open on purpose. Each names what should bring it back. container `fluksio-app-proxy-1`. With `REVERSE_PROXY=external` and no host proxy, the whole stack then has nothing bound to :80 and `*.localhost` stops resolving. Either drop `--remove-orphans` or make `update` refuse when `ENVIRONMENT=local`. + +- CHORE: `frontend/vite.config.js` and `frontend/vite.config.d.ts` are committed build + output of `vite.config.ts` and fail `biome check` as tracked (semicolons, 4-space + indent). The pre-commit hook therefore fails for anyone who stages any other file + under `frontend/`. Either gitignore the two or run the formatter over them once. +- CHORE: the pre-commit biome hook selects files by the `frontend/` prefix rather than by + extension, so a non-JS file placed there is handed to biome and blows up with an + internal error on a doubled path (`frontend/frontend/...`). diff --git a/docker/compose.lan.yml b/docker/compose.lan.yml new file mode 100644 index 0000000..6dcea53 --- /dev/null +++ b/docker/compose.lan.yml @@ -0,0 +1,19 @@ +# Direct LAN access — layered on top of compose.local.yml by `make dev-lan`. +# +# For reaching the app from a machine that cannot be given a DNS record or a +# hosts entry. Traefik routes on the Host header and a bare IP matches no +# router, so the frontend is published on a host port instead and proxies the +# API on that same port: http://:${APP_PORT} is the whole app. +services: + + frontend: + ports: !override + - "${APP_PORT:-8080}:80" + volumes: + - ./nginx-api-proxy.conf:/etc/nginx/extra-conf.d/backend-not-found.conf:ro + build: + args: + # Empty on purpose: the client then addresses the API as a path on + # whichever origin served the page, so one build works through + # app.${DOMAIN}, the published port, and an ssh tunnel alike. + - VITE_API_URL= diff --git a/docker/nginx-api-proxy.conf b/docker/nginx-api-proxy.conf new file mode 100644 index 0000000..cf93bdc --- /dev/null +++ b/docker/nginx-api-proxy.conf @@ -0,0 +1,32 @@ +# Serves the API from the same origin as the SPA, replacing the 404 stub the +# image ships. Traefik routes on the Host header, which a bare IP never +# matches, so this is what makes http://: a complete app: the +# client asks for /api/v1/... on whatever origin served it, and never needs a +# resolvable api.${DOMAIN} or a CORS allowance. + +location /api { + # Docker's embedded DNS, re-resolved: nginx otherwise pins the backend's + # address at startup and 502s for good once that container is recreated. + resolver 127.0.0.11 valid=10s ipv6=off; + set $backend_upstream http://backend:8000; + proxy_pass $backend_upstream$request_uri; + + proxy_http_version 1.1; + proxy_set_header Host $host; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + # The flow websocket (/api/v1/flows/ws) upgrades through here; passing the + # client's own Connection header covers both it and ordinary requests. + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection $http_connection; + # A live socket outlives the 60s default by design. + proxy_read_timeout 1d; +} + +# Unchanged from the stub: the API docs stay off the frontend's origin. +location /docs { + return 404; +} +location /redoc { + return 404; +}