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
+73 -41
View File
@@ -1,47 +1,44 @@
FROM python:3.13
# The whole instance in one image: engine, API and dashboard on port 8000.
#
# docker build -f backend/Dockerfile -t fluksio .
# docker run -d -p 8080:8000 -v fluksio-data:/data fluksio
#
# The build context is the app repository root. EXTRAS picks the optional
# features (mqtt, influx, email, sentry, ml, or server for all of them); the
# default is the minimal engine.
ENV PYTHONUNBUFFERED=1
# ── Dashboard ────────────────────────────────────────────────────
FROM oven/bun:1 AS frontend
# Install uv
# Ref: https://docs.astral.sh/uv/guides/integration/docker/#installing-uv
COPY --from=ghcr.io/astral-sh/uv:0.9.26 /uv /uvx /bin/
WORKDIR /app
COPY package.json bun.lock /app/
COPY frontend/package.json /app/frontend/
WORKDIR /app/frontend
RUN bun install
# Compile bytecode
# Ref: https://docs.astral.sh/uv/guides/integration/docker/#compiling-bytecode
ENV UV_COMPILE_BYTECODE=1
COPY ./frontend /app/frontend
# Empty: the dashboard calls the API as a path on whichever origin served it,
# so one build works behind any port, hostname or proxy.
ENV VITE_API_URL=
RUN bun run build
# uv Cache
# Ref: https://docs.astral.sh/uv/guides/integration/docker/#caching
ENV UV_LINK_MODE=copy
# ── Python environment ───────────────────────────────────────────
FROM python:3.13-slim AS backend
# Ref: https://docs.astral.sh/uv/guides/integration/docker/
COPY --from=ghcr.io/astral-sh/uv:0.9.26 /uv /bin/
ENV UV_COMPILE_BYTECODE=1 UV_LINK_MODE=copy
WORKDIR /app/
# Place executables in the environment at the front of the path
# Ref: https://docs.astral.sh/uv/guides/integration/docker/#using-the-environment
ENV PATH="/app/.venv/bin:$PATH"
ARG EXTRAS=""
# /app/.venv holds the app and nothing of anybody else's, so there is nothing
# here to adopt: node code gets a venv of its own on the data volume, which is
# what the Modules screen installs into. A `pip install fluksio` into an
# environment somebody already works in is the case that adopts instead.
ENV NODE_VENV=managed
# ffmpeg for node code that transcodes, such as the clip recipe in
# docs/code/nodes.md. Off by default: Debian's package and what it pulls in add
# about 400 MB. Set WITH_FFMPEG=true in .env and rebuild to include it.
ARG WITH_FFMPEG=false
RUN if [ "$WITH_FFMPEG" = true ]; then \
apt-get update \
&& apt-get install -y --no-install-recommends ffmpeg \
&& rm -rf /var/lib/apt/lists/*; \
fi
# Install dependencies
# Ref: https://docs.astral.sh/uv/guides/integration/docker/#intermediate-layers
# Dependencies first, so a source change does not reinstall them.
RUN --mount=type=cache,target=/root/.cache/uv \
--mount=type=bind,source=uv.lock,target=uv.lock \
--mount=type=bind,source=pyproject.toml,target=pyproject.toml \
uv sync --frozen --no-install-workspace --package fluksio --extra server
uv sync --frozen --no-dev --no-install-workspace --package fluksio \
$(for e in $(echo "$EXTRAS" | tr , ' '); do printf -- '--extra %s ' "$e"; done)
COPY ./backend/scripts /app/backend/scripts
@@ -57,12 +54,11 @@ COPY ./backend/fluksio /app/backend/fluksio
# node code with.
COPY ./worker /app/worker
# Sync the project
# Ref: https://docs.astral.sh/uv/guides/integration/docker/#intermediate-layers
RUN --mount=type=cache,target=/root/.cache/uv \
--mount=type=bind,source=uv.lock,target=uv.lock \
--mount=type=bind,source=pyproject.toml,target=pyproject.toml \
uv sync --frozen --package fluksio --extra server
uv sync --frozen --no-dev --package fluksio \
$(for e in $(echo "$EXTRAS" | tr , ' '); do printf -- '--extra %s ' "$e"; done)
# Connectors are ordinary installed packages found through the
# `fluksio.node_types` entry point. `make connectors` builds them into here;
@@ -73,9 +69,45 @@ RUN --mount=type=cache,target=/root/.cache/uv \
&& uv pip install --python /app/.venv/bin/python /tmp/connector-wheels/*.whl \
|| echo "no connectors bundled"'
WORKDIR /app/backend/
# ── Runtime ──────────────────────────────────────────────────────
FROM python:3.13-slim
# Single worker on purpose: the process hosts the flow engine, and a second
# worker would be a second engine — duplicated subscriptions, cron ticks and
# webhooks. Scaling out is the M5 worker split, not more uvicorn processes.
CMD ["fastapi", "run", "fluksio/main.py"]
# git versions the flows. redis-server keeps message state when no external
# Redis is named. tini reaps the python node workers that outlive their pool,
# which would otherwise pile up as zombies under PID 1. procps is uptime, ps
# and free for Command nodes, the demo's among them.
RUN apt-get update \
&& apt-get install -y --no-install-recommends git procps redis-server tini \
&& rm -rf /var/lib/apt/lists/*
# /app/.venv holds the app and nothing of anybody else's, so there is nothing
# here to adopt: node code gets a venv of its own on the data volume, which is
# what the Modules screen installs into.
ENV PYTHONUNBUFFERED=1 \
PATH="/app/.venv/bin:$PATH" \
NODE_VENV=managed \
FRONTEND_DIST=/app/frontend/dist \
FRONTEND_HOST=http://localhost:8000
# The venv installs the app editable, so the source stays at the path it was
# synced from. uv comes with it: the Modules screen installs with it.
COPY --from=backend /app /app
COPY --from=frontend /app/frontend/dist /app/frontend/dist
COPY ./backend/docker-entrypoint.sh /usr/local/bin/
WORKDIR /app/backend/
VOLUME /data
EXPOSE 8000
# Deep health: fails when the event loop is wedged or Redis is gone, not just
# when the process is dead.
HEALTHCHECK --interval=10s --timeout=5s --start-period=30s --retries=5 \
CMD python -c "import urllib.request; urllib.request.urlopen('http://127.0.0.1:8000/api/v1/utils/health/', timeout=4)"
ENTRYPOINT ["tini", "--", "docker-entrypoint.sh"]
# Single process on purpose: it hosts the flow engine, and a second worker
# would be a second engine — duplicated subscriptions, cron ticks and webhooks.
# `serve` makes the secret key and the first admin on first start, and prints
# the admin's password once.
CMD ["fluksio", "serve", "--plain", "--host", "0.0.0.0", "--port", "8000", "--data-dir", "/data"]