chore: phase 0 reproducible docker/dev setup with version pinning

This commit is contained in:
Jonas Miederer 2026-09-09 09:44:59 +00:00
parent af993bd43f
commit 1e8050611c
12 changed files with 286 additions and 31 deletions

View file

@ -11,7 +11,9 @@
}, },
"mounts": [ "mounts": [
"source=${localEnv:USERPROFILE}/.ssh,target=/mnt/host-ssh,type=bind,readonly", "source=${localEnv:USERPROFILE}/.ssh,target=/mnt/host-ssh,type=bind,readonly",
"source=/var/run/docker.sock,target=/var/run/docker-host.sock,type=bind" "source=/var/run/docker.sock,target=/var/run/docker-host.sock,type=bind",
"source=kirche-opencode-share,target=/home/vscode/.local/share/opencode,type=volume",
"source=kirche-opencode-config,target=/home/vscode/.config/opencode,type=volume"
], ],
"features": { "features": {
"ghcr.io/devcontainers/features/docker-outside-of-docker:1": {} "ghcr.io/devcontainers/features/docker-outside-of-docker:1": {}
@ -29,7 +31,7 @@
"esbenp.prettier-vscode" "esbenp.prettier-vscode"
], ],
"settings": { "settings": {
"python.defaultInterpreterPath": "/srv/kirche-einfach/backend/.venv/bin/python" "python.defaultInterpreterPath": "/opt/venv/bin/python"
} }
} }
} }

View file

@ -6,12 +6,34 @@ set -e
REPO=/srv/kirche-einfach REPO=/srv/kirche-einfach
# Venv lives outside the bind-mount (see backend/Dockerfile).
export UV_PYTHON_INSTALL_DIR=/opt/python UV_PROJECT_ENVIRONMENT=/opt/venv
echo "=== Setting up kirche-einfach ===" echo "=== Setting up kirche-einfach ==="
# Backend deps into the venv volume (setup runs via sudo; handed to vscode below). # Writable dirs first: this script runs via sudo as root, but the editor and
# most commands run as vscode. (A root-created backend/data/ or venv used to
# break the DB and the interpreter for vscode.)
mkdir -p "$REPO/backend/data" \
"$REPO/frontend/node_modules" \
/opt/python /opt/venv \
/home/vscode/.cache \
/home/vscode/.local/share/uv \
/home/vscode/.local/share/opencode \
/home/vscode/.config/opencode
chown -R vscode:vscode "$REPO/backend/data" \
"$REPO/frontend/node_modules" \
/opt/python /opt/venv \
/home/vscode/.cache \
/home/vscode/.local/share/uv \
/home/vscode/.local/share/opencode \
/home/vscode/.config/opencode 2>/dev/null || true
# Backend deps into /opt/venv, as vscode (a root-owned venv is unusable for
# the editor; uv resolves at setup time, no lockfile is committed).
echo "--- Installing Python dependencies ---" echo "--- Installing Python dependencies ---"
cd "$REPO/backend" cd "$REPO/backend"
uv sync su vscode -c "uv sync"
# opencode CLI (dev tooling; intentionally not baked into the image). # opencode CLI (dev tooling; intentionally not baked into the image).
if [ ! -x /home/vscode/.opencode/bin/opencode ]; then if [ ! -x /home/vscode/.opencode/bin/opencode ]; then
@ -57,13 +79,8 @@ EOF
# entrypoint on every start). # entrypoint on every start).
echo "--- Syncing canonical flows ---" echo "--- Syncing canonical flows ---"
cd "$REPO/backend" cd "$REPO/backend"
uv run python scripts/sync_flows.py --sync || echo "WARN: flow sync failed (check backend/flows and LFX)" su vscode -c "uv run python scripts/sync_flows.py --sync" || echo "WARN: flow sync failed (check backend/flows and LFX)"
echo "" echo ""
echo "=== Setup complete ===" echo "=== Setup complete ==="
echo "Forgejo username: $FORGEJO_USERNAME" echo "Forgejo username: $FORGEJO_USERNAME"
# Hand dirs back to vscode (this script runs via sudo; uv creates caches as root)
chown -R vscode:vscode "$REPO/backend/.venv" \
"$REPO/frontend/node_modules" \
/home/vscode/.cache 2>/dev/null || true

