feat: unify on single repo-root compose file for dev and prod
This commit is contained in:
parent
9acdbaa453
commit
af993bd43f
18 changed files with 253 additions and 229 deletions
|
|
@ -1,48 +0,0 @@
|
||||||
FROM mcr.microsoft.com/devcontainers/base:debian
|
|
||||||
|
|
||||||
ARG FORGEJO_USERNAME
|
|
||||||
ENV FORGEJO_USERNAME=${FORGEJO_USERNAME}
|
|
||||||
|
|
||||||
# SSH (missing from base image)
|
|
||||||
RUN apt-get update \
|
|
||||||
&& apt-get install -y --no-install-recommends openssh-client \
|
|
||||||
&& rm -rf /var/lib/apt/lists/*
|
|
||||||
|
|
||||||
# Node.js 22 + npm (npm tree copied; bin symlinks recreated - COPY dereferences them)
|
|
||||||
COPY --from=node:22-slim /usr/local/bin/node /usr/local/bin/
|
|
||||||
COPY --from=node:22-slim /usr/local/lib/node_modules /usr/local/lib/node_modules
|
|
||||||
RUN ln -s ../lib/node_modules/npm/bin/npm-cli.js /usr/local/bin/npm \
|
|
||||||
&& ln -s ../lib/node_modules/npm/bin/npx-cli.js /usr/local/bin/npx
|
|
||||||
|
|
||||||
# uv (from distroless)
|
|
||||||
COPY --from=ghcr.io/astral-sh/uv:latest /uv /usr/local/bin/uv
|
|
||||||
|
|
||||||
# Python 3.12 (system-shared location; default /root is unreadable for vscode)
|
|
||||||
ENV UV_PYTHON_INSTALL_DIR=/opt/uv/python
|
|
||||||
RUN uv python install 3.12 \
|
|
||||||
&& ln -s /opt/uv/python/cpython-3.12-linux-x86_64-gnu/bin/python3.12 /usr/local/bin/python3.12 \
|
|
||||||
&& ln -s python3.12 /usr/local/bin/python3 \
|
|
||||||
&& ln -s python3 /usr/local/bin/python
|
|
||||||
|
|
||||||
# Trust the workspace bind mount (source on Windows FS -> uid mismatch)
|
|
||||||
RUN git config --system --add safe.directory /workspaces/kirche-einfach
|
|
||||||
|
|
||||||
# Pre-create named-volume targets (volumes inherit image content on first mount).
|
|
||||||
# The backend venv is created as `vscode` so VS Code's Python extension finds an
|
|
||||||
# interpreter immediately on first attach, before postCreateCommand runs `uv sync`.
|
|
||||||
USER vscode
|
|
||||||
RUN mkdir -p /workspaces/kirche-einfach/backend \
|
|
||||||
&& uv venv /workspaces/kirche-einfach/backend/.venv \
|
|
||||||
&& mkdir -p /workspaces/kirche-einfach/frontend/node_modules
|
|
||||||
USER root
|
|
||||||
|
|
||||||
# opencode (as non-root user)
|
|
||||||
USER vscode
|
|
||||||
RUN curl -fsSL https://opencode.ai/install | bash
|
|
||||||
USER root
|
|
||||||
RUN ln -s /home/vscode/.opencode/bin/opencode /usr/local/bin/opencode
|
|
||||||
|
|
||||||
# Fix ownership (home + volume targets so vscode user can write)
|
|
||||||
RUN chown -R vscode:vscode /home/vscode /workspaces
|
|
||||||
|
|
||||||
CMD ["sleep", "infinity"]
|
|
||||||
|
|
@ -1,17 +1,22 @@
|
||||||
{
|
{
|
||||||
"name": "kirche-einfach",
|
"name": "kirche-einfach",
|
||||||
"dockerComposeFile": "docker-compose.yml",
|
"dockerComposeFile": ["../docker-compose.yml"],
|
||||||
"service": "app",
|
"service": "api",
|
||||||
"runServices": ["db", "langflow", "lfx"],
|
"runServices": ["api", "frontend", "langflow", "lfx"],
|
||||||
"mounts": [
|
"workspaceFolder": "/srv/kirche-einfach",
|
||||||
"source=${localEnv:USERPROFILE}/.ssh,target=/mnt/host-ssh,type=bind,readonly"
|
|
||||||
],
|
|
||||||
"remoteUser": "vscode",
|
"remoteUser": "vscode",
|
||||||
"containerEnv": {
|
"containerEnv": {
|
||||||
|
"APP_ENV": "dev",
|
||||||
"FORGEJO_USERNAME": "jonas"
|
"FORGEJO_USERNAME": "jonas"
|
||||||
},
|
},
|
||||||
|
"mounts": [
|
||||||
|
"source=${localEnv:USERPROFILE}/.ssh,target=/mnt/host-ssh,type=bind,readonly",
|
||||||
|
"source=/var/run/docker.sock,target=/var/run/docker-host.sock,type=bind"
|
||||||
|
],
|
||||||
|
"features": {
|
||||||
|
"ghcr.io/devcontainers/features/docker-outside-of-docker:1": {}
|
||||||
|
},
|
||||||
"overrideCommand": false,
|
"overrideCommand": false,
|
||||||
"workspaceFolder": "/workspaces/kirche-einfach",
|
|
||||||
"postCreateCommand": "sudo -E bash .devcontainer/setup.sh",
|
"postCreateCommand": "sudo -E bash .devcontainer/setup.sh",
|
||||||
"postStartCommand": "bash .devcontainer/postStart.sh",
|
"postStartCommand": "bash .devcontainer/postStart.sh",
|
||||||
"customizations": {
|
"customizations": {
|
||||||
|
|
@ -24,7 +29,7 @@
|
||||||
"esbenp.prettier-vscode"
|
"esbenp.prettier-vscode"
|
||||||
],
|
],
|
||||||
"settings": {
|
"settings": {
|
||||||
"python.defaultInterpreterPath": "/workspaces/kirche-einfach/backend/.venv/bin/python"
|
"python.defaultInterpreterPath": "/srv/kirche-einfach/backend/.venv/bin/python"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,92 +0,0 @@
|
||||||
# Compose stack for the kirche-einfach devcontainer.
|
|
||||||
# VS Code attaches to the `app` service (see devcontainer.json); all values
|
|
||||||
# come from the repo-root `.env` via `env_file` (no ${} interpolation needed).
|
|
||||||
services:
|
|
||||||
app:
|
|
||||||
build:
|
|
||||||
context: .
|
|
||||||
dockerfile: Dockerfile
|
|
||||||
args:
|
|
||||||
FORGEJO_USERNAME: jonas
|
|
||||||
command: sleep infinity
|
|
||||||
volumes:
|
|
||||||
- ..:/workspaces/kirche-einfach:cached
|
|
||||||
- kirche-einfach-venv:/workspaces/kirche-einfach/backend/.venv
|
|
||||||
- kirche-einfach-node-modules:/workspaces/kirche-einfach/frontend/node_modules
|
|
||||||
env_file:
|
|
||||||
- ../.env
|
|
||||||
ports:
|
|
||||||
- "8000:8000" # FastAPI
|
|
||||||
- "3000:3000" # Vite
|
|
||||||
|
|
||||||
langflow:
|
|
||||||
image: langflowai/langflow:latest
|
|
||||||
env_file:
|
|
||||||
- ../.env
|
|
||||||
environment:
|
|
||||||
# Static values only; secrets/URLs come from ../.env via env_file
|
|
||||||
# (compose interpolates ${} from .devcontainer/.env, not the repo-root .env).
|
|
||||||
LANGFLOW_CONFIG_DIR: /app/langflow
|
|
||||||
LANGFLOW_HOST: 0.0.0.0
|
|
||||||
PORT: "7860"
|
|
||||||
LANGFLOW_AUTO_LOGIN: "false"
|
|
||||||
LANGFLOW_API_KEY_SOURCE: env
|
|
||||||
# SQLite file inside the langflow-data volume (absolute path required
|
|
||||||
# by Langflow); overrides any postgres URL in ../.env.
|
|
||||||
LANGFLOW_DATABASE_URL: sqlite:////app/langflow/langflow.db
|
|
||||||
volumes:
|
|
||||||
- langflow-data:/app/langflow
|
|
||||||
ports:
|
|
||||||
- "7860:7860"
|
|
||||||
healthcheck:
|
|
||||||
test:
|
|
||||||
[
|
|
||||||
"CMD-SHELL",
|
|
||||||
"python -c \"import urllib.request; urllib.request.urlopen('http://localhost:7860/health', timeout=5)\"",
|
|
||||||
]
|
|
||||||
interval: 10s
|
|
||||||
timeout: 5s
|
|
||||||
retries: 20
|
|
||||||
start_period: 60s
|
|
||||||
|
|
||||||
lfx:
|
|
||||||
build:
|
|
||||||
context: .
|
|
||||||
dockerfile: Dockerfile.lfx
|
|
||||||
env_file:
|
|
||||||
- ../.env
|
|
||||||
volumes:
|
|
||||||
- lfx-data:/flows
|
|
||||||
ports:
|
|
||||||
- "6004:6004"
|
|
||||||
healthcheck:
|
|
||||||
test:
|
|
||||||
[
|
|
||||||
"CMD-SHELL",
|
|
||||||
"python -c \"import urllib.request; urllib.request.urlopen('http://localhost:6004/health', timeout=5)\"",
|
|
||||||
]
|
|
||||||
interval: 10s
|
|
||||||
timeout: 5s
|
|
||||||
retries: 12
|
|
||||||
start_period: 30s
|
|
||||||
|
|
||||||
# Optional nightly auto-updates of :latest images. Not part of the default
|
|
||||||
# dev startup (no container updates needed while iterating).
|
|
||||||
watchtower:
|
|
||||||
image: nickfedor/watchtower:latest
|
|
||||||
profiles: ["updates"]
|
|
||||||
volumes:
|
|
||||||
- /var/run/docker.sock:/var/run/docker.sock
|
|
||||||
environment:
|
|
||||||
WATCHTOWER_LABEL_ENABLE: "true"
|
|
||||||
WATCHTOWER_SCHEDULE: "0 0 4 * * *"
|
|
||||||
WATCHTOWER_CLEANUP: "true"
|
|
||||||
WATCHTOWER_ROLLING_RESTART: "true"
|
|
||||||
labels:
|
|
||||||
com.centurylinklabs.watchtower.enable: "true"
|
|
||||||
|
|
||||||
volumes:
|
|
||||||
kirche-einfach-venv:
|
|
||||||
kirche-einfach-node-modules:
|
|
||||||
langflow-data:
|
|
||||||
lfx-data:
|
|
||||||
|
|
@ -1,22 +1,30 @@
|
||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
|
# Dev-only setup (runs via postCreateCommand in the attached `api` container).
|
||||||
|
# Production needs none of this: images carry their dependencies and the
|
||||||
|
# api entrypoint migrates on every start.
|
||||||
set -e
|
set -e
|
||||||
|
|
||||||
|
REPO=/srv/kirche-einfach
|
||||||
|
|
||||||
echo "=== Setting up kirche-einfach ==="
|
echo "=== Setting up kirche-einfach ==="
|
||||||
|
|
||||||
# Backend
|
# Backend deps into the venv volume (setup runs via sudo; handed to vscode below).
|
||||||
echo "--- Installing Python dependencies ---"
|
echo "--- Installing Python dependencies ---"
|
||||||
cd /workspaces/kirche-einfach/backend
|
cd "$REPO/backend"
|
||||||
uv sync
|
uv sync
|
||||||
|
|
||||||
# Frontend
|
# opencode CLI (dev tooling; intentionally not baked into the image).
|
||||||
echo "--- Installing Node dependencies ---"
|
if [ ! -x /home/vscode/.opencode/bin/opencode ]; then
|
||||||
cd /workspaces/kirche-einfach/frontend
|
echo "--- Installing opencode ---"
|
||||||
npm install
|
su vscode -c "curl -fsSL https://opencode.ai/install | bash"
|
||||||
|
ln -sf /home/vscode/.opencode/bin/opencode /usr/local/bin/opencode
|
||||||
|
fi
|
||||||
|
|
||||||
# Compose services (langflow, lfx) must be up; fail loudly otherwise.
|
# Compose services (langflow, lfx) must be up; fail loudly otherwise.
|
||||||
|
# (api/frontend are starting alongside this script.)
|
||||||
echo "--- Waiting for compose services ---"
|
echo "--- Waiting for compose services ---"
|
||||||
if [ ! -f /workspaces/kirche-einfach/.env ]; then
|
if [ ! -f "$REPO/.env" ]; then
|
||||||
echo "ERROR: /workspaces/kirche-einfach/.env is missing. Copy .env.example to .env and fill in secrets." >&2
|
echo "ERROR: $REPO/.env is missing. Copy .env.example to .env and fill in secrets." >&2
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
python3 - <<'EOF'
|
python3 - <<'EOF'
|
||||||
|
|
@ -45,19 +53,17 @@ if pending:
|
||||||
sys.exit(f"ERROR: compose services not ready: {sorted(pending)}")
|
sys.exit(f"ERROR: compose services not ready: {sorted(pending)}")
|
||||||
EOF
|
EOF
|
||||||
|
|
||||||
# Migrate the SQLite schema and reconcile backend/flows into the DB + LFX.
|
# Reconcile backend/flows into the DB + LFX (schema is migrated by the api
|
||||||
echo "--- Migrating database ---"
|
# entrypoint on every start).
|
||||||
cd /workspaces/kirche-einfach/backend
|
|
||||||
uv run alembic upgrade head
|
|
||||||
|
|
||||||
echo "--- Syncing canonical flows ---"
|
echo "--- Syncing canonical flows ---"
|
||||||
|
cd "$REPO/backend"
|
||||||
uv run python scripts/sync_flows.py --sync || echo "WARN: flow sync failed (check backend/flows and LFX)"
|
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/npm create caches as root)
|
# Hand dirs back to vscode (this script runs via sudo; uv creates caches as root)
|
||||||
chown -R vscode:vscode /workspaces/kirche-einfach/backend/.venv \
|
chown -R vscode:vscode "$REPO/backend/.venv" \
|
||||||
/workspaces/kirche-einfach/frontend/node_modules \
|
"$REPO/frontend/node_modules" \
|
||||||
/home/vscode/.cache 2>/dev/null || true
|
/home/vscode/.cache 2>/dev/null || true
|
||||||
|
|
|
||||||
19
.env.example
19
.env.example
|
|
@ -1,16 +1,10 @@
|
||||||
# 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-only variables travel via devcontainer.json `containerEnv`
|
||||||
|
# (e.g. APP_ENV=dev); paths resolve from the repo root in code.
|
||||||
FORGEJO_TOKEN=
|
FORGEJO_TOKEN=
|
||||||
|
|
||||||
# --- app database (SQLite, default if unset) ---
|
# --- langflow (authoring UI) ---
|
||||||
# Override only if you want a different engine/URL.
|
|
||||||
# DATABASE_URL=sqlite:///backend/data/kirche.db
|
|
||||||
|
|
||||||
# --- canonical flow definitions ---
|
|
||||||
# FLOWS_DIR=backend/flows
|
|
||||||
# Auto-reconcile backend/flows into DB + LFX while the API runs.
|
|
||||||
# FLOWS_WATCH_ENABLED=true
|
|
||||||
|
|
||||||
# --- langflow (authoring, dev only) ---
|
|
||||||
LANGFLOW_URL=http://langflow:7860
|
LANGFLOW_URL=http://langflow:7860
|
||||||
LANGFLOW_SUPERUSER=langflow
|
LANGFLOW_SUPERUSER=langflow
|
||||||
LANGFLOW_SUPERUSER_PASSWORD=
|
LANGFLOW_SUPERUSER_PASSWORD=
|
||||||
|
|
@ -24,7 +18,10 @@ LFX_URL=http://lfx:6004
|
||||||
LFX_API_KEY=lfx-
|
LFX_API_KEY=lfx-
|
||||||
|
|
||||||
# --- API self-reference for flow callbacks (must be reachable FROM lfx) ---
|
# --- API self-reference for flow callbacks (must be reachable FROM lfx) ---
|
||||||
CALLBACK_BASE_URL=http://app:8000
|
CALLBACK_BASE_URL=http://api:8000
|
||||||
|
|
||||||
|
# --- frontend mode: dev server with HMR (prod .env must set `preview`) ---
|
||||||
|
FRONTEND_MODE=dev
|
||||||
|
|
||||||
# --- LLM (optional; tests mock the provider without it) ---
|
# --- LLM (optional; tests mock the provider without it) ---
|
||||||
# LLM_API_KEY=
|
# LLM_API_KEY=
|
||||||
22
.forgejo/workflows/backend.yml
Normal file
22
.forgejo/workflows/backend.yml
Normal file
|
|
@ -0,0 +1,22 @@
|
||||||
|
# Backend test suite. Runs the full pytest suite on a throwaway SQLite file.
|
||||||
|
# Prerequisites on the Forgejo instance (not verifiable from the repo):
|
||||||
|
# - a Runner with the `docker` label online (repo Settings -> Actions -> Runners)
|
||||||
|
# - action resolution for actions/checkout (github proxy or code.forgejo.org mirror)
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [main]
|
||||||
|
pull_request:
|
||||||
|
paths:
|
||||||
|
- "backend/**"
|
||||||
|
- "docker-compose.yml"
|
||||||
|
- ".forgejo/workflows/backend.yml"
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
test:
|
||||||
|
runs-on: docker
|
||||||
|
container:
|
||||||
|
image: ghcr.io/astral-sh/uv:python3.12-bookworm
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
- run: cd backend && uv sync
|
||||||
|
- run: cd backend && uv run pytest
|
||||||
20
.forgejo/workflows/frontend.yml
Normal file
20
.forgejo/workflows/frontend.yml
Normal file
|
|
@ -0,0 +1,20 @@
|
||||||
|
# Frontend typecheck + build. Same prerequisites as backend.yml (Runner
|
||||||
|
# with the `docker` label, action resolution on the Forgejo instance).
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [main]
|
||||||
|
pull_request:
|
||||||
|
paths:
|
||||||
|
- "frontend/**"
|
||||||
|
- "docker-compose.yml"
|
||||||
|
- ".forgejo/workflows/frontend.yml"
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
runs-on: docker
|
||||||
|
container:
|
||||||
|
image: node:22-slim
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
- run: cd frontend && npm ci
|
||||||
|
- run: cd frontend && npm run build
|
||||||
22
.forgejo/workflows/infra.yml
Normal file
22
.forgejo/workflows/infra.yml
Normal file
|
|
@ -0,0 +1,22 @@
|
||||||
|
# Validates the single compose file parses (needs only the CLI, no daemon).
|
||||||
|
# Same Runner prerequisites as backend.yml.
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [main]
|
||||||
|
pull_request:
|
||||||
|
paths:
|
||||||
|
- "docker-compose.yml"
|
||||||
|
- ".env.example"
|
||||||
|
- "backend/Dockerfile"
|
||||||
|
- "frontend/Dockerfile"
|
||||||
|
- "Dockerfile.lfx"
|
||||||
|
- ".forgejo/workflows/infra.yml"
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
validate:
|
||||||
|
runs-on: docker
|
||||||
|
container:
|
||||||
|
image: docker:24
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
- run: docker compose -f docker-compose.yml config --quiet
|
||||||
20
AGENTS.md
20
AGENTS.md
|
|
@ -13,6 +13,26 @@ Backend (run from `backend/`):
|
||||||
|
|
||||||
Frontend (run from `frontend/`): `npm install`, `npm run dev`, `npm run build` (typecheck + build).
|
Frontend (run from `frontend/`): `npm install`, `npm run dev`, `npm run build` (typecheck + build).
|
||||||
|
|
||||||
|
## Multi-container ops
|
||||||
|
|
||||||
|
One `docker-compose.yml` at the repo root for dev and prod. Dev specifics
|
||||||
|
live in `.devcontainer/devcontainer.json` only — never add dev-only services,
|
||||||
|
profiles, or override files. Containers: `api` (FastAPI :8000, attached),
|
||||||
|
`frontend` (Vite :3000), `langflow` (:7860), `lfx` (:6004), `watchtower`
|
||||||
|
(prod only, not in `runServices`).
|
||||||
|
|
||||||
|
- Backend suite (attached container, from `backend/`): `uv run pytest`.
|
||||||
|
- Frontend build (node lives in `frontend`, reach it via exec from the repo
|
||||||
|
root): `docker compose exec frontend npm run build`. Fallback without a
|
||||||
|
docker socket: VS Code "Attach to Running Container" → frontend.
|
||||||
|
- Logs/restart: `docker compose logs -f <service>`, `docker compose restart api`.
|
||||||
|
- If `exec` misses the project, find it via `docker compose ls`.
|
||||||
|
- CI runs on Forgejo (`.forgejo/workflows/`): backend pytest, frontend
|
||||||
|
build, compose validation. Needs a Runner with the `docker` label online.
|
||||||
|
- Prod: own `.env` with secrets + `FRONTEND_MODE=preview`; code updates via
|
||||||
|
`git pull` + restart, image rebuilds only for dependency changes
|
||||||
|
(`docker compose up -d --build`).
|
||||||
|
|
||||||
## Definition of done
|
## Definition of done
|
||||||
|
|
||||||
- New behavior has tests; `uv run pytest` is green before reporting completion.
|
- New behavior has tests; `uv run pytest` is green before reporting completion.
|
||||||
|
|
|
||||||
56
README.md
56
README.md
|
|
@ -27,6 +27,10 @@ needs judgment. Every step is documented and traceable.
|
||||||
|
|
||||||
## Infrastructure
|
## Infrastructure
|
||||||
|
|
||||||
|
One `docker-compose.yml` at the repo root runs the whole stack — identical
|
||||||
|
services in dev and prod. Dev-only needs (editor, SSH keys, autoreload,
|
||||||
|
docker CLI) come from `.devcontainer/devcontainer.json`.
|
||||||
|
|
||||||
```mermaid
|
```mermaid
|
||||||
flowchart LR
|
flowchart LR
|
||||||
subgraph Repo["Git repo — source of truth"]
|
subgraph Repo["Git repo — source of truth"]
|
||||||
|
|
@ -34,11 +38,11 @@ flowchart LR
|
||||||
end
|
end
|
||||||
subgraph Stack["Compose stack (dev + prod)"]
|
subgraph Stack["Compose stack (dev + prod)"]
|
||||||
UI["frontend — React UI (3000)"]
|
UI["frontend — React UI (3000)"]
|
||||||
API["app — FastAPI (8000)<br/>+ embedded flow-watcher"]
|
API["api — FastAPI (8000)<br/>+ embedded flow-watcher"]
|
||||||
DB[("app.db — SQLite file")]
|
DB[("app.db — SQLite file")]
|
||||||
LF["langflow — authoring UI (7860)<br/>langflow.db — SQLite file"]
|
LF["langflow — authoring UI (7860)<br/>langflow.db — SQLite file"]
|
||||||
LFX["lfx — flow runtime (6004)"]
|
LFX["lfx — flow runtime (6004)"]
|
||||||
WT["watchtower — nightly image updates"]
|
WT["watchtower — nightly image updates (prod)"]
|
||||||
end
|
end
|
||||||
UI --> API
|
UI --> API
|
||||||
FLOWS -->|watch + sync| API
|
FLOWS -->|watch + sync| API
|
||||||
|
|
@ -51,8 +55,8 @@ flowchart LR
|
||||||
|
|
||||||
| Service | Purpose | URL |
|
| Service | Purpose | URL |
|
||||||
| -------------- | ------------------------------------ | -------------------------- |
|
| -------------- | ------------------------------------ | -------------------------- |
|
||||||
| React frontend | Web UI (Vite) | http://localhost:3000 |
|
| React frontend | Web UI (Vite, HMR in dev) | http://localhost:3000 |
|
||||||
| FastAPI backend| API + flow orchestration | http://localhost:8000 |
|
| FastAPI backend| API + flow orchestration (reload in dev) | http://localhost:8000 |
|
||||||
| API docs | Swagger UI | http://localhost:8000/api/docs |
|
| API docs | Swagger UI | http://localhost:8000/api/docs |
|
||||||
| Langflow | Visual flow authoring | http://localhost:7860 |
|
| Langflow | Visual flow authoring | http://localhost:7860 |
|
||||||
| LFX | Flow runtime (internal, no direct UI)| localhost:6004 |
|
| LFX | Flow runtime (internal, no direct UI)| localhost:6004 |
|
||||||
|
|
@ -63,23 +67,22 @@ Langflow itself runs on SQLite by default — so the whole stack works without
|
||||||
a database server. Postgres can return later by changing `DATABASE_URL`
|
a database server. Postgres can return later by changing `DATABASE_URL`
|
||||||
(migrations stay portable).
|
(migrations stay portable).
|
||||||
|
|
||||||
|
`.env` holds only ubiquitous variables (identical value in dev and prod:
|
||||||
|
secrets, service URLs). Dev-only variables travel via `devcontainer.json`
|
||||||
|
(`APP_ENV=dev`); paths resolve from the repo root in code.
|
||||||
|
|
||||||
## Getting Started
|
## Getting Started
|
||||||
|
|
||||||
1. Copy `.env.example` to `.env` and fill in the secrets.
|
1. Copy `.env.example` to `.env` and fill in the secrets.
|
||||||
2. Open the repo in VS Code → "Reopen in Container". Setup installs
|
2. Open the repo in VS Code → "Reopen in Container". Setup installs
|
||||||
dependencies, migrates the database, and syncs `backend/flows/`.
|
dependencies and syncs `backend/flows/`; the entrypoints migrate and
|
||||||
3. Run the backend from `backend/`: `uv run uvicorn app.main:app --port 8000`.
|
serve automatically.
|
||||||
Run the frontend from `frontend/`: `npm run dev`.
|
3. The stack is already running: UI at http://localhost:3000, API docs at
|
||||||
|
http://localhost:8000/api/docs.
|
||||||
Production (self-hosted, internal use — no TLS/reverse proxy included):
|
|
||||||
|
|
||||||
```sh
|
|
||||||
docker compose -f compose.prod.yml up -d --build
|
|
||||||
```
|
|
||||||
|
|
||||||
## Development
|
## Development
|
||||||
|
|
||||||
Backend (from `backend/`):
|
Backend (attached `api` container, from `backend/`):
|
||||||
|
|
||||||
- `uv run pytest` — full test suite (throwaway SQLite file, no external DB).
|
- `uv run pytest` — full test suite (throwaway SQLite file, no external DB).
|
||||||
- `uv run alembic upgrade head` — migrate the dev database.
|
- `uv run alembic upgrade head` — migrate the dev database.
|
||||||
|
|
@ -92,12 +95,33 @@ Backend (from `backend/`):
|
||||||
flows (writes test fixtures and `backend/flows/`).
|
flows (writes test fixtures and `backend/flows/`).
|
||||||
- `uv run python scripts/smoke_e2e.py` — manual end-to-end smoke.
|
- `uv run python scripts/smoke_e2e.py` — manual end-to-end smoke.
|
||||||
|
|
||||||
Frontend (from `frontend/`): `npm install`, `npm run dev`, `npm run build`.
|
Frontend (node lives in the `frontend` container):
|
||||||
|
|
||||||
|
- `docker compose exec frontend npm run dev` — HMR dev server (already
|
||||||
|
running via compose; this is for a second instance or flags).
|
||||||
|
- `docker compose exec frontend npm run build` — typecheck + build.
|
||||||
|
- Without a docker socket: VS Code "Attach to Running Container" → frontend.
|
||||||
|
|
||||||
Flow workflow: design in Langflow → export into `backend/flows/<name>.json`
|
Flow workflow: design in Langflow → export into `backend/flows/<name>.json`
|
||||||
(`--pull`, or UI download) → commit → the watcher deploys it automatically.
|
(`--pull`, or UI download) → commit → the watcher deploys it automatically.
|
||||||
Deleting a file removes the flow including its runs (git keeps the JSON).
|
Deleting a file removes the flow including its runs (git keeps the JSON).
|
||||||
|
|
||||||
|
## Production (self-hosted, internal use — no TLS/reverse proxy included)
|
||||||
|
|
||||||
|
1. Check out the repo on the host, create `.env` from `.env.example` with
|
||||||
|
real secrets and `FRONTEND_MODE=preview`.
|
||||||
|
2. `docker compose up -d --build`.
|
||||||
|
3. Code updates via `git pull` + `docker compose restart api frontend`
|
||||||
|
(the checkout is bind-mounted); image rebuilds only for dependency
|
||||||
|
changes (`docker compose up -d --build`).
|
||||||
|
|
||||||
Container updates: registry images (`langflowai/langflow:latest`)
|
Container updates: registry images (`langflowai/langflow:latest`)
|
||||||
auto-update nightly via Watchtower (`nickfedor/watchtower`). Locally built
|
auto-update nightly via Watchtower (`nickfedor/watchtower`). Locally built
|
||||||
images (`app`, `lfx`, `frontend`) update with `docker compose build --pull`.
|
images (`api`, `lfx`, `frontend`) update with `docker compose build --pull`.
|
||||||
|
|
||||||
|
## Continuous integration
|
||||||
|
|
||||||
|
Workflows in `.forgejo/workflows/` run on Forgejo: backend pytest, frontend
|
||||||
|
build, compose validation. They need a Runner with the `docker` label online
|
||||||
|
(repo Settings → Actions → Runners) and action resolution for
|
||||||
|
`actions/checkout` (github proxy or `code.forgejo.org` mirror).
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,6 @@
|
||||||
# Production image for the FastAPI backend.
|
# Backend image for dev and prod (single image, full dependencies).
|
||||||
|
# Dev specifics (APP_ENV=dev for autoreload, opencode, SSH keys) arrive via
|
||||||
|
# devcontainer.json / setup.sh — never via this file.
|
||||||
FROM python:3.12-slim
|
FROM python:3.12-slim
|
||||||
|
|
||||||
ENV PYTHONUNBUFFERED=1 \
|
ENV PYTHONUNBUFFERED=1 \
|
||||||
|
|
@ -6,16 +8,24 @@ ENV PYTHONUNBUFFERED=1 \
|
||||||
|
|
||||||
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
|
||||||
|
|
||||||
WORKDIR /srv/backend
|
# Foundational, inert in prod: non-root user for the editor, sudo for setup,
|
||||||
COPY pyproject.toml alembic.ini ./
|
# openssh-client for the devcontainer post-start hook.
|
||||||
|
RUN apt-get update \
|
||||||
|
&& apt-get install -y --no-install-recommends sudo openssh-client \
|
||||||
|
&& rm -rf /var/lib/apt/lists/* \
|
||||||
|
&& useradd -m -s /bin/bash vscode \
|
||||||
|
&& echo "vscode ALL=(ALL) NOPASSWD:ALL" > /etc/sudoers.d/vscode
|
||||||
|
|
||||||
|
WORKDIR /srv/kirche-einfach/backend
|
||||||
|
COPY pyproject.toml alembic.ini entrypoint.sh ./
|
||||||
COPY app ./app
|
COPY app ./app
|
||||||
COPY migrations ./migrations
|
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.
|
||||||
RUN uv sync --no-dev
|
# Full groups (incl. dev): pytest and friends must work in dev with zero steps.
|
||||||
|
RUN uv sync && chmod +x entrypoint.sh
|
||||||
|
|
||||||
ENV FLOWS_WATCH_ENABLED=true
|
ENV FLOWS_WATCH_ENABLED=true
|
||||||
EXPOSE 8000
|
EXPOSE 8000
|
||||||
|
|
||||||
# Migrate on start, then serve (watcher reconciles backend/flows automatically).
|
ENTRYPOINT ["./entrypoint.sh"]
|
||||||
CMD ["sh", "-c", ".venv/bin/alembic upgrade head && exec .venv/bin/uvicorn app.main:app --host 0.0.0.0 --port 8000"]
|
|
||||||
|
|
|
||||||
|
|
@ -27,7 +27,7 @@ class Settings(BaseSettings):
|
||||||
lfx_api_key: str = ""
|
lfx_api_key: str = ""
|
||||||
llm_api_key: str = ""
|
llm_api_key: str = ""
|
||||||
# Base URL flows use for callbacks into this API. Must be reachable
|
# Base URL flows use for callbacks into this API. Must be reachable
|
||||||
# FROM the LFX process (compose: http://app:8000).
|
# FROM the LFX process (compose: http://api:8000).
|
||||||
callback_base_url: str = "http://localhost:8000"
|
callback_base_url: str = "http://localhost:8000"
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
15
backend/entrypoint.sh
Normal file
15
backend/entrypoint.sh
Normal file
|
|
@ -0,0 +1,15 @@
|
||||||
|
#!/bin/sh
|
||||||
|
# Backend entrypoint (dev + prod): migrate, then serve.
|
||||||
|
# APP_ENV=dev (set via devcontainer.json containerEnv, attached container
|
||||||
|
# only) enables autoreload. Anything else runs the plain production server.
|
||||||
|
set -e
|
||||||
|
|
||||||
|
cd /srv/kirche-einfach/backend
|
||||||
|
|
||||||
|
.venv/bin/alembic upgrade head
|
||||||
|
|
||||||
|
if [ "${APP_ENV:-prod}" = "dev" ]; then
|
||||||
|
exec .venv/bin/uvicorn app.main:app --host 0.0.0.0 --port 8000 --reload
|
||||||
|
else
|
||||||
|
exec .venv/bin/uvicorn app.main:app --host 0.0.0.0 --port 8000
|
||||||
|
fi
|
||||||
|
|
@ -1,29 +1,23 @@
|
||||||
# Minimal self-hosted deployment (internal use: no TLS / reverse proxy).
|
# The single compose file for dev and prod.
|
||||||
#
|
#
|
||||||
# Flows are the bind-mounted ./backend/flows directory (source of truth);
|
# Dev: VS Code attaches to `api` (see .devcontainer/devcontainer.json) and
|
||||||
# the embedded API watcher reconciles them into the DB + LFX automatically.
|
# starts `runServices` (everything but watchtower). Dev specifics
|
||||||
# Registry images (:latest) auto-update nightly via watchtower; locally
|
# (autoreload, editor, SSH keys, docker CLI) come from devcontainer.json.
|
||||||
# built images (app, lfx, frontend) update via `docker compose build --pull`.
|
# Prod: `docker compose up -d --build` (own .env with secrets +
|
||||||
#
|
# FRONTEND_MODE=preview). Code updates via `git pull` + restart; image
|
||||||
# Usage: docker compose -f compose.prod.yml up -d --build
|
# rebuilds only for dependency changes.
|
||||||
services:
|
services:
|
||||||
app:
|
api:
|
||||||
build:
|
build:
|
||||||
context: ./backend
|
context: ./backend
|
||||||
dockerfile: Dockerfile
|
dockerfile: Dockerfile
|
||||||
env_file:
|
env_file:
|
||||||
- .env
|
- .env
|
||||||
environment:
|
|
||||||
DATABASE_URL: sqlite:////srv/backend/data/kirche.db
|
|
||||||
FLOWS_DIR: /srv/backend/flows
|
|
||||||
LFX_URL: http://lfx:6004
|
|
||||||
LANGFLOW_URL: http://langflow:7860
|
|
||||||
CALLBACK_BASE_URL: http://app:8000
|
|
||||||
volumes:
|
volumes:
|
||||||
- ./backend/flows:/srv/backend/flows
|
- .:/srv/kirche-einfach
|
||||||
- appdata:/srv/backend/data
|
- kirche-venv:/srv/kirche-einfach/backend/.venv
|
||||||
ports:
|
ports:
|
||||||
- "8000:8000"
|
- "8000:8000" # FastAPI
|
||||||
depends_on:
|
depends_on:
|
||||||
lfx:
|
lfx:
|
||||||
condition: service_healthy
|
condition: service_healthy
|
||||||
|
|
@ -45,10 +39,18 @@ services:
|
||||||
build:
|
build:
|
||||||
context: ./frontend
|
context: ./frontend
|
||||||
dockerfile: Dockerfile
|
dockerfile: Dockerfile
|
||||||
|
env_file:
|
||||||
|
- .env
|
||||||
|
environment:
|
||||||
|
# Dev default (HMR); prod .env sets `preview` (build + serve bundle).
|
||||||
|
FRONTEND_MODE: ${FRONTEND_MODE:-dev}
|
||||||
|
volumes:
|
||||||
|
- .:/srv/kirche-einfach
|
||||||
|
- kirche-node-modules:/srv/kirche-einfach/frontend/node_modules
|
||||||
ports:
|
ports:
|
||||||
- "3000:3000"
|
- "3000:3000" # Vite
|
||||||
depends_on:
|
depends_on:
|
||||||
app:
|
api:
|
||||||
condition: service_healthy
|
condition: service_healthy
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
|
|
||||||
|
|
@ -57,11 +59,14 @@ services:
|
||||||
env_file:
|
env_file:
|
||||||
- .env
|
- .env
|
||||||
environment:
|
environment:
|
||||||
|
# Static values only; secrets/URLs come from .env via env_file.
|
||||||
LANGFLOW_CONFIG_DIR: /app/langflow
|
LANGFLOW_CONFIG_DIR: /app/langflow
|
||||||
LANGFLOW_HOST: 0.0.0.0
|
LANGFLOW_HOST: 0.0.0.0
|
||||||
PORT: "7860"
|
PORT: "7860"
|
||||||
LANGFLOW_AUTO_LOGIN: "false"
|
LANGFLOW_AUTO_LOGIN: "false"
|
||||||
LANGFLOW_API_KEY_SOURCE: env
|
LANGFLOW_API_KEY_SOURCE: env
|
||||||
|
# SQLite file inside the langflow-data volume (absolute path required
|
||||||
|
# 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
|
||||||
volumes:
|
volumes:
|
||||||
- langflow-data:/app/langflow
|
- langflow-data:/app/langflow
|
||||||
|
|
@ -83,7 +88,7 @@ services:
|
||||||
|
|
||||||
lfx:
|
lfx:
|
||||||
build:
|
build:
|
||||||
context: .devcontainer
|
context: .
|
||||||
dockerfile: Dockerfile.lfx
|
dockerfile: Dockerfile.lfx
|
||||||
env_file:
|
env_file:
|
||||||
- .env
|
- .env
|
||||||
|
|
@ -103,6 +108,8 @@ services:
|
||||||
retries: 12
|
retries: 12
|
||||||
start_period: 30s
|
start_period: 30s
|
||||||
|
|
||||||
|
# Nightly auto-updates of :latest images (prod). Never started in dev
|
||||||
|
# (not in devcontainer.json runServices; also needs a docker socket).
|
||||||
watchtower:
|
watchtower:
|
||||||
image: nickfedor/watchtower:latest
|
image: nickfedor/watchtower:latest
|
||||||
volumes:
|
volumes:
|
||||||
|
|
@ -117,6 +124,7 @@ services:
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
|
|
||||||
volumes:
|
volumes:
|
||||||
appdata:
|
kirche-venv:
|
||||||
|
kirche-node-modules:
|
||||||
langflow-data:
|
langflow-data:
|
||||||
lfx-data:
|
lfx-data:
|
||||||
|
|
@ -1,13 +1,13 @@
|
||||||
# Production image for the React UI (build + Vite preview; internal use).
|
# Frontend image for dev and prod (single image).
|
||||||
|
# Dev specifics arrive via devcontainer.json / compose env — never via this
|
||||||
|
# file. DevDeps are included: `npm run dev` must work with zero steps.
|
||||||
FROM node:22-slim
|
FROM node:22-slim
|
||||||
|
|
||||||
WORKDIR /srv/frontend
|
WORKDIR /srv/kirche-einfach/frontend
|
||||||
COPY package.json package-lock.json ./
|
COPY package.json package-lock.json entrypoint.sh ./
|
||||||
RUN npm ci
|
RUN npm ci && chmod +x entrypoint.sh
|
||||||
COPY . ./
|
COPY . ./
|
||||||
RUN npm run build
|
|
||||||
|
|
||||||
ENV VITE_API_PROXY_TARGET=http://app:8000
|
|
||||||
EXPOSE 3000
|
EXPOSE 3000
|
||||||
|
|
||||||
CMD ["npm", "run", "preview", "--", "--host", "0.0.0.0", "--port", "3000"]
|
ENTRYPOINT ["./entrypoint.sh"]
|
||||||
|
|
|
||||||
14
frontend/entrypoint.sh
Normal file
14
frontend/entrypoint.sh
Normal file
|
|
@ -0,0 +1,14 @@
|
||||||
|
#!/bin/sh
|
||||||
|
# Frontend entrypoint (dev + prod).
|
||||||
|
# FRONTEND_MODE=dev (compose default) runs the HMR dev server.
|
||||||
|
# FRONTEND_MODE=preview (prod .env) builds once and serves the bundle.
|
||||||
|
set -e
|
||||||
|
|
||||||
|
cd /srv/kirche-einfach/frontend
|
||||||
|
|
||||||
|
if [ "${FRONTEND_MODE:-dev}" = "dev" ]; then
|
||||||
|
exec npm run dev -- --host 0.0.0.0 --port 3000
|
||||||
|
else
|
||||||
|
npm run build
|
||||||
|
exec npm run preview -- --host 0.0.0.0 --port 3000
|
||||||
|
fi
|
||||||
|
|
@ -1,13 +1,15 @@
|
||||||
import { defineConfig } from "vite";
|
import { defineConfig } from "vite";
|
||||||
import react from "@vitejs/plugin-react";
|
import react from "@vitejs/plugin-react";
|
||||||
|
|
||||||
|
// Both dev server and preview proxy /api to the backend service.
|
||||||
|
// `api` is the compose service name, identical in dev and prod.
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
plugins: [react()],
|
plugins: [react()],
|
||||||
server: {
|
server: {
|
||||||
port: 3000,
|
port: 3000,
|
||||||
proxy: {
|
proxy: {
|
||||||
"/api": {
|
"/api": {
|
||||||
target: "http://localhost:8000",
|
target: "http://api:8000",
|
||||||
changeOrigin: true,
|
changeOrigin: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
@ -16,8 +18,7 @@ export default defineConfig({
|
||||||
port: 3000,
|
port: 3000,
|
||||||
proxy: {
|
proxy: {
|
||||||
"/api": {
|
"/api": {
|
||||||
// Overridable at container start (prod compose: http://app:8000).
|
target: "http://api:8000",
|
||||||
target: process.env.VITE_API_PROXY_TARGET ?? "http://localhost:8000",
|
|
||||||
changeOrigin: true,
|
changeOrigin: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue