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
This commit is contained in:
+63
@@ -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"]
|
||||||
+48
-4
@@ -1,7 +1,10 @@
|
|||||||
from fastapi import FastAPI
|
from fastapi import FastAPI, Request
|
||||||
from fastapi.middleware.cors import CORSMiddleware
|
from fastapi.middleware.cors import CORSMiddleware
|
||||||
from fastapi.staticfiles import StaticFiles
|
from fastapi.staticfiles import StaticFiles
|
||||||
|
from fastapi.responses import FileResponse, HTMLResponse
|
||||||
from contextlib import asynccontextmanager
|
from contextlib import asynccontextmanager
|
||||||
|
from pathlib import Path
|
||||||
|
import os
|
||||||
|
|
||||||
from app.config import settings
|
from app.config import settings
|
||||||
from app.database import init_db
|
from app.database import init_db
|
||||||
@@ -40,9 +43,9 @@ app.include_router(generate.router)
|
|||||||
app.include_router(tools.router)
|
app.include_router(tools.router)
|
||||||
|
|
||||||
|
|
||||||
@app.get("/")
|
@app.get("/api")
|
||||||
def root():
|
def api_root():
|
||||||
"""API root endpoint"""
|
"""API info endpoint"""
|
||||||
return {
|
return {
|
||||||
"name": "AI Photo Edit API",
|
"name": "AI Photo Edit API",
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
@@ -54,3 +57,44 @@ def root():
|
|||||||
def health():
|
def health():
|
||||||
"""Health check endpoint"""
|
"""Health check endpoint"""
|
||||||
return {"status": "healthy"}
|
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("<h1>Frontend not built. Run npm build in frontend/</h1>")
|
||||||
|
|
||||||
|
|
||||||
|
@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("<h1>Frontend not built</h1>", status_code=404)
|
||||||
|
|||||||
+5
-27
@@ -1,16 +1,13 @@
|
|||||||
version: '3.8'
|
|
||||||
|
|
||||||
services:
|
services:
|
||||||
backend:
|
app:
|
||||||
build:
|
build:
|
||||||
context: ./backend
|
context: .
|
||||||
dockerfile: Dockerfile
|
dockerfile: Dockerfile
|
||||||
container_name: ai-photo-edit-backend
|
container_name: ai-photo-edit
|
||||||
ports:
|
ports:
|
||||||
- "8101:8000" # External 8101, internal 8000
|
- "3080:8000"
|
||||||
volumes:
|
volumes:
|
||||||
- ./data:/app/data
|
- ./data:/app/data
|
||||||
- ./backend:/app
|
|
||||||
- ./scripts:/scripts
|
- ./scripts:/scripts
|
||||||
environment:
|
environment:
|
||||||
- DATABASE_URL=sqlite:///./data/ai_photo_edit.db
|
- DATABASE_URL=sqlite:///./data/ai_photo_edit.db
|
||||||
@@ -19,27 +16,8 @@ services:
|
|||||||
- OPENAI_API_KEY=${OPENAI_API_KEY:-}
|
- OPENAI_API_KEY=${OPENAI_API_KEY:-}
|
||||||
- STABILITY_API_KEY=${STABILITY_API_KEY:-}
|
- STABILITY_API_KEY=${STABILITY_API_KEY:-}
|
||||||
- REPLICATE_API_KEY=${REPLICATE_API_KEY:-}
|
- REPLICATE_API_KEY=${REPLICATE_API_KEY:-}
|
||||||
- CORS_ORIGINS=http://localhost:5173,http://localhost:3000,http://localhost
|
- CORS_ORIGINS=*
|
||||||
restart: unless-stopped
|
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:
|
volumes:
|
||||||
data:
|
data:
|
||||||
|
|||||||
Reference in New Issue
Block a user