smart-chatbot-api / Dockerfile
GitHub Actions
Deploy from GitHub Actions (2026-06-13 09:21 UTC)
8dfae2b
Raw
History Blame Contribute Delete
12.1 kB
# ============================================================================
# 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
#
# ============================================================================