View file

@ -1,10 +1,15 @@
# Copy to `.env` and fill in values. `.env` is gitignored. # Copy to `.env` and fill in values. `.env` is gitignored.
# It holds only ubiquitous variables (identical value in dev and prod). # Dev defaults live in docker-compose.yml (APP_ENV=dev, FORGEJO_USERNAME=jonas).
# Dev-only variables travel via devcontainer.json `containerEnv` # Prod overrides via this .env file (e.g. APP_ENV=prod).
# (e.g. APP_ENV=dev); paths resolve from the repo root in code. # Paths resolve from the repo root in code.
FORGEJO_TOKEN= FORGEJO_TOKEN=
# --- langflow (authoring UI) --- # --- langflow (authoring UI) + lfx (flow runtime): pinned in lockstep ---
# LFX contract: major.minor of Langflow and LFX must match (patches are
# independent). Bump deliberately on both sides together (compose image pin
# below + Dockerfile.lfx ARG + backend dev dep `lfx~=`), then re-check
# backend/flows/*.json with `lfx upgrade --upgrade-flow=safe`.
LANGFLOW_VERSION=1.12.0
LANGFLOW_URL=http://langflow:7860 LANGFLOW_URL=http://langflow:7860
LANGFLOW_SUPERUSER=langflow LANGFLOW_SUPERUSER=langflow
LANGFLOW_SUPERUSER_PASSWORD= LANGFLOW_SUPERUSER_PASSWORD=
@ -23,5 +28,18 @@ CALLBACK_BASE_URL=http://api:8000
# --- frontend mode: dev server with HMR (prod .env must set `preview`) --- # --- frontend mode: dev server with HMR (prod .env must set `preview`) ---
FRONTEND_MODE=dev FRONTEND_MODE=dev
# --- access management (request + approval, invite via email) ---
# Public frontend URL (magic links point here).
FRONTEND_URL=http://localhost:3000
# Comma-separated emails that become admins (role assignment + bootstrap).
ADMIN_EMAILS=
# Reply-To on outgoing mail; also receives new access-request notifications.
CONTACT_TO=
MAIL_FROM=
SMTP_HOST=
SMTP_PORT=587
SMTP_USERNAME=
SMTP_PASSWORD=
# --- LLM (optional; tests mock the provider without it) --- # --- LLM (optional; tests mock the provider without it) ---
# LLM_API_KEY= # LLM_API_KEY=

4
.gitignore vendored
View file

@ -26,6 +26,10 @@ backend/data/
*.swp *.swp
*.swo *.swo
# Devcontainer
.devcontainer/devcontainer-lock.json
.devcontainer/opencode-data/
# OS # OS
.DS_Store .DS_Store
Thumbs.db Thumbs.db

View file

@ -8,7 +8,10 @@ ENV PYTHONUNBUFFERED=1 \
PIP_NO_CACHE_DIR=1 PIP_NO_CACHE_DIR=1
# lfx = flow runtime; requests = convenience for custom-component code. # lfx = flow runtime; requests = convenience for custom-component code.
RUN pip install lfx requests # Pinned to the Langflow minor (contract: major.minor must match Langflow;
# patches independent). Bump together with LANGFLOW_VERSION (.env.example).
ARG LFX_VERSION=1.12.0
RUN pip install "lfx~=${LFX_VERSION}" requests
RUN mkdir -p /flows RUN mkdir -p /flows
VOLUME /flows VOLUME /flows

158
PLAN_NEXT.md Normal file
View file

@ -0,0 +1,158 @@
# Continuation plan — kirche-einfach next steps (written after Phase 0)
> For a future agent restoring this session (e.g. after a container rebuild).
> Read this file + `AGENTS.md` + `README.md` first. All code/comments in
> English; only user-visible website strings are German.
## 0. Where we are
**Phase 0 (this commit) is done** — Docker/devcontainer reproducibility:
* `langflow` image pinned via `${LANGFLOW_VERSION:-1.12.0}` (was `:latest`
with nightly Watchtower drift). `Dockerfile.lfx` pins `lfx~=<same>`
(`ARG LFX_VERSION=1.12.0`). Backend test dep pinned `lfx~=1.12.0`.
Reason: official LFX contract — `major.minor` of Langflow and LFX must
match, patches are independent.
* `langflow` mounts `backend/components` (`LANGFLOW_COMPONENTS_PATH`) and
`backend/flows` (`LANGFLOW_LOAD_FLOWS_PATH`), both `:ro`. Files stay
source of truth; the API watcher (`app/flows/sync.py`) remains the ONLY
sync path — never add a second watcher.
* Backend venv moved to `/opt/venv` (image-local, `UV_PROJECT_ENVIRONMENT`),
managed Python to `/opt/python` (`UV_PYTHON_INSTALL_DIR`), both `a+rX`.
Removed `kirche-venv` + `kirche-node-modules` named volumes (they shadowed
image content with empty volumes on first start = split-brain).
`backend/entrypoint.sh` uses `/opt/venv/bin/*`, ensures `data/` exists and
is `vscode`-owned. `frontend/entrypoint.sh` runs `npm ci` when
`node_modules/.bin/vite` is missing (covers fresh prod checkouts).
* `devcontainer.json`: opencode session persistence via named volumes
`kirche-opencode-share → ~/.local/share/opencode` (sessions DB) and
`kirche-opencode-config → ~/.config/opencode`; interpreter path is now
`/opt/venv/bin/python`. `setup.sh` chowns first, then runs `uv sync` as
`vscode` (was: root `uv sync` into `/root`-backed venv → `Permission
denied` for vscode; `backend/data` root-owned → DB unwritable).
* Stale local `backend/.venv/` (bind-mount leftover) can be deleted manually
after rebuild: `rm -rf backend/.venv`. It is gitignored; nothing references
it anymore.
**Verified in this session:** `uv run pytest` green (after local venv repair,
see §5). NOT verifiable here (no node/docker in this sandbox): `npm run
build`, `docker compose config`, container rebuild. CI covers all three
(`.forgejo/workflows/`). After rebuilding the devcontainer, re-run: `uv run
pytest` (attached api, from `backend/`), `docker compose exec frontend npm
run build`, `docker compose config --quiet`.
**Still open (docs-only):** `README.md` + `.env.example` uncommitted diff
describes the target vision (access mgmt, dynamic forms, new Web UI) that is
NOT yet implemented: `app/auth/` is empty, `backend/components/kirche/` is
empty, frontend is still the old 3-tab UI, no `input-schema` endpoint.
## 1. Locked decisions (do not re-ask)
* Auth: SMTP magic-link, 15-min single-use tokens (`secrets.token_urlsafe`,
store `sha256`, `used_at` set before session creation), session = opaque id
in **DB table**, cookie `HttpOnly; Secure; SameSite=Lax; Path=/`.
SMTP creds already in `.env` (`SMTP_HOST/PORT/USERNAME/PASSWORD`,
`MAIL_FROM`, `CONTACT_TO`); dev fallback logs the link when SMTP unset.
* `FileInput` is a **stub** for now (string reference only); real upload later.
* Frontend: `react-router-dom` + RJSF (`@rjsf/core` + `@rjsf/validator-ajv8`).
RJSF renders backend-supplied `{json_schema, ui_schema}`; server
re-validates (client validation is UX only).
* **Routes are pure English** (user correction — no German slugs anywhere,
including URLs):
`/`, `/access` (was once sketched as `/zugang` — do NOT use),
`/login?token=`, `/app/workflows`, `/app/tasks`, `/app/admin`.
Visible labels/nav are German (`Start`, `Zugang`, `Workflows`, `Aufgaben`,
`Verwaltung`); code, props, comments, routes English.
* Langflow preload + mount (done in Phase 0). Custom-component lib in
`backend/components/kirche/` **must stay GUI-editable**: category folder +
`__init__.py` + `Component` subclasses using `lfx.io` inputs only
(stdlib + `lfx`, so the LFX runtime can execute them).
* Watchtower: `langflow` label removed (pinned images must not auto-update).
Version bumps = deliberate `LANGFLOW_VERSION` + `LFX_VERSION` change, then
`lfx upgrade --upgrade-flow=safe` check over `backend/flows/*.json`.
## 2. Phase 1 — Backend auth (magic-link + RBAC)
Files: `backend/app/models/user.py` (`User`, `AccessRequest`,
`MagicLinkToken`, `Session`), `backend/app/auth/{schemas,service,mail,api,
deps}.py`, wire router in `app/main.py`, settings in `app/settings.py`
(`frontend_url, admin_emails, contact_to, mail_from, smtp_*`), ONE new
Alembic revision (never edit applied migrations).
Endpoints:
* `POST /api/access/requests` public, rate-limited: `{name,email,reason}`
→ row `pending` + notify `CONTACT_TO`.
* `POST /api/access/requests/{id}/approve|reject` admin: approve →
upsert `User` + emit magic-link + SMTP send.
* `POST /api/auth/magic/consume {token}` → session cookie; `GET
/api/auth/me`; `POST /api/auth/logout` (delete session row).
* `POST /api/auth/admin-bootstrap`: only when user table empty AND email in
`ADMIN_EMAILS`.
* Tenant from session (`user.tenant_id`); keep old `tenant_id` body param as
deprecated fallback. `require_admin` dependency; keep API handlers thin
(DB only via `app/flows/store.py` + new `app/auth/*` service funcs).
Tests (all in `backend/tests/`): request→approve→consume→me→logout happy
path; token expiry boundary (15 min); token reuse → 400; admin-only → 403;
bootstrap-once; cookie flags (`HttpOnly`, `SameSite`, `Path`); assert no
token/secret in logs.
## 3. Phase 2 — `CustomInput*` library (GUI-editable)
* `backend/components/kirche/__init__.py` +
`backend/components/kirche/form_inputs.py`:
`class CustomInputForm(Component)` with inputs `StrInput`,
`MultilineInput`, `SecretStrInput`, `IntInput`, `FloatInput`, `BoolInput`,
`DropdownInput`, `MultiselectInput`, `SliderInput`, `FileInput` (stub) +
`Output`. Category dir `kirche/` = appears in Langflow GUI.
* Regenerate/add fixture flow using it via
`backend/scripts/make_flow_fixtures.py`; keep `simple_rule.json` as the
textarea-fallback case. Commit flow JSON (source of truth).
## 4. Phase 3 — Dynamic-form parsing (no second watcher)
* New `backend/app/flows/input_schema.py`:
`extract_form_schema(langflow_json) -> {json_schema, ui_schema}`.
Find nodes with `type`/`display_name` starting `CustomInput`; iterate
`template` minus `code`; map `_input_type` → JSON Schema type/widget
(`StrInput`/`MessageTextInput`→string, `MultilineInput`→string+textarea,
`SecretStrInput`→string+password, `IntInput`→integer,
`FloatInput`→number, `BoolInput`→boolean, `DropdownInput`→string+enum,
`MultiselectInput`→array+enum, `SliderInput`→number+range,
`FileInput`→string stub); `display_name/info/value/options/required/
advanced` → `title/description/default/enum/required/uiSchema`.
No such node → `{fallback: "textarea"}`.
* Called from existing `sync_from_directory` (cache per version in new
nullable `input_schema` JSON column via Alembic) + new `GET
/api/flows/{id}/input-schema → {flow_id, version_no, json_schema,
ui_schema}` (latest **deployed** version; 404 when none).
* Tests: per-type mapping table, fallback, sync persistence, endpoint 404.
## 5. Phase 4 — Frontend (router + RJSF, German UI, English routes)
Deps: `react-router-dom`, `@rjsf/core`, `@rjsf/validator-ajv8`.
Routes (English, see §1): `/`, `/access`, `/login`, `/app/workflows`
(flow select → `GET input-schema` → RJSF `<Form>` → `POST run`),
`/app/tasks` (own runs + approval states), `/app/admin` (admin-only:
access-request queue, ALL tasks incl. open human-intervention with
approve/reject + note, import/sync/deploy). `fetch` with
`credentials: "include"`. Refactor `FlowsTab/ApprovalsTab/AdminTab` into
route components; drop old tab shell. Verify with `npm run build`.
## 6. Operational notes for the next agent
* Commands: backend from `backend/`: `uv run pytest`,
`uv run alembic upgrade head`, `uv run python scripts/sync_flows.py
--sync`; frontend via `docker compose exec frontend npm run build`.
* Local venv repair (this sandbox had a root-backed broken `.venv`):
`rm -rf backend/.venv && uv sync` (network needed for `uv python
install`). In rebuilt containers this is unnecessary (`/opt/venv`).
* `.env` (gitignored) still lacks `FRONTEND_URL`/`ADMIN_EMAILS` from the
`.env.example` diff — copy them over locally when starting Phase 1. Never
commit `.env`.
* Flow JSON that fails LFX after a version bump: run
`lfx upgrade --upgrade-flow=safe` on the file, review diff, commit.
* Definition of done per `AGENTS.md`: new behavior has tests, `uv run
pytest` green, `npm run build` green when frontend changed, schema changes
as new Alembic revisions.

