Spaces:
Runtime error
Runtime error
| # ============================================================================ | |
| # MULTI-STAGE DOCKERFILE: Development + Production | |
| # ============================================================================ | |
| # What is multi-stage build? | |
| # - ONE Dockerfile that can create DIFFERENT images for dev vs prod | |
| # - Uses "FROM ... AS stage-name" to define stages | |
| # - Build specific stage with: `docker build --target stage-name -t image:tag .` | |
| # | |
| # Benefits: | |
| # - Single source of truth (one Dockerfile, not two separate files) | |
| # - Share common setup (base stage) between dev and prod | |
| # - Actually different images (not just different docker-compose configs) | |
| # | |
| # Reference: docker_images_vs_services.md - Approach B (Multi-Stage Dockerfile) | |
| # ============================================================================ | |
| # ============================================================================ | |
| # STAGE 1: BASE (Shared by both dev and prod) | |
| # ============================================================================ | |
| # Why separate base stage? | |
| # - Avoid duplicating common setup (Python install, uv setup, dependencies) | |
| # - Changes here affect both dev and prod | |
| # - Faster builds (shared layers cached once) | |
| # ============================================================================ | |
| FROM cgr.dev/chainguard/python:latest-dev AS base | |
| # Chainguard sets ENTRYPOINT ["python"] β clear it so exec-form CMD works directly. | |
| ENTRYPOINT [] | |
| # Set working directory to project root | |
| # NOTE: Changed from /app to /project to support absolute imports (app.*) | |
| # This allows imports like "from app.utils.config import get_logger" | |
| # to work consistently in both IDE and Docker environments | |
| WORKDIR /project | |
| # Install uv (modern Python package manager) | |
| # COPY --from=IMAGE means: copy FROM another image (not from host) | |
| # Source: /uv (binary from astral-sh's image) | |
| # Destination: /usr/local/bin/uv (system PATH in our image) | |
| # Why uv? Faster than pip, uses lockfile (uv.lock) for reproducible builds | |
| COPY --from=ghcr.io/astral-sh/uv:latest /uv /usr/local/bin/uv | |
| # Copy dependency files first (for layer caching optimization) | |
| # Why before code? Dependencies change less frequently than code | |
| # If only code changes, Docker reuses this cached layer (faster rebuilds) | |
| # | |
| # What are these files? | |
| # - pyproject.toml = Python project metadata + dependency list (like package.json) | |
| # - uv.lock = Exact pinned versions of all dependencies (like package-lock.json) | |
| COPY pyproject.toml uv.lock ./ | |
| # Chainguard runs as nonroot (uid 65532) β cannot write to system Python paths. | |
| # Install into /project/.venv (owned by nonroot) and put it on PATH. | |
| RUN uv venv /project/.venv && \ | |
| uv pip install --python /project/.venv/bin/python -r pyproject.toml | |
| ENV PATH="/project/.venv/bin:$PATH" | |
| # Avoid HuggingFace Hub Xet transfer stalls during startup embedding warmup. | |
| ENV HF_HUB_DISABLE_XET=1 | |
| # Copy application code | |
| # NOTE: Changed to copy entire project instead of just app/ directory | |
| # This maintains the project structure: /project/app/, /project/pyproject.toml, etc. | |
| # Allows absolute imports like "from app.utils.config import get_logger" to work | |
| # | |
| # Source: ./ (entire project root on host) | |
| # Destination: . (current WORKDIR, which is /project inside container) | |
| # | |
| # Result: Files land at /project/app/main.py, /project/app/models/, etc. | |
| # Note: chroma_data/ is excluded via .dockerignore β created below with correct ownership. | |
| COPY . . | |
| # Pre-create ChromaDB data directory as nonroot so the named volume is initialised | |
| # with correct ownership. Without this, Docker creates the volume mount point as root, | |
| # and the nonroot process cannot write the SQLite file β causing 500 errors on all KB writes. | |
| RUN mkdir -p /project/chroma_data | |
| # ============================================================================ | |
| # Why we DON'T set CMD or ENV here? | |
| # - Base stage is INCOMPLETE - not meant to run directly | |
| # - Each final stage (development/production) will set their own CMD and ENV | |
| # - This keeps base stage flexible for different use cases | |
| # ============================================================================ | |
| # ============================================================================ | |
| # STAGE 2: DEVELOPMENT | |
| # ============================================================================ | |
| # When to use this stage? | |
| # - Local development on host machine | |
| # - Need hot reload (code changes auto-restart server) | |
| # - Need debug mode (verbose logging, error traces) | |
| # - Don't care about image size or optimization | |
| # | |
| # How to build: docker build --target development -t smart-chatbot:dev . | |
| # ============================================================================ | |
| FROM base AS development | |
| # Dev-group dependencies (QA tooling like the anthropic SDK) β installed only | |
| # in the development image so the production image stays lean | |
| RUN uv pip install --python /project/.venv/bin/python --group dev | |
| # Set environment variables for development | |
| # DEBUG=true enables: | |
| # - Verbose logging | |
| # - Detailed error traces in API responses | |
| # - Development-only features | |
| ENV DEBUG=true | |
| # Development-specific command | |
| # Flags explained: | |
| # uvicorn = ASGI server for FastAPI (available via /project/.venv on PATH) | |
| # app.main:app = Import path (app/main.py file, app object) - using absolute import | |
| # --reload = Watch for code changes, auto-restart server (hot reload) | |
| # --host 0.0.0.0 = Listen on ALL network interfaces (required for Docker) | |
| # --port 8000 = Port number (matches EXPOSE, nginx proxy_pass target) | |
| # | |
| # NOTE: Changed from "main:app" to "app.main:app" to match absolute import structure | |
| # This works because WORKDIR is /project (contains app/ directory) | |
| # | |
| # Why different from production CMD? | |
| # - --reload is EXPENSIVE (file watching, auto-restart) - only for development | |
| # - Production uses gunicorn (multi-worker, more robust) | |
| # | |
| # Note: No "uv run" needed β venv bin is on PATH via ENV PATH above | |
| CMD ["uvicorn", "app.main:app", "--reload", "--host", "0.0.0.0", "--port", "8000"] | |
| # Port documentation (doesn't actually open ports) | |
| # How this works with docker-compose in DEVELOPMENT: | |
| # | |
| # smart-chatbot: | |
| # image: smart-chatbot:dev | |
| # ports: | |
| # - "8000:8000" β EXPOSED to host | |
| # volumes: | |
| # - ./app:/app β Bind mount (code changes reflect immediately) | |
| # | |
| # Access options: | |
| # - Direct: http://localhost:8000 | |
| # - Via nginx proxy: http://smart-chatbot.local (requires /etc/hosts + nginx config) | |
| EXPOSE 8000 | |
| # ============================================================================ | |
| # STAGE 3: PRODUCTION | |
| # ============================================================================ | |
| # When to use this stage? | |
| # - Deploying to server (Railway, AWS, DigitalOcean, etc.) | |
| # - Need performance (multi-worker, optimized) | |
| # - Need security (debug off, minimal attack surface) | |
| # - Care about image size and resource usage | |
| # | |
| # How to build: docker build --target production -t smart-chatbot:prod . | |
| # ============================================================================ | |
| FROM base AS production | |
| # Install curl for Docker health checks (Chainguard runs as nonroot β switch to root for apk) | |
| USER root | |
| RUN apk add --no-cache curl | |
| USER nonroot | |
| # Set environment variables for production | |
| # DEBUG=false disables: | |
| # - Verbose logging (only errors/warnings logged) | |
| # - Detailed error traces (attackers can't see internal structure) | |
| # - Development-only features | |
| ENV DEBUG=false | |
| # Production-specific command | |
| # Why gunicorn instead of uvicorn? | |
| # - Gunicorn = Production-grade WSGI server with process management | |
| # - Can run MULTIPLE workers (parallel request handling) | |
| # - Better stability, graceful shutdowns, worker health monitoring | |
| # | |
| # Flags explained: | |
| # gunicorn = WSGI server (wraps uvicorn workers, available via /project/.venv on PATH) | |
| # app.main:app = Import path (app/main.py file, app object) - using absolute import | |
| # -k uvicorn.workers.UvicornWorker = Use Uvicorn worker class (for ASGI support) | |
| # -w 4 = 4 worker processes (adjust based on CPU cores) | |
| # -b 0.0.0.0:8000 = Bind to all interfaces, port 8000 | |
| # | |
| # NOTE: Changed from "main:app" to "app.main:app" to match absolute import structure | |
| # This works because WORKDIR is /project (contains app/ directory) | |
| # | |
| # Worker count rule of thumb: (2 Γ CPU_cores) + 1 | |
| # Example: 2-core server β 5 workers, 4-core server β 9 workers | |
| # | |
| # Note: No "uv run" needed β venv bin is on PATH via ENV PATH above | |
| CMD ["gunicorn", "app.main:app", "-k", "uvicorn.workers.UvicornWorker", "-w", "4", "-b", "0.0.0.0:8000"] | |
| # Port documentation (doesn't actually open ports) | |
| # How this works with docker-compose in PRODUCTION: | |
| # | |
| # smart-chatbot: | |
| # image: smart-chatbot:prod | |
| # expose: | |
| # - 8000 β INTERNAL ONLY (only other containers can access) | |
| # # NO "ports:" section = NOT accessible from host/internet | |
| # | |
| # nginx: | |
| # ports: | |
| # - "80:80" β Only nginx exposed to internet | |
| # # nginx.conf: proxy_pass http://smart-chatbot:8000 | |
| # | |
| # Request flow: Internet β nginx:80 β smart-chatbot:8000 (internal) | |
| EXPOSE 8000 | |
| # ============================================================================ | |
| # STAGE 4: HUGGINGFACE SPACE | |
| # ============================================================================ | |
| FROM base AS huggingface | |
| ENV DEBUG=false | |
| ENV ENVIRONMENT=production | |
| # HuggingFace Spaces default port is 7860 | |
| CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "7860", "--workers", "1"] | |
| EXPOSE 7860 | |
| # ============================================================================ | |
| # HOW TO USE THIS DOCKERFILE | |
| # ============================================================================ | |
| # | |
| # BUILD DEVELOPMENT IMAGE: | |
| # docker build --target development -t smart-chatbot:dev . | |
| # | |
| # BUILD PRODUCTION IMAGE: | |
| # docker build --target production -t smart-chatbot:prod . | |
| # | |
| # BUILD BOTH (for testing): | |
| # docker build --target development -t smart-chatbot:dev . | |
| # docker build --target production -t smart-chatbot:prod . | |
| # | |
| # VERIFY IMAGES EXIST: | |
| # docker images | grep smart-chatbot | |
| # | |
| # RUN DEVELOPMENT CONTAINER (standalone, no compose): | |
| # docker run -p 8000:8000 -v $(pwd)/app:/project/app smart-chatbot:dev | |
| # | |
| # RUN PRODUCTION CONTAINER (standalone, no compose): | |
| # docker run -p 8000:8000 smart-chatbot:prod | |
| # | |
| # ============================================================================ | |
| # WHAT'S THE DIFFERENCE BETWEEN IMAGES? | |
| # ============================================================================ | |
| # | |
| # smart-chatbot:dev (DEVELOPMENT) | |
| # β Has: --reload flag (hot reload for code changes) | |
| # β Has: DEBUG=true (verbose logging, error traces) | |
| # β Has: Single-worker uvicorn (simpler debugging) | |
| # β Slower, higher memory usage, less secure | |
| # | |
| # smart-chatbot:prod (PRODUCTION) | |
| # β Has: Multi-worker gunicorn (parallel requests, better performance) | |
| # β Has: DEBUG=false (minimal logging, no error traces) | |
| # β Has: Optimized for speed and security | |
| # β No hot reload (must rebuild image for code changes) | |
| # | |
| # ============================================================================ | |
| # WHAT ABOUT DOCKER-COMPOSE? | |
| # ============================================================================ | |
| # You can use these images in docker-compose.yml: | |
| # | |
| # DEVELOPMENT (docker-compose.dev.yml): | |
| # services: | |
| # smart-chatbot: | |
| # image: smart-chatbot:dev β Uses development stage | |
| # ports: | |
| # - "8000:8000" β Exposed to host (localhost:8000) | |
| # volumes: | |
| # - ./app:/app β Bind mount (hot reload works!) | |
| # | |
| # PRODUCTION (docker-compose.prod.yml): | |
| # services: | |
| # smart-chatbot: | |
| # image: smart-chatbot:prod β Uses production stage | |
| # expose: | |
| # - 8000 β Internal only (nginx access only) | |
| # # NO volumes (code baked into image) | |
| # | |
| # nginx: | |
| # image: nginx:alpine | |
| # ports: | |
| # - "80:80" β Only nginx exposed to internet | |
| # | |
| # ============================================================================ | |