feat(01-01): Backend FastAPI skeleton + Docker build config
- FastAPI app with SPAStaticFiles catch-all (root_path NOT in constructor)
- config.py with DATA_PATH, PROMPTS_PATH, OUTPUTS_PATH, CAMPAIGNS_PATH, CONFIG_PATH
- Startup lifespan creates data directories automatically
- Health endpoint: GET /api/health -> {"status": "ok"}
- Dockerfile multi-stage: node:22-slim builds React, python:3.12-slim serves API+SPA
- --root-path /postgenerator set in Uvicorn CMD only (avoids Pitfall #4)
- docker-compose.yml: lab-postgenerator-app, proxy_net, named volume for data persistence
- requirements.txt with pinned versions: fastapi[standard]==0.135.1, anthropic==0.84.0
This commit is contained in:
3
.env.example
Normal file
3
.env.example
Normal file
@@ -0,0 +1,3 @@
|
||||
ANTHROPIC_API_KEY=your-key-here
|
||||
LLM_MODEL=claude-sonnet-4-5
|
||||
DATA_PATH=/app/data
|
||||
38
.gitignore
vendored
Normal file
38
.gitignore
vendored
Normal file
@@ -0,0 +1,38 @@
|
||||
# Dependencies
|
||||
node_modules/
|
||||
__pycache__/
|
||||
*.pyc
|
||||
venv/
|
||||
.venv/
|
||||
|
||||
# Environment
|
||||
.env
|
||||
.env.local
|
||||
.env.*.local
|
||||
|
||||
# Build
|
||||
.next/
|
||||
dist/
|
||||
build/
|
||||
out/
|
||||
frontend/dist/
|
||||
|
||||
# IDE
|
||||
.idea/
|
||||
.vscode/
|
||||
*.swp
|
||||
*.swo
|
||||
|
||||
# OS
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
|
||||
# Logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
|
||||
# Data (local dev)
|
||||
data/
|
||||
*.db
|
||||
*.sqlite
|
||||
backend/data/outputs/
|
||||
41
Dockerfile
Normal file
41
Dockerfile
Normal file
@@ -0,0 +1,41 @@
|
||||
# ============================================================
|
||||
# Stage 1: Build React SPA
|
||||
# ============================================================
|
||||
FROM node:22-slim AS frontend-builder
|
||||
|
||||
WORKDIR /app/frontend
|
||||
|
||||
# Install dependencies first (layer cache optimization)
|
||||
COPY frontend/package*.json ./
|
||||
RUN npm ci
|
||||
|
||||
# Copy source and build
|
||||
COPY frontend/ ./
|
||||
RUN npm run build
|
||||
|
||||
# ============================================================
|
||||
# Stage 2: Python runtime (serves API + built SPA)
|
||||
# ============================================================
|
||||
FROM python:3.12-slim AS runtime
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
# Install Python dependencies
|
||||
COPY requirements.txt .
|
||||
RUN pip install --no-cache-dir -r requirements.txt
|
||||
|
||||
# Copy backend source
|
||||
COPY backend/ ./backend/
|
||||
|
||||
# Copy built React SPA from frontend-builder stage
|
||||
COPY --from=frontend-builder /app/frontend/dist ./static
|
||||
|
||||
# Create data directory structure
|
||||
RUN mkdir -p /app/data/prompts /app/data/outputs /app/data/campaigns /app/data/config
|
||||
|
||||
# Expose port
|
||||
EXPOSE 8000
|
||||
|
||||
# CRITICAL: --root-path /postgenerator is set HERE (Uvicorn), NOT in FastAPI() constructor
|
||||
# This avoids the double-path bug (Pitfall #4)
|
||||
CMD ["uvicorn", "backend.main:app", "--host", "0.0.0.0", "--port", "8000", "--root-path", "/postgenerator"]
|
||||
0
backend/__init__.py
Normal file
0
backend/__init__.py
Normal file
53
backend/config.py
Normal file
53
backend/config.py
Normal file
@@ -0,0 +1,53 @@
|
||||
"""Centralized configuration and path constants for PostGenerator."""
|
||||
|
||||
import os
|
||||
from pathlib import Path
|
||||
from dataclasses import dataclass
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Path constants (read from env, defaulting to ./data for local dev)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
DATA_PATH = Path(os.getenv("DATA_PATH", "./data"))
|
||||
PROMPTS_PATH = DATA_PATH / "prompts"
|
||||
OUTPUTS_PATH = DATA_PATH / "outputs"
|
||||
CAMPAIGNS_PATH = DATA_PATH / "campaigns"
|
||||
CONFIG_PATH = DATA_PATH / "config"
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# App settings
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
@dataclass
|
||||
class Settings:
|
||||
anthropic_api_key: str
|
||||
llm_model: str
|
||||
data_path: Path
|
||||
prompts_path: Path
|
||||
outputs_path: Path
|
||||
campaigns_path: Path
|
||||
config_path: Path
|
||||
|
||||
|
||||
def get_settings() -> Settings:
|
||||
"""Load settings from environment variables."""
|
||||
api_key = os.getenv("ANTHROPIC_API_KEY", "")
|
||||
if not api_key:
|
||||
import warnings
|
||||
warnings.warn(
|
||||
"ANTHROPIC_API_KEY not set — generation endpoints will fail",
|
||||
RuntimeWarning,
|
||||
stacklevel=2,
|
||||
)
|
||||
|
||||
return Settings(
|
||||
anthropic_api_key=api_key,
|
||||
llm_model=os.getenv("LLM_MODEL", "claude-sonnet-4-5"),
|
||||
data_path=DATA_PATH,
|
||||
prompts_path=PROMPTS_PATH,
|
||||
outputs_path=OUTPUTS_PATH,
|
||||
campaigns_path=CAMPAIGNS_PATH,
|
||||
config_path=CONFIG_PATH,
|
||||
)
|
||||
78
backend/main.py
Normal file
78
backend/main.py
Normal file
@@ -0,0 +1,78 @@
|
||||
"""PostGenerator FastAPI application.
|
||||
|
||||
IMPORTANT: root_path is intentionally NOT set in the FastAPI() constructor.
|
||||
It is passed only via Uvicorn's --root-path flag at runtime to avoid the
|
||||
double-path bug (Pitfall #4).
|
||||
"""
|
||||
|
||||
from contextlib import asynccontextmanager
|
||||
from pathlib import Path
|
||||
|
||||
from fastapi import FastAPI
|
||||
from fastapi.responses import FileResponse
|
||||
from fastapi.staticfiles import StaticFiles
|
||||
|
||||
from backend.config import CAMPAIGNS_PATH, CONFIG_PATH, OUTPUTS_PATH, PROMPTS_PATH
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# SPA static files with catch-all fallback
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
class SPAStaticFiles(StaticFiles):
|
||||
"""Serve a React SPA: return index.html for any 404 so the client-side
|
||||
router handles unknown paths instead of returning a 404 from the server."""
|
||||
|
||||
async def get_response(self, path: str, scope):
|
||||
try:
|
||||
return await super().get_response(path, scope)
|
||||
except Exception:
|
||||
# Fall back to index.html for client-side routing
|
||||
return await super().get_response("index.html", scope)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Startup / shutdown lifecycle
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
@asynccontextmanager
|
||||
async def lifespan(app: FastAPI):
|
||||
"""Create required data directories on startup if they do not exist."""
|
||||
for directory in (PROMPTS_PATH, OUTPUTS_PATH, CAMPAIGNS_PATH, CONFIG_PATH):
|
||||
directory.mkdir(parents=True, exist_ok=True)
|
||||
yield
|
||||
# Nothing to clean up on shutdown
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Application
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
# CRITICAL: Do NOT pass root_path here — use Uvicorn --root-path instead.
|
||||
app = FastAPI(
|
||||
title="PostGenerator",
|
||||
description="Instagram carousel post generator powered by Claude",
|
||||
version="0.1.0",
|
||||
lifespan=lifespan,
|
||||
# root_path is intentionally absent — set via Uvicorn --root-path at runtime
|
||||
)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# API routes (must be registered BEFORE the SPA catch-all mount)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
@app.get("/api/health")
|
||||
async def health() -> dict:
|
||||
"""Health check endpoint."""
|
||||
return {"status": "ok"}
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# SPA catch-all mount (MUST be last — catches everything not matched above)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
_STATIC_DIR = Path(__file__).parent.parent / "static"
|
||||
|
||||
if _STATIC_DIR.exists():
|
||||
app.mount("/", SPAStaticFiles(directory=str(_STATIC_DIR), html=True), name="spa")
|
||||
0
backend/routers/__init__.py
Normal file
0
backend/routers/__init__.py
Normal file
26
docker-compose.yml
Normal file
26
docker-compose.yml
Normal file
@@ -0,0 +1,26 @@
|
||||
services:
|
||||
app:
|
||||
container_name: lab-postgenerator-app
|
||||
build:
|
||||
context: .
|
||||
dockerfile: Dockerfile
|
||||
env_file: .env
|
||||
volumes:
|
||||
- postgenerator-data:/app/data
|
||||
networks:
|
||||
- proxy_net
|
||||
# NO exposed ports — traffic routed via lab-router nginx
|
||||
deploy:
|
||||
resources:
|
||||
limits:
|
||||
memory: 1024M
|
||||
cpus: '1.0'
|
||||
restart: unless-stopped
|
||||
|
||||
volumes:
|
||||
postgenerator-data:
|
||||
name: postgenerator-data
|
||||
|
||||
networks:
|
||||
proxy_net:
|
||||
external: true
|
||||
5
requirements.txt
Normal file
5
requirements.txt
Normal file
@@ -0,0 +1,5 @@
|
||||
fastapi[standard]==0.135.1
|
||||
anthropic==0.84.0
|
||||
httpx==0.28.1
|
||||
python-dotenv==1.2.2
|
||||
aiofiles==24.1.0
|
||||
Reference in New Issue
Block a user