One image for the whole app: engine, API and dashboard on one port

The backend image now builds the SPA and serves it on the API's own origin
(fluksio/spa.py), so an instance is a single container with no proxy, DNS,
port 80 or .env: `docker compose up -d` with the new compose.yml runs it on
localhost:8080. It starts on fluksio serve (persisted secret key, first
admin printed once) and brings its own Redis when REDIS_HOST is unset.

Slimmer: python:3.13-slim, no dev group, uv once, fastapi without the cloud
CLI. Optional features are per-feature extras (mqtt, influx, email, sentry,
ml, server for all) picked with the EXTRAS build argument; numpy moved to
`ml`. ffmpeg left the image for imageio-ffmpeg via the Modules screen.

The operator stack runs the same image with app. and api. routed to it; the
nginx frontend service, the LAN nginx proxy and DOMAIN in the backend are
gone, and the MCP issuer is FRONTEND_HOST.
This commit is contained in:
2026-10-16 11:40:02 +02:00
parent 9d9dac0bd0
commit dea25ec08d
39 changed files with 573 additions and 728 deletions
+18 -22
View File
@@ -17,13 +17,13 @@ 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.
# Host port `make dev-lan` publishes the app 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).
# Same stack, with the app also published on APP_PORT, for clients that cannot
# resolve app.$(DOMAIN).
COMPOSE_LAN := $(COMPOSE_LOCAL) -f docker/compose.lan.yml
# The frontend suite's own stack, a project of its own behind the local Traefik
# (compose.test.yml). The hostname is fixed, never read from a file or the
@@ -79,7 +79,7 @@ dev: ## Start the full dev stack (includes a local Traefik proxy)
dev-local: ## Start the local stack at http://app.localhost (also run by the root `make dev`)
DOMAIN=$${DOMAIN:-localhost} ENVIRONMENT=$${ENVIRONMENT:-local} \
$(COMPOSE_LOCAL) up --build -d proxy backend frontend docs mailcatcher
$(COMPOSE_LOCAL) up --build -d --remove-orphans proxy backend docs mailcatcher
# The host's LAN address is the source of its default route. Without `ip`
# (macOS) it stays empty and the instance keeps calling itself app.$(DOMAIN).
@@ -87,21 +87,18 @@ dev-lan: ## Same, plus the app on http://<host-ip>:$(APP_PORT) (no DNS needed)
@ip=$$(ip route get 1.1.1.1 2>/dev/null | sed -n 's/.* src \([0-9.]*\).*/\1/p'); \
DOMAIN=$${DOMAIN:-localhost} ENVIRONMENT=$${ENVIRONMENT:-local} APP_PORT=$(APP_PORT) \
LAN_URL=$${ip:+http://$$ip:$(APP_PORT)} \
$(COMPOSE_LAN) up --build -d proxy backend frontend mailcatcher
$(COMPOSE_LAN) up --build -d --remove-orphans proxy backend mailcatcher
# The frontend is an nginx image, so a UI change needs a rebuild. Both the
# domain and the overlay set come off the *running* stack rather than from a
# file: VITE_API_URL is baked in at build time, so rebuilding under a different
# domain — or without the lan overlay a stack was started with — leaves the SPA
# calling an API that answers elsewhere. The lan overlay is what publishes the
# host port and builds with an empty VITE_API_URL, so dropping it takes a wall
# panel reaching http://<host-ip>:$(APP_PORT) off the air entirely and bakes a
# name it cannot resolve into the bundle. A published host port is how that
# overlay is recognised, because it is the thing only that overlay adds.
rebuild-frontend: ## Rebuild and restart the local frontend (after a UI change)
@domain=$$(docker inspect fluksio-app 2>/dev/null \
# The dashboard is built into the app image, so a UI change needs a rebuild;
# only the image's frontend stage runs again. The domain and the overlay set
# come off the *running* stack rather than from a file, so the rebuilt
# container keeps the hostname and, on a LAN stack, the published port it was
# started with. A published host port is how the lan overlay is recognised,
# because it is the thing only that overlay adds.
rebuild-frontend: ## Rebuild the app image and restart it (after a UI change)
@domain=$$(docker inspect fluksio-api 2>/dev/null \
| grep -o 'Host(`app\.[^`]*`)' | head -1 | sed 's/Host(`app\.//;s/`)//'); \
port=$$(docker inspect fluksio-app \
port=$$(docker inspect fluksio-api \
--format '{{range $$p, $$c := .NetworkSettings.Ports}}{{range $$c}}{{.HostPort}} {{end}}{{end}}' \
2>/dev/null | awk '{print $$1}'); \
if [ -n "$$port" ]; then \
@@ -112,7 +109,7 @@ rebuild-frontend: ## Rebuild and restart the local frontend (after a UI change)
fi; \
DOMAIN=$${domain:-$$DOMAIN} ENVIRONMENT=$${ENVIRONMENT:-local} \
APP_PORT=$${port:-$(APP_PORT)} \
$$compose up --build -d frontend
$$compose up --build -d backend
up: ## Start the production stack
$(COMPOSE_PROD_RUN) up --build -d
@@ -204,14 +201,13 @@ PW_VERSION = $(shell sed -n 's/.*"@playwright\/test": "[^0-9]*\([0-9.]*\)".*/\1/
test-stack: ## Start or update the frontend suite's own stack at http://app.test.localhost
@docker ps -q -f name='^fluksio-app-proxy-1$$' | grep -q . \
|| { echo " ✗ the local Traefik is not running, 'make dev' first"; exit 1; }
$(COMPOSE_TEST) up --build -d --wait redis backend frontend
$(COMPOSE_TEST) up --build -d --wait --remove-orphans redis backend
# The Playwright container, minus its network. tests/guard.ts refuses a public
# address on top of the fixed hostname.
PW_RUN = docker run --rm --ipc=host --user $$(id -u):$$(id -g) -e HOME=/tmp \
-v $(COMPOSE_ROOT):/app -w /app/frontend \
-e PLAYWRIGHT_BASE_URL=http://app.$(TEST_DOMAIN) \
-e PLAYWRIGHT_API_URL=http://api.$(TEST_DOMAIN) \
-e CI=$${CI:-1} \
-e FIRST_SUPERUSER="$$(sed -n 's/^FIRST_SUPERUSER=//p' $(COMPOSE_ROOT)/.env | head -1)" \
-e FIRST_SUPERUSER_PASSWORD="$$(sed -n 's/^FIRST_SUPERUSER_PASSWORD=//p' $(COMPOSE_ROOT)/.env | head -1)"
@@ -225,8 +221,8 @@ test-frontend: test-stack ## Run frontend tests (Playwright e2e) against the te
2>/dev/null | cut -d/ -f1); \
[ -n "$$ip" ] || { echo " ✗ the local Traefik is not on the proxy network"; exit 1; }; \
$(PW_RUN) --network proxy \
--add-host app.$(TEST_DOMAIN):$$ip --add-host api.$(TEST_DOMAIN):$$ip \
-e HOST_RESOLVER_RULES="MAP app.$(TEST_DOMAIN) $$ip, MAP api.$(TEST_DOMAIN) $$ip" \
--add-host app.$(TEST_DOMAIN):$$ip \
-e HOST_RESOLVER_RULES="MAP app.$(TEST_DOMAIN) $$ip" \
$(PW_IMAGE) npx playwright test $(PLAYWRIGHT_ARGS)
# WebKit takes *.localhost to loopback through GLib and ignores /etc/hosts, so