Files
app/backend/Dockerfile
T

142 lines
6.5 KiB
Docker

# 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.9.26 /uv /bin/
ENV UV_COMPILE_BYTECODE=1 UV_LINK_MODE=copy
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"]