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.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-16 11:40:02 +02:00
co-authored by Claude Opus 5.5
parent 29aca91978
commit 48aac3855b
39 changed files with 573 additions and 728 deletions
+19 -19
View File
@@ -7,7 +7,8 @@ How to run, test and lint the `app` stack.
**Integrated — the normal way.** From the workspace root, once: `make init` (submodules,
secrets, the per-stack `.env` files, the shared external `proxy` docker network). Then
`make dev` brings both stacks up detached behind a single Traefik — which is what lets the
SPA, the API and the marketing site answer on one port. `make status` lists the containers,
app and the marketing site answer on one port. The app is one container
(`backend/Dockerfile`): the engine, its API and the built dashboard on port 8000. `make status` lists the containers,
`make down` stops everything.
The hostname comes from `DOMAIN` in `app/.env`, which `scripts/setup.sh` copies out of the
@@ -22,6 +23,11 @@ port 80. `make dev` starts this stack on its own Traefik with all host ports
published, in the foreground. `make dev-utils` starts just proxy and mailcatcher — the
useful half when the backend runs on the host.
**Single container.** `docker compose up -d` in this repo's root uses `compose.yml`, the
setup the getting-started guide gives: the image on `localhost:8080` with its own Redis, no
Traefik, no `.env`. It is a separate project from the stack below and shares nothing
with it.
**No Docker.** `make install` once, then `make dev-backend` (FastAPI on :8000) and
`make dev-frontend` (Vite on :5173) in two terminals. The backend needs nothing running: its
database is SQLite in `DATA_DIR` (`backend/flow-data` by default), created on first start.
@@ -45,9 +51,9 @@ already being taken would stop the whole stack from starting.
| URL | Service |
|---|---|
| http://app.localhost | dashboard SPA |
| http://api.localhost | backend API |
| http://api.localhost/docs | OpenAPI / Swagger UI |
| http://app.localhost | dashboard, with the API under `/api/v1` |
| http://api.localhost | the same container, for callers configured with the API host |
| http://app.localhost/docs | OpenAPI / Swagger UI |
| http://localhost | marketing site (the `index` stack) |
Mailcatcher has no route here, so use the container directly:
@@ -59,8 +65,7 @@ Standalone (`cd app && make dev`) publishes the full set:
| URL | Service |
|---|---|
| http://app.localhost, http://api.localhost | this stack's own Traefik on :80 |
| http://localhost:5173 | SPA (the nginx image, not the Vite dev server) |
| http://localhost:8000 | backend API |
| http://localhost:8000 | dashboard and API |
| http://localhost:8090 | Traefik dashboard |
| http://localhost:1080 | mailcatcher web UI (SMTP on 1025) |
@@ -74,14 +79,9 @@ syncs the same directories, but only under `docker compose watch`, which no targ
before the mount existed keeps serving the source baked into its image and has to be
recreated once.
The frontend is built into an nginx image, so a UI change needs an explicit rebuild, from
`app/`:
```sh
docker compose -p fluksio-app --env-file "$PWD/.env" \
-f docker/compose.yml -f docker/compose.dev.yml -f docker/compose.local.yml \
up --build -d frontend
```
The dashboard is built into the app image, so a UI change needs a rebuild:
`make rebuild-frontend` from `app/`. Only the image's frontend stage runs again, and the
container is recreated with the domain and overlays the running one was started with.
## Services the dev stack adds
@@ -154,8 +154,8 @@ The specs are `frontend/tests/*.spec.ts`; `playwright.config.ts` defines `setup`
in once into `playwright/.auth/user.json`, which the browser projects reuse; `mobile` runs
`mobile.spec.ts` only, on a Pixel 5 at 393px.
Both origins come from `PLAYWRIGHT_BASE_URL` and `PLAYWRIGHT_API_URL`, deliberately never
from `VITE_API_URL` — that one belongs to the app build and in a deployment checkout it
The origin comes from `PLAYWRIGHT_BASE_URL`, with the API on it unless `PLAYWRIGHT_API_URL`
names another, deliberately never from `VITE_API_URL` — that one belongs to the app build and in a deployment checkout it
names the deployment, which would mean driving a browser at the local stack while sending
teardown `DELETE`s to the live instance. `tests/guard.ts` runs first and refuses the whole
run when either origin resolves outside loopback or the private ranges, because the suite
@@ -166,7 +166,7 @@ creates and deletes flows, dashboards and users. `PLAYWRIGHT_ALLOW_PUBLIC=1` ove
always happens in the Playwright container, which pins the browser by reading
`frontend/package.json`. Running Playwright on the host is unsupported — the workspace pins a
browser revision whose install is incomplete. The domain comes off the running stack (the
frontend container's Traefik rule), so the shots are of whatever `make dev` last started.
app container's Traefik rule), so the shots are of whatever `make dev` last started.
## Lint, hooks and the generated client
@@ -186,7 +186,7 @@ contract between the two frontends, and that target is the only thing checking i
`.gitea/workflows/` runs four jobs:
`test-backend.yml` (pytest on Python 3.12 and 3.14, plus a check that both wheels build), `playwright.yml` (the e2e suite in
two shards, one compose project each, the browser loading the SPA from the nginx `frontend`
two shards, one compose project each, the browser loading the SPA from the `backend`
service), `pre-commit.yml` (the hooks above, plus strict mypy, which they do not cover) and
`test-compose.yml` (a smoke test that the production images actually come up). All but
`test-compose.yml` (a smoke test that the production image actually comes up). All but
`test-backend.yml` write `.env` from `.env.example` first. The backend suite needs none.