From 2ba062c4c5c459de2caae0fe9e61ca4899781cb9 Mon Sep 17 00:00:00 2001 From: Jonas Miederer Date: Sun, 6 Sep 2026 22:23:30 +0000 Subject: [PATCH] feat: compose stack in devcontainer (db, langflow, lfx) - docker-compose.yml with app/db/langflow/lfx services and healthchecks - Dockerfile.lfx (stateless flow runtime on :6004) - init-db.sql (langflow + test databases) - devcontainer.json switched to compose mode with runServices - setup.sh waits for compose services and requires .env --- .devcontainer/Dockerfile.lfx | 19 +++++++ .devcontainer/devcontainer.json | 13 ++--- .devcontainer/docker-compose.yml | 91 ++++++++++++++++++++++++++++++++ .devcontainer/init-db.sql | 7 +++ .devcontainer/setup.sh | 32 +++++++++++ .env.example | 1 + 6 files changed, 154 insertions(+), 9 deletions(-) create mode 100644 .devcontainer/Dockerfile.lfx create mode 100644 .devcontainer/docker-compose.yml create mode 100644 .devcontainer/init-db.sql diff --git a/.devcontainer/Dockerfile.lfx b/.devcontainer/Dockerfile.lfx new file mode 100644 index 0000000..42c9480 --- /dev/null +++ b/.devcontainer/Dockerfile.lfx @@ -0,0 +1,19 @@ +# Stateless Langflow flow runtime (LFX). Serves versioned flow JSON files +# deployed by our backend via POST /flows/upload/ (persisted in /flows). +# LFX stays on the internal compose network; our FastAPI is the only +# public surface and proxies run/stream calls. +FROM python:3.12-slim + +ENV PYTHONUNBUFFERED=1 \ + PIP_NO_CACHE_DIR=1 + +# lfx = flow runtime; requests = convenience for custom-component code. +RUN pip install "lfx==1.12.0" requests + +RUN mkdir -p /flows +VOLUME /flows +EXPOSE 6004 + +# Serve the /flows dir (uploaded flows persist there via --flow-dir). +# Flags verified against `lfx serve --help` (lfx 1.12.0). +CMD ["lfx", "serve", "--host", "0.0.0.0", "--port", "6004", "--flow-dir", "/flows", "/flows"] diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index 1ce1b7e..b396724 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -1,15 +1,10 @@ { "name": "kirche-einfach", - "build": { - "dockerfile": "Dockerfile", - "args": { - "FORGEJO_USERNAME": "jonas" - } - }, + "dockerComposeFile": "docker-compose.yml", + "service": "app", + "runServices": ["db", "langflow", "lfx"], "mounts": [ - "source=${localEnv:USERPROFILE}/.ssh,target=/mnt/host-ssh,type=bind,readonly", - "source=kirche-einfach-venv,target=/workspaces/kirche-einfach/backend/.venv,type=volume", - "source=kirche-einfach-node-modules,target=/workspaces/kirche-einfach/frontend/node_modules,type=volume" + "source=${localEnv:USERPROFILE}/.ssh,target=/mnt/host-ssh,type=bind,readonly" ], "remoteUser": "vscode", "containerEnv": { diff --git a/.devcontainer/docker-compose.yml b/.devcontainer/docker-compose.yml new file mode 100644 index 0000000..a448b6d --- /dev/null +++ b/.devcontainer/docker-compose.yml @@ -0,0 +1,91 @@ +# 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 + + db: + image: postgres:16-trixie + env_file: + - ../.env + volumes: + - postgres-data:/var/lib/postgresql/data + - ./init-db.sql:/docker-entrypoint-initdb.d/init-db.sql:ro + ports: + - "5432:5432" + healthcheck: + test: ["CMD-SHELL", "pg_isready -U postgres"] + interval: 5s + timeout: 3s + retries: 10 + + langflow: + image: langflowai/langflow:1.12.0 + depends_on: + db: + condition: service_healthy + 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 + 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 + 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 + +volumes: + kirche-einfach-venv: + kirche-einfach-node-modules: + postgres-data: + langflow-data: + lfx-data: diff --git a/.devcontainer/init-db.sql b/.devcontainer/init-db.sql new file mode 100644 index 0000000..795450a --- /dev/null +++ b/.devcontainer/init-db.sql @@ -0,0 +1,7 @@ +-- Runs once on first init of the postgres-data volume. +-- The main app database ($POSTGRES_DB) is created by the image itself. +-- Langflow (authoring UI) gets its own database on the same instance. +CREATE DATABASE langflow; + +-- Isolated database for the pytest suite (migrated via Alembic in conftest). +CREATE DATABASE kirche_einfach_test; diff --git a/.devcontainer/setup.sh b/.devcontainer/setup.sh index d2f9d7d..5b9be3b 100644 --- a/.devcontainer/setup.sh +++ b/.devcontainer/setup.sh @@ -13,6 +13,38 @@ echo "--- Installing Node dependencies ---" cd /workspaces/kirche-einfach/frontend npm install +# Compose services (db, langflow, lfx) must be up; fail loudly otherwise. +echo "--- Waiting for compose services ---" +if [ ! -f /workspaces/kirche-einfach/.env ]; then + echo "ERROR: /workspaces/kirche-einfach/.env is missing. Copy .env.example to .env and fill in secrets." >&2 + exit 1 +fi +python3 - <<'EOF' +import socket +import sys +import time + +targets = [("db", 5432), ("langflow", 7860), ("lfx", 6004)] +deadline = time.time() + 180 +pending = dict(targets) +while pending and time.time() < deadline: + for host, port in list(pending): + s = socket.socket() + s.settimeout(2) + try: + s.connect((host, port)) + print(f"OK {host}:{port}", flush=True) + del pending[(host, port)] + except OSError: + pass + finally: + s.close() + if pending: + time.sleep(2) +if pending: + sys.exit(f"ERROR: compose services not ready: {sorted(pending)}") +EOF + echo "" echo "=== Setup complete ===" echo "Forgejo username: $FORGEJO_USERNAME" diff --git a/.env.example b/.env.example index 7711661..6623210 100644 --- a/.env.example +++ b/.env.example @@ -15,6 +15,7 @@ LANGFLOW_SUPERUSER_PASSWORD= LANGFLOW_SECRET_KEY= # Single static key; validated against env (LANGFLOW_API_KEY_SOURCE=env), superuser scope. LANGFLOW_API_KEY=sk- +LANGFLOW_DATABASE_URL=postgresql://kirche:@db:5432/langflow # --- lfx (flow runtime) --- LFX_URL=http://lfx:6004