chore: dockerize api proxy and add gitignore

Add a Docker-based FastAPI proxy skeleton (charter §4) for parity across
dev / homelab VPS / fly.io, plus root .gitignore for Godot 4.7 and Python.

- api/Dockerfile: python:3.12-slim, non-root, PORT-aware CMD
- api/app/main.py: /health + five role endpoint stubs (§4)
- api/requirements.txt: fastapi/uvicorn/httpx/pydantic, pinned
- docker-compose.yml: local dev with live reload
- api/fly.toml: prod deploy stub with /health check
- api/.env.example: key lives in the proxy, never the client (§4)
- .gitignore: ignores .env, whitelists .env.example

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-07-09 11:23:27 -05:00
parent d54599e27d
commit d4ada9bedb
9 changed files with 201 additions and 0 deletions

58
.gitignore vendored Normal file
View File

@@ -0,0 +1,58 @@
# ─────────────────────────────────────────────
# Godot 4.7 (client/)
# ─────────────────────────────────────────────
# Editor + import cache (4.x replaced .import/ with .godot/)
.godot/
# Exported builds
/client/build/
export.cfg
# export_presets.cfg holds keystore paths / signing config — keep secrets out of history
export_presets.cfg
# Mono/C# (only if we drop to C# per charter §16)
.mono/
data_*/
*.mono/
# ─────────────────────────────────────────────
# Python / FastAPI (api/)
# ─────────────────────────────────────────────
__pycache__/
*.py[cod]
*$py.class
*.egg-info/
.eggs/
build/
dist/
# Virtual envs
.venv/
venv/
env/
ENV/
# Tooling caches
.pytest_cache/
.mypy_cache/
.ruff_cache/
.coverage
htmlcov/
.tox/
# ─────────────────────────────────────────────
# Secrets — the API key lives here in dev, NEVER in the client (charter §4)
# ─────────────────────────────────────────────
.env
.env.*
!.env.example
# ─────────────────────────────────────────────
# Editors / OS
# ─────────────────────────────────────────────
.vscode/
.idea/
*.swp
.DS_Store
Thumbs.db
# ─────────────────────────────────────────────
# Logs (charter §4/§10 logs go to a store, not the repo)
# ─────────────────────────────────────────────
*.log

16
api/.dockerignore Normal file
View File

@@ -0,0 +1,16 @@
# Keep the build context small and secrets out of the image.
.env
.env.*
__pycache__/
*.py[cod]
.venv/
venv/
.pytest_cache/
.mypy_cache/
.ruff_cache/
.coverage
htmlcov/
docs/
README.md
Dockerfile
.dockerignore

11
api/.env.example Normal file
View File

@@ -0,0 +1,11 @@
# Copy to .env for local dev. NEVER commit the real .env. The client never sees
# any of these — keys live only in the proxy (charter §4).
# Where the proxy sends model calls in dev (Ollama on the homelab).
OLLAMA_BASE_URL=http://localhost:11434
# Prod model provider (charter §4). Leave blank in dev.
REPLICATE_API_TOKEN=
# Port the proxy binds. compose / fly.io override this.
PORT=8000

26
api/Dockerfile Normal file
View File

@@ -0,0 +1,26 @@
# /api — FastAPI proxy (charter §4). Docker-based for parity across
# dev / homelab VPS / fly.io.
FROM python:3.12-slim
# Faster, quieter Python in containers.
ENV PYTHONUNBUFFERED=1 \
PYTHONDONTWRITEBYTECODE=1 \
PIP_NO_CACHE_DIR=1
WORKDIR /app
# Install deps first so the layer caches when only app code changes.
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
# App code.
COPY app ./app
# Run as non-root.
RUN useradd --create-home --uid 1000 appuser
USER appuser
EXPOSE 8000
# fly.io / compose set PORT; default to 8000 (charter localhost:8000).
CMD ["sh", "-c", "uvicorn app.main:app --host 0.0.0.0 --port ${PORT:-8000}"]

0
api/app/__init__.py Normal file
View File

47
api/app/main.py Normal file
View File

@@ -0,0 +1,47 @@
"""FastAPI proxy entrypoint — the guarding proxy of charter §4.
Skeleton only: a health check plus the five role endpoints as stubs so the
container runs and the client has something to point at. Real prompt routing,
model selection, logging, and auth land later — all server-side (§4, §5).
"""
from fastapi import FastAPI
app = FastAPI(title="coc-rpg proxy", version="0.0.1")
@app.get("/health")
def health() -> dict:
"""Liveness probe for compose / fly.io."""
return {"status": "ok"}
# ── Role endpoints (charter §4) ──────────────────────────────────────────────
# The client knows these paths and nothing about which model or prompt serves
# them. Stubs for now; each will load its prompt from api/prompts/ and route to
# the configured model.
@app.post("/dm/narrate")
def narrate() -> dict:
return {"detail": "not implemented"}
@app.post("/dm/adjudicate")
def adjudicate() -> dict:
return {"detail": "not implemented"}
@app.post("/dm/improvise")
def improvise() -> dict:
return {"detail": "not implemented"}
@app.post("/npc/speak")
def npc_speak() -> dict:
return {"detail": "not implemented"}
@app.post("/party/banter")
def banter() -> dict:
return {"detail": "not implemented"}

21
api/fly.toml Normal file
View File

@@ -0,0 +1,21 @@
# fly.io deploy config for the proxy (charter §4, prod). Stub — set app name and
# region, then `fly deploy` from api/. Secrets (REPLICATE_API_TOKEN) go via
# `fly secrets set`, never in this file.
app = "coc-rpg-proxy" # TODO: claim a real app name
primary_region = "ord" # TODO: pick a region
[build]
dockerfile = "Dockerfile"
[http_service]
internal_port = 8000
force_https = true
auto_stop_machines = true
auto_start_machines = true
min_machines_running = 0
[[http_service.checks]]
method = "GET"
path = "/health"
interval = "15s"
timeout = "2s"

5
api/requirements.txt Normal file
View File

@@ -0,0 +1,5 @@
# FastAPI proxy runtime deps. Pinned from first resolve; bump deliberately.
fastapi==0.139.0
uvicorn[standard]==0.51.0
httpx==0.28.1 # calling Ollama / Replicate
pydantic==2.13.4 # request/response contracts

17
docker-compose.yml Normal file
View File

@@ -0,0 +1,17 @@
# Local dev — run the proxy in Docker with live reload.
# docker compose up --build
# Ollama runs on the homelab (charter §4), not here — point OLLAMA_BASE_URL at it.
services:
api:
build: ./api
ports:
- "8000:8000"
env_file:
- ./api/.env
environment:
PORT: 8000
# Mount source + reload so edits are live without a rebuild.
volumes:
- ./api/app:/app/app:ro
command: >
uvicorn app.main:app --host 0.0.0.0 --port 8000 --reload