From 2a5b50ee4c27cdc42b71e93a2f39f22ffe2bb688 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 25 Jan 2026 23:40:51 +0000 Subject: [PATCH] Consolidate to single container, remove nginx - Create unified Dockerfile with multi-stage build (Node + Python) - FastAPI now serves React static files directly - Remove frontend service and nginx dependency - Simplify docker-compose to single service - All routes work without proxy configuration --- Dockerfile | 63 +++++++++++++++++++++++++++++++++++++++++++++ backend/app/main.py | 52 ++++++++++++++++++++++++++++++++++--- docker-compose.yml | 32 ++++------------------- 3 files changed, 116 insertions(+), 31 deletions(-) create mode 100644 Dockerfile diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..2e7a629 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,63 @@ +# ============================================================================== +# AI Photo Edit - Unified Container +# Builds React frontend and serves it alongside FastAPI backend +# ============================================================================== + +# Stage 1: Build React frontend +FROM node:20-alpine AS frontend-build + +WORKDIR /frontend + +# Copy package files and install dependencies +COPY frontend/package.json frontend/package-lock.json* ./ +RUN npm install + +# Copy frontend source and build +COPY frontend/ ./ +RUN npm run build + +# Stage 2: Python backend with frontend static files +FROM python:3.11-slim + +WORKDIR /app + +# Install system dependencies for OpenCV, rembg, SAM, and image processing +RUN apt-get update && apt-get install -y \ + libgl1 \ + libglib2.0-0 \ + libsm6 \ + libxext6 \ + libxrender-dev \ + libgomp1 \ + wget \ + git \ + && rm -rf /var/lib/apt/lists/* + +# Copy requirements and install Python dependencies +COPY backend/requirements.txt . +RUN pip install --no-cache-dir -r requirements.txt + +# Pre-download rembg model (u2net) to avoid first-run delay +RUN python -c "from rembg import remove; print('rembg model downloaded')" || true + +# Copy backend application +COPY backend/ . + +# Copy entrypoint script +COPY backend/entrypoint.sh /entrypoint.sh +RUN chmod +x /entrypoint.sh + +# Copy scripts +COPY scripts/ /scripts/ + +# Copy built frontend from stage 1 +COPY --from=frontend-build /frontend/dist /app/static + +# Create data directories +RUN mkdir -p /app/data/projects /app/data/patches /app/data/models + +# Expose port +EXPOSE 8000 + +# Use entrypoint script +ENTRYPOINT ["/entrypoint.sh"] diff --git a/backend/app/main.py b/backend/app/main.py index f6a95fd..540f490 100644 --- a/backend/app/main.py +++ b/backend/app/main.py @@ -1,7 +1,10 @@ -from fastapi import FastAPI +from fastapi import FastAPI, Request from fastapi.middleware.cors import CORSMiddleware from fastapi.staticfiles import StaticFiles +from fastapi.responses import FileResponse, HTMLResponse from contextlib import asynccontextmanager +from pathlib import Path +import os from app.config import settings from app.database import init_db @@ -40,9 +43,9 @@ app.include_router(generate.router) app.include_router(tools.router) -@app.get("/") -def root(): - """API root endpoint""" +@app.get("/api") +def api_root(): + """API info endpoint""" return { "name": "AI Photo Edit API", "version": "1.0.0", @@ -54,3 +57,44 @@ def root(): def health(): """Health check endpoint""" return {"status": "healthy"} + + +# Static files directory +STATIC_DIR = Path("/app/static") + + +# Serve static assets (JS, CSS, images) +if STATIC_DIR.exists(): + app.mount("/assets", StaticFiles(directory=STATIC_DIR / "assets"), name="assets") + + +@app.get("/", response_class=HTMLResponse) +async def serve_spa(): + """Serve React SPA index.html""" + index_path = STATIC_DIR / "index.html" + if index_path.exists(): + return FileResponse(index_path) + return HTMLResponse("

Frontend not built. Run npm build in frontend/

") + + +@app.get("/{full_path:path}") +async def serve_spa_routes(request: Request, full_path: str): + """ + Catch-all route for React Router (SPA). + Serves static files if they exist, otherwise returns index.html. + """ + # Don't catch API routes + if full_path.startswith(("projects", "edits", "patches", "tools", "generate", "health", "docs", "openapi.json", "api")): + return {"detail": "Not Found"} + + # Check if it's a static file + static_file = STATIC_DIR / full_path + if static_file.exists() and static_file.is_file(): + return FileResponse(static_file) + + # Otherwise serve index.html for React Router + index_path = STATIC_DIR / "index.html" + if index_path.exists(): + return FileResponse(index_path) + + return HTMLResponse("

Frontend not built

", status_code=404) diff --git a/docker-compose.yml b/docker-compose.yml index ee51300..0c31dbe 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,16 +1,13 @@ -version: '3.8' - services: - backend: + app: build: - context: ./backend + context: . dockerfile: Dockerfile - container_name: ai-photo-edit-backend + container_name: ai-photo-edit ports: - - "8101:8000" # External 8101, internal 8000 + - "3080:8000" volumes: - ./data:/app/data - - ./backend:/app - ./scripts:/scripts environment: - DATABASE_URL=sqlite:///./data/ai_photo_edit.db @@ -19,27 +16,8 @@ services: - OPENAI_API_KEY=${OPENAI_API_KEY:-} - STABILITY_API_KEY=${STABILITY_API_KEY:-} - REPLICATE_API_KEY=${REPLICATE_API_KEY:-} - - CORS_ORIGINS=http://localhost:5173,http://localhost:3000,http://localhost + - CORS_ORIGINS=* restart: unless-stopped - networks: - - ai-photo-edit-network - - frontend: - build: - context: ./frontend - dockerfile: Dockerfile - container_name: ai-photo-edit-frontend - ports: - - "3080:80" # Frontend on port 3080 - depends_on: - - backend - restart: unless-stopped - networks: - - ai-photo-edit-network - -networks: - ai-photo-edit-network: - driver: bridge volumes: data: