Spaces:
Running
Running
File size: 12,638 Bytes
c4d486b | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 | # ============================================================================
# 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 python:3.11-slim AS base
# 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 ./
# Install dependencies using uv
# Using "uv pip install" instead of "uv sync" for system-wide installation
#
# Flags explained:
# pip install = Use pip-compatible interface (supports --system)
# --system = Install to system Python (not venv) - CRITICAL for Docker
# -r pyproject.toml = Install from project dependencies
#
# Why --system is required in Docker:
#
# WITHOUT --system (default behavior):
# - uv creates a virtual environment (.venv/) and installs packages there
# - Executables end up in: .venv/bin/uvicorn (hidden location)
# - Docker can't find them in PATH β Error: "No such file or directory"
# - You'd need to use "uv run uvicorn ..." which adds complexity
#
# WITH --system:
# - uv installs directly to system Python (no .venv/ folder created)
# - Executables go to: /usr/local/bin/uvicorn (standard PATH location)
# - Docker finds them immediately β uvicorn command just works
# - Cleaner CMD: just "uvicorn ..." instead of "uv run uvicorn ..."
#
# Why virtual environments (.venv/) are "extra" in Docker:
# On your laptop: .venv/ needed to separate multiple projects
# - Project A needs pandas 1.0
# - Project B needs pandas 2.0
# - .venv/ prevents conflicts
#
# In Docker: Container itself provides isolation
# - Container 1 has its own Python + packages (completely isolated)
# - Container 2 has its own Python + packages (can't see Container 1)
# - .venv/ inside container = locking a safe inside another safe (redundant)
#
# Analogy:
# Without --system = Hiding keys in a drawer (Docker can't find them)
# With --system = Putting keys on the key hook (Docker finds them easily)
#
# This replaces traditional: pip install -r requirements.txt
RUN uv pip install --system -r pyproject.toml
# 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.
COPY . .
# ============================================================================
# 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
# 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 (installed to system with --system flag)
# 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 because we used --system flag during install
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
# 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, installed to system)
# 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 because we used --system flag during install
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:/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
#
# ============================================================================
|