View file

@ -22,8 +22,22 @@ needs judgment. Every step is documented and traceable.
deploy to the lightweight LFX runtime and can run synchronously or streamed. deploy to the lightweight LFX runtime and can run synchronously or streamed.
- **Approvals** — a flow can pause and ask a human for a decision. Approving - **Approvals** — a flow can pause and ask a human for a decision. Approving
can automatically start a follow-up ("continuation") flow. can automatically start a follow-up ("continuation") flow.
- **Web UI** — three tabs: *Vorgänge* (flows and runs), *Freigaben* - **Access management** — public landing page (German) with an access-request
(approvals), *Administration* (import, sync, deploy). form (request + approval). Approved users get a passwordless magic-link
invite by email (SMTP via `.env`); sessions are HttpOnly cookies, tenants
come from the session. First admin via `POST /api/auth/admin-bootstrap`
(only on an empty user table, email must be in `ADMIN_EMAILS`).
- **Dynamic forms** — flows declare form fields as typed inputs on a
`CustomInput*` custom component (reusable library in
`backend/components/`, mounted into Langflow via
`LANGFLOW_COMPONENTS_PATH`). `GET /api/flows/{id}/input-schema` derives a
JSON Schema + RJSF uiSchema from the deployed version; the webapp renders
the matching form automatically. Flows without such a node fall back to a
single textarea.
- **Web UI** — landing page (*Start*), access request (*Zugang*), and the
logged-in webapp: *Workflows* (choose + run with dynamic form),
*Aufgaben* (approvals), *Verwaltung* (admin: access requests, import,
sync, deploy).
## Infrastructure ## Infrastructure

