diff --git a/managed-chatkit/.dockerignore b/managed-chatkit/.dockerignore new file mode 100644 index 000000000..fb2ccb162 --- /dev/null +++ b/managed-chatkit/.dockerignore @@ -0,0 +1,32 @@ +node_modules +.git +.gitignore +*.md +.env* +!.env.example +__pycache__ +*.pyc +*.pyo +*.pyd +.Python +*.so +*.egg +*.egg-info +dist +build +.venv +.venv/* +venv +venv/* +.ruff_cache +.pytest_cache +.vscode +.idea +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +Dockerfile +.dockerignore +docker-compose* diff --git a/managed-chatkit/Dockerfile b/managed-chatkit/Dockerfile new file mode 100644 index 000000000..e90b3fc38 --- /dev/null +++ b/managed-chatkit/Dockerfile @@ -0,0 +1,39 @@ +# syntax=docker/dockerfile:1 + +FROM node:20-slim AS builder + +WORKDIR /app + +COPY frontend/package*.json ./ + +RUN npm ci + +COPY frontend/ ./ + +ARG VITE_CHATKIT_WORKFLOW_ID +ENV VITE_CHATKIT_WORKFLOW_ID=${VITE_CHATKIT_WORKFLOW_ID} + +RUN npm run build + +FROM python:3.12-slim AS runtime + +WORKDIR /app + +RUN pip install --no-cache-dir uv + +COPY --from=builder /app/dist ./dist + +COPY backend/pyproject.toml ./ + +RUN uv pip install --system -e . + +COPY backend/app ./app + +ENV PYTHONUNBUFFERED=1 + +EXPOSE 8000 + +HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \ + CMD python -c "import httpx; httpx.get('http://localhost:8000/health').raise_for_status()" || exit 1 + +CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000", "--workers", "4", "--log-level", "info"] diff --git a/managed-chatkit/README.md b/managed-chatkit/README.md index 6e32dbccb..3d99f40aa 100644 --- a/managed-chatkit/README.md +++ b/managed-chatkit/README.md @@ -33,3 +33,42 @@ same project and organization. - UI: `frontend/src/components/ChatKitPanel.tsx` - Session logic: `backend/app/main.py` + +## Deploy with Docker + +### Build + +```bash +docker build \ + --build-arg VITE_CHATKIT_WORKFLOW_ID=wf_xxxxx \ + -t managed-chatkit . +``` + +### Run + +```bash +docker run -d \ + -e OPENAI_API_KEY=sk-proj-xxxxx \ + -p 8000:8000 \ + --name managed-chatkit \ + managed-chatkit +``` + +The app will be available at `http://localhost:8000`. + +### Environment variables + +| Variable | Build-time | Runtime | Description | +|----------|------------|---------|-------------| +| `VITE_CHATKIT_WORKFLOW_ID` | Required | Required | Your ChatKit workflow ID | +| `OPENAI_API_KEY` | - | Required | OpenAI API key | +| `CHATKIT_API_BASE` | - | Optional | ChatKit API base URL | + +### Production deployment + +For production, pass `OPENAI_API_KEY` via your orchestrator's secret management: + +- **Kubernetes**: Use Secrets +- **Cloud Run**: Use `--set-secrets` +- **ECS**: Use Task Definition secrets +- **Azure Container Apps**: Use secret references diff --git a/managed-chatkit/backend/app/main.py b/managed-chatkit/backend/app/main.py index 4106d0b21..6f7fd79cf 100644 --- a/managed-chatkit/backend/app/main.py +++ b/managed-chatkit/backend/app/main.py @@ -10,7 +10,8 @@ import httpx from fastapi import FastAPI, Request from fastapi.middleware.cors import CORSMiddleware -from fastapi.responses import JSONResponse +from fastapi.responses import HTMLResponse, JSONResponse +from fastapi.staticfiles import StaticFiles DEFAULT_CHATKIT_BASE = "https://api.openai.com" SESSION_COOKIE_NAME = "chatkit_session_id" @@ -18,6 +19,8 @@ app = FastAPI(title="Managed ChatKit Session API") +app.mount("/static", StaticFiles(directory="dist/assets"), name="static") + app.add_middleware( CORSMiddleware, allow_origins=["*"], @@ -164,3 +167,12 @@ def parse_json(response: httpx.Response) -> Mapping[str, Any]: return parsed if isinstance(parsed, Mapping) else {} except (json.JSONDecodeError, httpx.DecodingError): return {} + + +@app.get("/{full_path:path}") +async def serve_spa(full_path: str) -> HTMLResponse: + index_path = os.path.join("dist", "index.html") + if os.path.exists(index_path): + with open(index_path, "r") as f: + return HTMLResponse(content=f.read()) + return HTMLResponse(content="

Not Found

", status_code=404)