smart-chatbot-api / Dockerfile
GitHub Actions
Deploy from GitHub Actions (2026-03-03 04:20 UTC)
c4d486b
# ============================================================================
# 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
#
# ============================================================================