chore: phase 0 reproducible docker/dev setup with version pinning
This commit is contained in:
parent
af993bd43f
commit
1e8050611c
12 changed files with 286 additions and 31 deletions
|
|
@ -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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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
|
|
||||||
|
|
|
||||||
26
.env.example
26
.env.example
|
|
@ -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
4
.gitignore
vendored
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
158
PLAN_NEXT.md
Normal 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.
|
||||||
18
README.md
18
README.md
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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",
|
||||||
|
|
|
||||||
|
|
@ -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:
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue