Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
85 changes: 85 additions & 0 deletions .github/workflows/deploy-app.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
name: Build And Deploy App

on:
push:
branches: ["main"]
workflow_dispatch:

permissions:
contents: read
packages: write

jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4

- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3

- name: Log in to GHCR
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}

- name: Extract image metadata
id: meta
uses: docker/metadata-action@v5
with:
images: ghcr.io/georgepearse/genesis
tags: |
type=raw,value=latest
type=sha

- name: Build and push image
uses: docker/build-push-action@v6
with:
context: .
file: ./Dockerfile
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
cache-from: type=gha
cache-to: type=gha,mode=max

deploy:
runs-on: ubuntu-latest
needs: build
if: ${{ secrets.DEPLOY_HOST != '' && secrets.DEPLOY_USER != '' && secrets.DEPLOY_SSH_KEY != '' && secrets.GHCR_USERNAME != '' && secrets.GHCR_PAT != '' && secrets.GENESIS_ENV != '' }}
steps:
- name: Checkout
uses: actions/checkout@v4

- name: Copy compose file
uses: appleboy/scp-action@v0.1.7
with:
host: ${{ secrets.DEPLOY_HOST }}
username: ${{ secrets.DEPLOY_USER }}
key: ${{ secrets.DEPLOY_SSH_KEY }}
source: "docker-compose.prod.yml"
target: ${{ secrets.DEPLOY_PATH || '/opt/genesis' }}

- name: Deploy on server
uses: appleboy/ssh-action@v1.2.0
env:
DEPLOY_PATH: ${{ secrets.DEPLOY_PATH || '/opt/genesis' }}
GHCR_USERNAME: ${{ secrets.GHCR_USERNAME }}
GHCR_PAT: ${{ secrets.GHCR_PAT }}
GENESIS_ENV: ${{ secrets.GENESIS_ENV }}
with:
host: ${{ secrets.DEPLOY_HOST }}
username: ${{ secrets.DEPLOY_USER }}
key: ${{ secrets.DEPLOY_SSH_KEY }}
envs: DEPLOY_PATH,GHCR_USERNAME,GHCR_PAT,GENESIS_ENV
script: |
set -euo pipefail
mkdir -p "$DEPLOY_PATH"
cd "$DEPLOY_PATH"
printf '%s' "$GENESIS_ENV" > .env
echo "$GHCR_PAT" | docker login ghcr.io -u "$GHCR_USERNAME" --password-stdin
docker compose -f docker-compose.prod.yml pull
docker compose -f docker-compose.prod.yml up -d
27 changes: 16 additions & 11 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -1,41 +1,46 @@
# Backend Dockerfile for Genesis Evolution Platform
FROM node:22-bookworm-slim AS frontend-build

WORKDIR /frontend

COPY genesis/webui/frontend/package.json ./
COPY genesis/webui/frontend/package-lock.json ./
RUN npm ci

COPY genesis/webui/frontend/ ./
RUN npm run build


FROM python:3.12-slim

# Set working directory
WORKDIR /app

