# 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. # ── Dashboard ──────────────────────────────────────────────────── # Built on the build machine's own CPU: the output is the same for every # platform, and under emulation for an arm64 image this stage alone takes # half an hour. FROM --platform=$BUILDPLATFORM oven/bun:1 AS frontend WORKDIR /app COPY package.json bun.lock /app/ COPY frontend/package.json /app/frontend/ WORKDIR /app/frontend RUN bun install 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= # Set only for the copy the portal serves (the appshell stage below). ARG VITE_BASE RUN bun run build # ── 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.13.6 /uv /bin/ # A slow link, such as a Raspberry Pi on WiFi, outlasts uv's default 30 s # download timeout. ENV UV_COMPILE_BYTECODE=1 UV_LINK_MODE=copy UV_HTTP_TIMEOUT=120 WORKDIR /app/ ARG EXTRAS="" # 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-dev --no-install-workspace --package fluksio \ $(for e in $(echo "$EXTRAS" | tr , ' '); do printf -- '--extra %s ' "$e"; done) COPY ./backend/scripts /app/backend/scripts # README.md and LICENSE come along because pyproject.toml names them in # `readme` and `license-files`; hatchling reads both when it builds the # package metadata below, and fails the build if either is missing. COPY ./backend/pyproject.toml ./backend/alembic.ini ./backend/README.md ./backend/LICENSE /app/backend/ COPY ./backend/fluksio /app/backend/fluksio # The worker is a workspace member of its own, so the engine's sync needs it # present to resolve the dependency on it. It is also what the engine runs # node code with. COPY ./worker /app/worker 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-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. Place built connector wheels here; # an image with none is the normal case. COPY ./backend/connector-wheels /tmp/connector-wheels RUN --mount=type=cache,target=/root/.cache/uv \ sh -c 'ls /tmp/connector-wheels/*.whl >/dev/null 2>&1 \ && uv pip install --python /app/.venv/bin/python /tmp/connector-wheels/*.whl \ || echo "no connectors bundled"' # ── The portal's copy of the dashboard ────────────────────────── # The hub serves the dashboard for every instance it tunnels to, from # /app-shell/. index/docker/compose.yml builds this target with # VITE_BASE=/app-shell/ and copies /dist into the hub's volume. FROM busybox:1.37 AS appshell COPY --from=frontend /app/frontend/dist /dist # ── git, without perl ──────────────────────────────────────────── # The flow store runs init, add, commit, gc and rev-parse, all built into the # one binary, which needs nothing slim lacks. Debian's package would bring # perl and the remote helpers along, about 100 MB nothing here calls. FROM python:3.13-slim AS git RUN apt-get update \ && apt-get install -y --no-install-recommends git \ && rm -rf /var/lib/apt/lists/* # ── Runtime ────────────────────────────────────────────────────── FROM python:3.13-slim COPY --from=git /usr/bin/git /usr/bin/git # 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. The git round trip fails the build if the binary # above ever needs something this image does not have. RUN apt-get update \ && apt-get install -y --no-install-recommends procps redis-server tini \ && rm -rf /var/lib/apt/lists/* \ && git init -q /tmp/g 2>/dev/null \ && git -C /tmp/g -c user.name=x -c user.email=x@x commit -q --allow-empty -m x \ && git -C /tmp/g gc --quiet && git -C /tmp/g rev-parse HEAD >/dev/null \ && rm -rf /tmp/g # /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"]