From af993bd43f3755ac1b62fb257844183b52049034 Mon Sep 17 00:00:00 2001 From: Jonas Miederer Date: Mon, 7 Sep 2026 19:46:17 +0000 Subject: [PATCH] feat: unify on single repo-root compose file for dev and prod --- .devcontainer/Dockerfile | 48 ---------- .devcontainer/devcontainer.json | 21 +++-- .devcontainer/docker-compose.yml | 92 ------------------- .devcontainer/setup.sh | 38 ++++---- .env.example | 19 ++-- .forgejo/workflows/backend.yml | 22 +++++ .forgejo/workflows/frontend.yml | 20 ++++ .forgejo/workflows/infra.yml | 22 +++++ AGENTS.md | 20 ++++ .../Dockerfile.lfx => Dockerfile.lfx | 0 README.md | 56 +++++++---- backend/Dockerfile | 22 +++-- backend/app/settings.py | 2 +- backend/entrypoint.sh | 15 +++ compose.prod.yml => docker-compose.yml | 50 +++++----- frontend/Dockerfile | 14 +-- frontend/entrypoint.sh | 14 +++ frontend/vite.config.ts | 7 +- 18 files changed, 253 insertions(+), 229 deletions(-) delete mode 100644 .devcontainer/Dockerfile delete mode 100644 .devcontainer/docker-compose.yml create mode 100644 .forgejo/workflows/backend.yml create mode 100644 .forgejo/workflows/frontend.yml create mode 100644 .forgejo/workflows/infra.yml rename .devcontainer/Dockerfile.lfx => Dockerfile.lfx (100%) create mode 100644 backend/entrypoint.sh rename compose.prod.yml => docker-compose.yml (64%) create mode 100644 frontend/entrypoint.sh diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile deleted file mode 100644 index ce24bff..0000000 --- a/.devcontainer/Dockerfile +++ /dev/null @@ -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"] diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index a162f71..41ed32c 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -1,17 +1,22 @@ { "name": "kirche-einfach", - "dockerComposeFile": "docker-compose.yml", - "service": "app", - "runServices": ["db", "langflow", "lfx"], - "mounts": [ - "source=${localEnv:USERPROFILE}/.ssh,target=/mnt/host-ssh,type=bind,readonly" - ], + "dockerComposeFile": ["../docker-compose.yml"], + "service": "api", + "runServices": ["api", "frontend", "langflow", "lfx"], + "workspaceFolder": "/srv/kirche-einfach", "remoteUser": "vscode", "containerEnv": { + "APP_ENV": "dev", "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, - "workspaceFolder": "/workspaces/kirche-einfach", "postCreateCommand": "sudo -E bash .devcontainer/setup.sh", "postStartCommand": "bash .devcontainer/postStart.sh", "customizations": { @@ -24,7 +29,7 @@ "esbenp.prettier-vscode" ], "settings": { - "python.defaultInterpreterPath": "/workspaces/kirche-einfach/backend/.venv/bin/python" + "python.defaultInterpreterPath": "/srv/kirche-einfach/backend/.venv/bin/python" } } } diff --git a/.devcontainer/docker-compose.yml b/.devcontainer/docker-compose.yml deleted file mode 100644 index 9acbe67..0000000 --- a/.devcontainer/docker-compose.yml +++ /dev/null @@ -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: \ No newline at end of file diff --git a/.devcontainer/setup.sh b/.devcontainer/setup.sh index 1ed3096..d002adf 100644 --- a/.devcontainer/setup.sh +++ b/.devcontainer/setup.sh @@ -1,22 +1,30 @@ #!/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 +REPO=/srv/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 ---" -cd /workspaces/kirche-einfach/backend +cd "$REPO/backend" uv sync -# Frontend -echo "--- Installing Node dependencies ---" -cd /workspaces/kirche-einfach/frontend -npm install +# opencode CLI (dev tooling; intentionally not baked into the image). +if [ ! -x /home/vscode/.opencode/bin/opencode ]; then + echo "--- Installing opencode ---" + 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. +# (api/frontend are starting alongside this script.) 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 +if [ ! -f "$REPO/.env" ]; then + echo "ERROR: $REPO/.env is missing. Copy .env.example to .env and fill in secrets." >&2 exit 1 fi python3 - <<'EOF' @@ -45,19 +53,17 @@ if pending: sys.exit(f"ERROR: compose services not ready: {sorted(pending)}") EOF -# Migrate the SQLite schema and reconcile backend/flows into the DB + LFX. -echo "--- Migrating database ---" -cd /workspaces/kirche-einfach/backend -uv run alembic upgrade head - +# Reconcile backend/flows into the DB + LFX (schema is migrated by the api +# entrypoint on every start). 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)" echo "" echo "=== Setup complete ===" echo "Forgejo username: $FORGEJO_USERNAME" -# Hand dirs back to vscode (this script runs via sudo; uv/npm create caches as root) -chown -R vscode:vscode /workspaces/kirche-einfach/backend/.venv \ - /workspaces/kirche-einfach/frontend/node_modules \ +# 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 diff --git a/.env.example b/.env.example index cb90e32..e65f05c 100644 --- a/.env.example +++ b/.env.example @@ -1,16 +1,10 @@ # 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= -# --- app database (SQLite, default if unset) --- -# 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 (authoring UI) --- LANGFLOW_URL=http://langflow:7860 LANGFLOW_SUPERUSER=langflow LANGFLOW_SUPERUSER_PASSWORD= @@ -24,7 +18,10 @@ LFX_URL=http://lfx:6004 LFX_API_KEY=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_API_KEY= \ No newline at end of file diff --git a/.forgejo/workflows/backend.yml b/.forgejo/workflows/backend.yml new file mode 100644 index 0000000..34217c1 --- /dev/null +++ b/.forgejo/workflows/backend.yml @@ -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 diff --git a/.forgejo/workflows/frontend.yml b/.forgejo/workflows/frontend.yml new file mode 100644 index 0000000..e09fb26 --- /dev/null +++ b/.forgejo/workflows/frontend.yml @@ -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 diff --git a/.forgejo/workflows/infra.yml b/.forgejo/workflows/infra.yml new file mode 100644 index 0000000..f083c14 --- /dev/null +++ b/.forgejo/workflows/infra.yml @@ -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 diff --git a/AGENTS.md b/AGENTS.md index a964607..910e1da 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -13,6 +13,26 @@ Backend (run from `backend/`): 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 `, `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 - New behavior has tests; `uv run pytest` is green before reporting completion. diff --git a/.devcontainer/Dockerfile.lfx b/Dockerfile.lfx similarity index 100% rename from .devcontainer/Dockerfile.lfx rename to Dockerfile.lfx diff --git a/README.md b/README.md index 46b87c4..8ca5514 100644 --- a/README.md +++ b/README.md @@ -27,6 +27,10 @@ needs judgment. Every step is documented and traceable. ## 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 flowchart LR subgraph Repo["Git repo — source of truth"] @@ -34,11 +38,11 @@ flowchart LR end subgraph Stack["Compose stack (dev + prod)"] UI["frontend — React UI (3000)"] - API["app — FastAPI (8000)
+ embedded flow-watcher"] + API["api — FastAPI (8000)
+ embedded flow-watcher"] DB[("app.db — SQLite file")] LF["langflow — authoring UI (7860)
langflow.db — SQLite file"] LFX["lfx — flow runtime (6004)"] - WT["watchtower — nightly image updates"] + WT["watchtower — nightly image updates (prod)"] end UI --> API FLOWS -->|watch + sync| API @@ -51,8 +55,8 @@ flowchart LR | Service | Purpose | URL | | -------------- | ------------------------------------ | -------------------------- | -| React frontend | Web UI (Vite) | http://localhost:3000 | -| FastAPI backend| API + flow orchestration | http://localhost:8000 | +| React frontend | Web UI (Vite, HMR in dev) | http://localhost:3000 | +| FastAPI backend| API + flow orchestration (reload in dev) | http://localhost:8000 | | API docs | Swagger UI | http://localhost:8000/api/docs | | Langflow | Visual flow authoring | http://localhost:7860 | | 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` (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 1. Copy `.env.example` to `.env` and fill in the secrets. 2. Open the repo in VS Code → "Reopen in Container". Setup installs - dependencies, migrates the database, and syncs `backend/flows/`. -3. Run the backend from `backend/`: `uv run uvicorn app.main:app --port 8000`. - Run the frontend from `frontend/`: `npm run dev`. - -Production (self-hosted, internal use — no TLS/reverse proxy included): - -```sh -docker compose -f compose.prod.yml up -d --build -``` + dependencies and syncs `backend/flows/`; the entrypoints migrate and + serve automatically. +3. The stack is already running: UI at http://localhost:3000, API docs at + http://localhost:8000/api/docs. ## Development -Backend (from `backend/`): +Backend (attached `api` container, from `backend/`): - `uv run pytest` — full test suite (throwaway SQLite file, no external DB). - `uv run alembic upgrade head` — migrate the dev database. @@ -92,12 +95,33 @@ Backend (from `backend/`): flows (writes test fixtures and `backend/flows/`). - `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/.json` (`--pull`, or UI download) → commit → the watcher deploys it automatically. 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`) 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). diff --git a/backend/Dockerfile b/backend/Dockerfile index d89de3e..7c5cdbb 100644 --- a/backend/Dockerfile +++ b/backend/Dockerfile @@ -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 ENV PYTHONUNBUFFERED=1 \ @@ -6,16 +8,24 @@ ENV PYTHONUNBUFFERED=1 \ COPY --from=ghcr.io/astral-sh/uv:latest /uv /usr/local/bin/uv -WORKDIR /srv/backend -COPY pyproject.toml alembic.ini ./ +# Foundational, inert in prod: non-root user for the editor, sudo for setup, +# 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 migrations ./migrations COPY scripts ./scripts # 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 EXPOSE 8000 -# Migrate on start, then serve (watcher reconciles backend/flows automatically). -CMD ["sh", "-c", ".venv/bin/alembic upgrade head && exec .venv/bin/uvicorn app.main:app --host 0.0.0.0 --port 8000"] +ENTRYPOINT ["./entrypoint.sh"] diff --git a/backend/app/settings.py b/backend/app/settings.py index f3373df..5c59dea 100644 --- a/backend/app/settings.py +++ b/backend/app/settings.py @@ -27,7 +27,7 @@ class Settings(BaseSettings): lfx_api_key: str = "" llm_api_key: str = "" # 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" diff --git a/backend/entrypoint.sh b/backend/entrypoint.sh new file mode 100644 index 0000000..71b0868 --- /dev/null +++ b/backend/entrypoint.sh @@ -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 diff --git a/compose.prod.yml b/docker-compose.yml similarity index 64% rename from compose.prod.yml rename to docker-compose.yml index 6afd1ea..cc0eec2 100644 --- a/compose.prod.yml +++ b/docker-compose.yml @@ -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); -# the embedded API watcher reconciles them into the DB + LFX automatically. -# Registry images (:latest) auto-update nightly via watchtower; locally -# built images (app, lfx, frontend) update via `docker compose build --pull`. -# -# Usage: docker compose -f compose.prod.yml up -d --build +# Dev: VS Code attaches to `api` (see .devcontainer/devcontainer.json) and +# starts `runServices` (everything but watchtower). Dev specifics +# (autoreload, editor, SSH keys, docker CLI) come from devcontainer.json. +# Prod: `docker compose up -d --build` (own .env with secrets + +# FRONTEND_MODE=preview). Code updates via `git pull` + restart; image +# rebuilds only for dependency changes. services: - app: + api: build: context: ./backend dockerfile: Dockerfile env_file: - .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: - - ./backend/flows:/srv/backend/flows - - appdata:/srv/backend/data + - .:/srv/kirche-einfach + - kirche-venv:/srv/kirche-einfach/backend/.venv ports: - - "8000:8000" + - "8000:8000" # FastAPI depends_on: lfx: condition: service_healthy @@ -45,10 +39,18 @@ services: build: context: ./frontend 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: - - "3000:3000" + - "3000:3000" # Vite depends_on: - app: + api: condition: service_healthy restart: unless-stopped @@ -57,11 +59,14 @@ services: env_file: - .env environment: + # Static values only; secrets/URLs come from .env via env_file. 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 that may linger in .env. LANGFLOW_DATABASE_URL: sqlite:////app/langflow/langflow.db volumes: - langflow-data:/app/langflow @@ -83,7 +88,7 @@ services: lfx: build: - context: .devcontainer + context: . dockerfile: Dockerfile.lfx env_file: - .env @@ -103,6 +108,8 @@ services: retries: 12 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: image: nickfedor/watchtower:latest volumes: @@ -117,6 +124,7 @@ services: restart: unless-stopped volumes: - appdata: + kirche-venv: + kirche-node-modules: langflow-data: lfx-data: diff --git a/frontend/Dockerfile b/frontend/Dockerfile index 5fbf3ee..c9d2c0f 100644 --- a/frontend/Dockerfile +++ b/frontend/Dockerfile @@ -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 -WORKDIR /srv/frontend -COPY package.json package-lock.json ./ -RUN npm ci +WORKDIR /srv/kirche-einfach/frontend +COPY package.json package-lock.json entrypoint.sh ./ +RUN npm ci && chmod +x entrypoint.sh COPY . ./ -RUN npm run build -ENV VITE_API_PROXY_TARGET=http://app:8000 EXPOSE 3000 -CMD ["npm", "run", "preview", "--", "--host", "0.0.0.0", "--port", "3000"] +ENTRYPOINT ["./entrypoint.sh"] diff --git a/frontend/entrypoint.sh b/frontend/entrypoint.sh new file mode 100644 index 0000000..13d0ea0 --- /dev/null +++ b/frontend/entrypoint.sh @@ -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 diff --git a/frontend/vite.config.ts b/frontend/vite.config.ts index 9da1522..75eeef7 100644 --- a/frontend/vite.config.ts +++ b/frontend/vite.config.ts @@ -1,13 +1,15 @@ import { defineConfig } from "vite"; 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({ plugins: [react()], server: { port: 3000, proxy: { "/api": { - target: "http://localhost:8000", + target: "http://api:8000", changeOrigin: true, }, }, @@ -16,8 +18,7 @@ export default defineConfig({ port: 3000, proxy: { "/api": { - // Overridable at container start (prod compose: http://app:8000). - target: process.env.VITE_API_PROXY_TARGET ?? "http://localhost:8000", + target: "http://api:8000", changeOrigin: true, }, },