# Install system dependencies
RUN apt-get update && apt-get install -y \
build-essential \
curl \
git \
postgresql-client \
&& rm -rf /var/lib/apt/lists/*

# Copy dependency files
COPY pyproject.toml ./
COPY README.md ./

# Copy application code
COPY genesis/ ./genesis/
COPY configs/ ./configs/
COPY examples/ ./examples/
COPY tests/ ./tests/

# Install Python dependencies
COPY --from=frontend-build /frontend/dist ./genesis/webui/frontend/dist

RUN pip install --no-cache-dir --upgrade pip && \
pip install --no-cache-dir -e .

# Create directory for results and database
RUN mkdir -p /app/results /app/data

# Expose port for any potential API/web service
EXPOSE 8000

# Set environment variables
ENV PYTHONUNBUFFERED=1
ENV GENESIS_DATA_DIR=/app/data
ENV GENESIS_RESULTS_DIR=/app/results
ENV GENESIS_WEBUI_PORT=8000

# Default command (can be overridden in docker-compose)
CMD ["python", "-m", "genesis.launch_hydra"]
CMD ["python", "-m", "genesis.webui.backend_server"]
44 changes: 44 additions & 0 deletions docker-compose.prod.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
services:
clickhouse:
image: clickhouse/clickhouse-server:latest
restart: unless-stopped
ports:
- "8123:8123"
- "9000:9000"
volumes:
- clickhouse_data:/var/lib/clickhouse
ulimits:
nofile:
soft: 262144
hard: 262144
healthcheck:
test: ["CMD", "wget", "--spider", "-q", "localhost:8123/ping"]
interval: 10s
timeout: 5s
retries: 5

backend:
image: ghcr.io/georgepearse/genesis:latest
restart: unless-stopped
depends_on:
clickhouse:
condition: service_healthy
ports:
- "${GENESIS_WEBUI_PORT:-8000}:8000"
environment:
GENESIS_DATA_DIR: /app/data
GENESIS_RESULTS_DIR: /app/results
PYTHONUNBUFFERED: 1
GENESIS_WEBUI_PORT: 8000
CLICKHOUSE_URL: http://default:@clickhouse:8123/default
CLICKHOUSE_HOST: clickhouse
CLICKHOUSE_PORT: 8123
OPENAI_API_KEY: ${OPENAI_API_KEY:-}
ANTHROPIC_API_KEY: ${ANTHROPIC_API_KEY:-}
E2B_API_KEY: ${E2B_API_KEY:-}
volumes:
- ./data:/app/data
- ./results:/app/results

volumes:
clickhouse_data:
2 changes: 1 addition & 1 deletion docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ services:
networks:
- genesis-network
restart: unless-stopped
command: ["python", "-m", "genesis.webui.visualization", "--port", "8000"]
command: ["python", "-m", "genesis.webui.backend_server"]

volumes:
clickhouse_data:
Expand Down
89 changes: 89 additions & 0 deletions docs/deployment.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
# Deployment

## Backend Decision

Deploy the Python backend, not the Rust backend.

Why:
- The Python backend already exposes the WebUI API and legacy endpoints the frontend uses.
- The repository already had Docker and ClickHouse wiring around the Python service.
- The Rust backend currently behaves as an evolution runner, not the integrated system entrypoint.
- The Rust backend has a small passing test suite, but it does not yet own frontend serving, API compatibility, or the operational deployment shape.

The practical deployment unit today is:
- `genesis.webui.backend_server`
- bundled React frontend
- ClickHouse

## What Changed

- `Dockerfile` now builds the React frontend and copies the static bundle into the Python image.
- `genesis/webui/backend_server.py` now serves the built frontend directly.
- `genesis/webui/frontend/src/services/api.ts` now uses same-origin requests by default instead of hardcoding `http://localhost:8000`.
- `docker-compose.yml` now starts the real backend module.
- `.github/workflows/deploy-app.yml` now builds and publishes `ghcr.io/georgepearse/genesis`, then optionally deploys it over SSH.

## Auto-Deploy Flow

On every push to `main`:

1. GitHub Actions builds the production image.
2. The image is pushed to GHCR as `ghcr.io/georgepearse/genesis:latest`.
3. If deploy secrets are configured, the workflow SSHes into your server.
4. The server pulls the latest image and runs `docker compose -f docker-compose.prod.yml up -d`.

## Required GitHub Secrets

Set these in GitHub Actions before enabling automatic rollout:

- `DEPLOY_HOST`: server hostname
- `DEPLOY_USER`: SSH user
- `DEPLOY_SSH_KEY`: private SSH key for the deploy user
- `DEPLOY_PATH`: optional, defaults to `/opt/genesis`
- `GHCR_USERNAME`: GHCR username for the server-side `docker login`
- `GHCR_PAT`: GHCR token with package read access
- `GENESIS_ENV`: complete `.env` file contents for production

Suggested `GENESIS_ENV` contents:

```env
GENESIS_WEBUI_PORT=8000
OPENAI_API_KEY=
ANTHROPIC_API_KEY=
E2B_API_KEY=
```

## Server Prerequisites

The target server needs:

- Docker Engine
- Docker Compose v2
- outbound access to `ghcr.io`
- open port for `GENESIS_WEBUI_PORT`

## First-Time Server Bootstrap

```bash
mkdir -p /opt/genesis
cd /opt/genesis
```

After that, the workflow can manage updates.

## Manual Deploy

If you want to deploy without GitHub Actions:

```bash
docker build -t ghcr.io/georgepearse/genesis:latest .
docker push ghcr.io/georgepearse/genesis:latest
docker compose -f docker-compose.prod.yml pull
docker compose -f docker-compose.prod.yml up -d
```

## Current Limitations

- ClickHouse is still part of the runtime stack, so this is a multi-container deployment.
- Production secrets are injected through `.env`; there is no secret manager integration yet.
- Rust should not replace Python in deployment until it owns the API and frontend contract end-to-end.
37 changes: 35 additions & 2 deletions genesis/webui/backend_server.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,8 @@

from fastapi import FastAPI, HTTPException, Query
from fastapi.middleware.cors import CORSMiddleware
from fastapi.responses import JSONResponse
from fastapi.responses import FileResponse, JSONResponse
from fastapi.staticfiles import StaticFiles
import clickhouse_connect
import uvicorn

Expand Down Expand Up @@ -48,6 +49,7 @@

# ClickHouse client (will be initialized on startup)
ch_client = None
FRONTEND_DIST_DIR = Path(__file__).parent / "frontend" / "dist"
RUN_ID_RE = re.compile(r"^[A-Za-z0-9._:-]+$")
PROGRAM_ID_RE = re.compile(r"^[A-Za-z0-9-]+$")
TASK_RE = re.compile(r"^[A-Za-z0-9_.:-]+$")
Expand Down Expand Up @@ -123,14 +125,29 @@ async def startup_event():

@app.get("/")
async def root():
"""Health check endpoint."""
"""Serve the built frontend when available, otherwise return service health."""
if FRONTEND_DIST_DIR.exists():
return FileResponse(FRONTEND_DIST_DIR / "index.html")
return {
"service": "Genesis WebUI API",
"status": "running",
"clickhouse": "connected" if ch_client else "disconnected",
}


if FRONTEND_DIST_DIR.exists():
assets_dir = FRONTEND_DIST_DIR / "assets"
if assets_dir.exists():
app.mount("/assets", StaticFiles(directory=assets_dir), name="frontend-assets")

favicon_path = FRONTEND_DIST_DIR / "favicon.ico"
if favicon_path.exists():

@app.get("/favicon.ico", include_in_schema=False)
async def favicon():
return FileResponse(favicon_path)


@app.get("/api/experiments")
async def list_experiments(
limit: int = Query(50, ge=1, le=500), task: Optional[str] = None
Expand Down Expand Up @@ -695,6 +712,22 @@ async def database_info():
raise HTTPException(status_code=500, detail=str(e))


@app.get("/{full_path:path}", include_in_schema=False)
async def frontend_fallback(full_path: str):
"""Serve SPA routes from the bundled frontend build."""
if not FRONTEND_DIST_DIR.exists():
raise HTTPException(status_code=404, detail="Not Found")

if full_path.startswith(("api/", "docs", "openapi.json")):
raise HTTPException(status_code=404, detail="Not Found")

candidate = FRONTEND_DIST_DIR / full_path
if full_path and candidate.is_file():
return FileResponse(candidate)

return FileResponse(FRONTEND_DIST_DIR / "index.html")


def main():
"""Run the server."""
port = int(os.getenv("GENESIS_WEBUI_PORT", 8000))
Expand Down
2 changes: 1 addition & 1 deletion genesis/webui/frontend/src/services/api.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import type { DatabaseInfo, Program, MetaFile, MetaContent } from '../types';

const API_BASE = 'http://localhost:8000';
const API_BASE = import.meta.env.VITE_API_BASE ?? '';

export async function listDatabases(): Promise<DatabaseInfo[]> {
const response = await fetch(`${API_BASE}/list_databases`);
Expand Down
1 change: 1 addition & 0 deletions mkdocs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ nav:
- Guides:
- Configuration: configuration.md
- Available LLMs: available_llms.md
- Deployment: deployment.md
- Logging & Analytics: logging.md
- ClickHouse Integration: clickhouse.md
- WebUI: webui.md
Expand Down