diff --git a/.dockerignore b/.dockerignore index 31a0560..a092830 100644 --- a/.dockerignore +++ b/.dockerignore @@ -3,7 +3,7 @@ # copies under frontend/ and backend/ were therefore never consulted, and the # host's stale build outputs and node_modules were shipped into every build. # -# Keep `package.json` and `bun.lock` at this root: frontend/Dockerfile copies +# Keep `package.json` and `bun.lock` at this root: backend/Dockerfile copies # them before the workspace install. # Build outputs — regenerated inside the image. diff --git a/backend/Dockerfile b/backend/Dockerfile index 7aba764..f9af4c3 100644 --- a/backend/Dockerfile +++ b/backend/Dockerfile @@ -20,6 +20,8 @@ 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 ─────────────────────────────────────────── @@ -69,16 +71,39 @@ RUN --mount=type=cache,target=/root/.cache/uv \ && 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 -# 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. +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 git procps redis-server tini \ - && rm -rf /var/lib/apt/lists/* + && 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 diff --git a/backend/docker-entrypoint.sh b/backend/docker-entrypoint.sh index 72e37e1..2ecdb21 100755 --- a/backend/docker-entrypoint.sh +++ b/backend/docker-entrypoint.sh @@ -5,12 +5,17 @@ # so without it a restart forgets all three. In the background on purpose: its # appendonly file is written on every change, so the SIGKILL it gets when the # container stops loses nothing that was acknowledged. +# +# Its memory-overcommit warning is dropped: the setting is the host kernel's, +# nothing in a container can change it, and it only matters to a fork for a +# snapshot under memory pressure. Every other warning still reaches the log. set -e if [ -z "$REDIS_HOST" ]; then mkdir -p /data/redis redis-server --bind 127.0.0.1 --port 6379 --dir /data/redis \ - --appendonly yes --loglevel warning & + --appendonly yes --loglevel warning \ + | grep --line-buffered -v "Memory overcommit must be enabled" & i=0 until redis-cli -h 127.0.0.1 ping >/dev/null 2>&1; do i=$((i + 1)) diff --git a/backend/fluksio/mcp/server.py b/backend/fluksio/mcp/server.py index 4cffc76..bbf2faf 100644 --- a/backend/fluksio/mcp/server.py +++ b/backend/fluksio/mcp/server.py @@ -13,6 +13,7 @@ identity rather than some service account. from __future__ import annotations import logging +import warnings from contextvars import ContextVar from typing import Any @@ -24,7 +25,12 @@ from fluksio.sdk.client import seg as _seg logger = logging.getLogger(__name__) -mcp = FastMCP("fluksio") +# FastMCP's own settings model leaves `lifespan` a forward reference, which +# pydantic-settings warns about on every start. Nothing reads that field from +# the environment, so the warning says nothing about this app. +with warnings.catch_warnings(): + warnings.filterwarnings("ignore", message="Field 'lifespan' has an incomplete") + mcp = FastMCP("fluksio") #: The tool calls never leave the process, so the host is a label, not a route. _INTERNAL_BASE = "http://fluksio-mcp.internal" diff --git a/frontend/Dockerfile b/frontend/Dockerfile deleted file mode 100644 index 63fc677..0000000 --- a/frontend/Dockerfile +++ /dev/null @@ -1,35 +0,0 @@ -# Stage 0, "build-stage", based on Bun, to build and compile the frontend -FROM oven/bun:1 AS build-stage - -WORKDIR /app - -COPY package.json bun.lock /app/ - -COPY frontend/package.json /app/frontend/ - -WORKDIR /app/frontend - -RUN bun install - -COPY ./frontend /app/frontend -ARG VITE_API_URL -# Set only for the copy a portal serves; see vite.config.ts. -ARG VITE_BASE - -RUN bun run build - - -# Stage 1, based on Nginx, to have only the compiled app, ready for production with Nginx -FROM nginx:1 - -COPY --from=build-stage /app/frontend/dist/ /usr/share/nginx/html - -COPY ./frontend/nginx.conf /etc/nginx/conf.d/default.conf -COPY ./frontend/nginx-security-headers.conf /etc/nginx/security-headers.conf -# The CSP's connect-src names the API when the bundle calls it on another -# origin (https://api.x and wss://api.x); empty, nginx proxies it on this one. -ARG VITE_API_URL -RUN api="${VITE_API_URL%/}"; \ - sed -i "s|__API_ORIGINS__|${api:+ $api $(echo "$api" | sed 's/^http/ws/')}|" \ - /etc/nginx/security-headers.conf -COPY ./frontend/nginx-backend-not-found.conf /etc/nginx/extra-conf.d/backend-not-found.conf diff --git a/frontend/nginx-backend-not-found.conf b/frontend/nginx-backend-not-found.conf deleted file mode 100644 index f6fea66..0000000 --- a/frontend/nginx-backend-not-found.conf +++ /dev/null @@ -1,9 +0,0 @@ -location /api { - return 404; -} -location /docs { - return 404; -} -location /redoc { - return 404; -} diff --git a/frontend/nginx-security-headers.conf b/frontend/nginx-security-headers.conf deleted file mode 100644 index 596a682..0000000 --- a/frontend/nginx-security-headers.conf +++ /dev/null @@ -1,15 +0,0 @@ -# Security headers. Included at server level and again in every location with -# an add_header of its own, since nginx drops the inherited ones there. -# -# script-src: the bundle, plus the theme script inline in index.html by its -# hash. Edit that script and this hash must change with it (sha256 of the text -# between the tags, base64), or the app paints light-then-dark again; -# `bun run check` (src/csp.check.ts) fails with the new value. -# style-src 'unsafe-inline': Monaco, Radix's scroll lock and the toasts insert -#