View file

@ -4,7 +4,14 @@
FROM python:3.12-slim FROM python:3.12-slim
ENV PYTHONUNBUFFERED=1 \ ENV PYTHONUNBUFFERED=1 \
PIP_NO_CACHE_DIR=1 PIP_NO_CACHE_DIR=1 \
# Venv + managed Python live outside the repo bind-mount: a venv under
# backend/ used to be shadowed by a named volume (empty on first start)
# and backed by a root-owned interpreter vscode could not execute.
# /opt is image-local, world-readable, identical in dev and prod.
UV_PYTHON_INSTALL_DIR=/opt/python \
UV_PROJECT_ENVIRONMENT=/opt/venv
ENV PATH="/opt/venv/bin:$PATH"
COPY --from=ghcr.io/astral-sh/uv:latest /uv /usr/local/bin/uv COPY --from=ghcr.io/astral-sh/uv:latest /uv /usr/local/bin/uv
@ -14,7 +21,9 @@ RUN apt-get update \
&& apt-get install -y --no-install-recommends sudo openssh-client \ && apt-get install -y --no-install-recommends sudo openssh-client \
&& rm -rf /var/lib/apt/lists/* \ && rm -rf /var/lib/apt/lists/* \
&& useradd -m -s /bin/bash vscode \ && useradd -m -s /bin/bash vscode \
&& echo "vscode ALL=(ALL) NOPASSWD:ALL" > /etc/sudoers.d/vscode && echo "vscode ALL=(ALL) NOPASSWD:ALL" > /etc/sudoers.d/vscode \
&& mkdir -p /opt/python /opt/venv \
&& chmod 755 /opt/python /opt/venv
WORKDIR /srv/kirche-einfach/backend WORKDIR /srv/kirche-einfach/backend
COPY pyproject.toml alembic.ini entrypoint.sh ./ COPY pyproject.toml alembic.ini entrypoint.sh ./
@ -23,7 +32,8 @@ COPY migrations ./migrations
COPY scripts ./scripts COPY scripts ./scripts
# No lockfile is committed; dependencies resolve at build time. # No lockfile is committed; dependencies resolve at build time.
# Full groups (incl. dev): pytest and friends must work in dev with zero steps. # Full groups (incl. dev): pytest and friends must work in dev with zero steps.
RUN uv sync && chmod +x entrypoint.sh # a+rX: the image builds as root, but the attached devcontainer runs as vscode.
RUN uv sync && chmod +x entrypoint.sh && chmod -R a+rX /opt/python /opt/venv
ENV FLOWS_WATCH_ENABLED=true ENV FLOWS_WATCH_ENABLED=true
EXPOSE 8000 EXPOSE 8000

View file

@ -6,10 +6,18 @@ set -e
cd /srv/kirche-einfach/backend cd /srv/kirche-einfach/backend
.venv/bin/alembic upgrade head # Runtime SQLite file lives in the bind-mount: ensure the dir exists and is
# owned by vscode (the entrypoint runs as root, but the editor runs as
# vscode — a root-owned data/ dir made the DB unwritable).
mkdir -p data
if id vscode >/dev/null 2>&1; then
chown -R vscode:vscode data 2>/dev/null || true
fi
/opt/venv/bin/alembic upgrade head
if [ "${APP_ENV:-prod}" = "dev" ]; then if [ "${APP_ENV:-prod}" = "dev" ]; then
exec .venv/bin/uvicorn app.main:app --host 0.0.0.0 --port 8000 --reload exec /opt/venv/bin/uvicorn app.main:app --host 0.0.0.0 --port 8000 --reload
else else
exec .venv/bin/uvicorn app.main:app --host 0.0.0.0 --port 8000 exec /opt/venv/bin/uvicorn app.main:app --host 0.0.0.0 --port 8000
fi fi

View file

@ -27,7 +27,9 @@ testpaths = ["tests"]
[dependency-groups] [dependency-groups]
dev = [ dev = [
"lfx", # Pinned to the Langflow minor (contract: major.minor must match).
# Bump together with LANGFLOW_VERSION (.env.example) + Dockerfile.lfx.
"lfx~=1.12.0",
"pytest>=8", "pytest>=8",
"pytest-asyncio>=0.24", "pytest-asyncio>=0.24",
"pytest-cov>=6", "pytest-cov>=6",

View file

@ -14,8 +14,11 @@ services:
env_file: env_file:
- .env - .env
volumes: volumes:
# No venv volume by design: the venv lives at /opt/venv (image-local,
# see backend/Dockerfile). A named volume here used to shadow the image
# content with an empty dir on first start (split-brain with the host
# checkout, plus root-owned interpreters).
- .:/srv/kirche-einfach - .:/srv/kirche-einfach
- kirche-venv:/srv/kirche-einfach/backend/.venv
ports: ports:
- "8000:8000" # FastAPI - "8000:8000" # FastAPI
depends_on: depends_on:
@ -45,8 +48,10 @@ services:
# Dev default (HMR); prod .env sets `preview` (build + serve bundle). # Dev default (HMR); prod .env sets `preview` (build + serve bundle).
FRONTEND_MODE: ${FRONTEND_MODE:-dev} FRONTEND_MODE: ${FRONTEND_MODE:-dev}
volumes: volumes:
# No node_modules volume by design: deps live in the bind-mount and the
# entrypoint runs `npm ci` on first start (a volume used to shadow the
# image install with an empty dir and diverge from the host checkout).
- .:/srv/kirche-einfach - .:/srv/kirche-einfach
- kirche-node-modules:/srv/kirche-einfach/frontend/node_modules
ports: ports:
- "3000:3000" # Vite - "3000:3000" # Vite
depends_on: depends_on:
@ -55,7 +60,10 @@ services:
restart: unless-stopped restart: unless-stopped
langflow: langflow:
image: langflowai/langflow:latest # Pinned (never :latest): Langflow and LFX are versioned together, their
# major.minor must match. Bump via LANGFLOW_VERSION in .env (see
# .env.example) together with Dockerfile.lfx + backend `lfx~=` pin.
image: langflowai/langflow:${LANGFLOW_VERSION:-1.12.0}
env_file: env_file:
- .env - .env
environment: environment:
@ -68,12 +76,17 @@ services:
# SQLite file inside the langflow-data volume (absolute path required # SQLite file inside the langflow-data volume (absolute path required
# by Langflow); overrides any postgres URL that may linger in .env. # by Langflow); overrides any postgres URL that may linger in .env.
LANGFLOW_DATABASE_URL: sqlite:////app/langflow/langflow.db LANGFLOW_DATABASE_URL: sqlite:////app/langflow/langflow.db
# Custom form-input components (backend/components, category folders).
LANGFLOW_COMPONENTS_PATH: /app/components
# Seed the authoring UI with the canonical flows (read-only: the files
# stay the source of truth, the API watcher is the only sync path).
LANGFLOW_LOAD_FLOWS_PATH: /app/flows
volumes: volumes:
- langflow-data:/app/langflow - langflow-data:/app/langflow
- ./backend/components:/app/components:ro
- ./backend/flows:/app/flows:ro
ports: ports:
- "7860:7860" - "7860:7860"
labels:
com.centurylinklabs.watchtower.enable: "true"
restart: unless-stopped restart: unless-stopped
healthcheck: healthcheck:
test: test:
@ -110,6 +123,8 @@ services:
# Nightly auto-updates of :latest images (prod). Never started in dev # Nightly auto-updates of :latest images (prod). Never started in dev
# (not in devcontainer.json runServices; also needs a docker socket). # (not in devcontainer.json runServices; also needs a docker socket).
# langflow/lfx carry no watchtower label on purpose: both are version-pinned
# (major.minor must match) and update only via deliberate version bumps.
watchtower: watchtower:
image: nickfedor/watchtower:latest image: nickfedor/watchtower:latest
volumes: volumes:
@ -124,7 +139,5 @@ services:
restart: unless-stopped restart: unless-stopped
volumes: volumes:
kirche-venv:
kirche-node-modules:
langflow-data: langflow-data:
lfx-data: lfx-data:

View file

@ -6,6 +6,12 @@ set -e
cd /srv/kirche-einfach/frontend cd /srv/kirche-einfach/frontend
# node_modules lives in the repo bind-mount (no named volume by design):
# install on first start after a fresh checkout (dev and prod).
if [ ! -x "node_modules/.bin/vite" ]; then
npm ci
fi
if [ "${FRONTEND_MODE:-dev}" = "dev" ]; then if [ "${FRONTEND_MODE:-dev}" = "dev" ]; then
exec npm run dev -- --host 0.0.0.0 --port 3000 exec npm run dev -- --host 0.0.0.0 --port 3000
else else