diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..edc4812 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,53 @@ +# Git +.git +.gitignore + +# Python +__pycache__ +*.pyc +*.pyo +*.pyd +.Python +*.so +*.egg +*.egg-info +dist +build +.pytest_cache +.coverage +htmlcov +.mypy_cache +.dmypy.json +dmypy.json + +# Virtual environments +venv +env +ENV +.venv + +# IDE +.vscode +.idea +*.swp +*.swo +*~ + +# Environment files +.env +.env.local +.env.*.local + +# Documentation +README.md +*.md + +# Lock files (not needed for pip install) +uv.lock + +# Vercel +vercel.json + +# Distribution files +*.tar.gz +*.zip diff --git a/DOCKER.md b/DOCKER.md new file mode 100644 index 0000000..b9337b5 --- /dev/null +++ b/DOCKER.md @@ -0,0 +1,215 @@ +# Docker Setup Guide + +This document explains how to build and run the Akern-Genai application using Docker. + +## Overview + +The Docker configuration includes: + +- **Multi-stage build** for smaller image size +- **Entrypoint script** with proper signal handling and exit status management +- **Configurable port** via environment variables +- **Health checks** for monitoring +- **Non-root user** for security +- **Optimized build** with `.dockerignore` + +## Files + +- [`Dockerfile`](Dockerfile) - Multi-stage Docker build configuration +- [`entrypoint.sh`](entrypoint.sh) - Entrypoint script with signal handling +- [`docker-compose.yml`](docker-compose.yml) - Docker Compose configuration +- [`.dockerignore`](.dockerignore) - Files excluded from Docker build context + +## Environment Variables + +The following environment variables can be configured: + +| Variable | Default | Description | +| ----------- | --------- | ----------------------------------------------------- | +| `PORT` | `8000` | Port on which the application listens | +| `HOST` | `0.0.0.0` | Host address to bind to | +| `WORKERS` | `1` | Number of uvicorn worker processes | +| `LOG_LEVEL` | `info` | Logging level (debug, info, warning, error, critical) | + +## Building the Image + +### Using Docker directly: + +```bash +docker build -t akern-genai:latest . +``` + +### Using Docker Compose: + +```bash +docker-compose build +``` + +## Running the Container + +### Using Docker directly: + +```bash +# Run with default configuration (port 8000) +docker run -p 8000:8000 akern-genai:latest + +# Run with custom port +docker run -p 3000:3000 -e PORT=3000 akern-genai:latest + +# Run with multiple workers +docker run -p 8000:8000 -e WORKERS=4 akern-genai:latest + +# Run with custom log level +docker run -p 8000:8000 -e LOG_LEVEL=debug akern-genai:latest +``` + +### Using Docker Compose: + +```bash +# Run with default configuration +docker-compose up + +# Run with custom port +PORT=3000 docker-compose up + +# Run in detached mode +docker-compose up -d + +# Stop the container +docker-compose down +``` + +### Using environment file: + +Create a `.env` file in the project root: + +```env +PORT=8000 +HOST=0.0.0.0 +WORKERS=1 +LOG_LEVEL=info +``` + +Then run: + +```bash +docker-compose up +``` + +## Signal Handling + +The entrypoint script properly handles termination signals: + +- **SIGTERM** - Graceful shutdown (sent by `docker stop`) +- **SIGINT** - Interrupt signal (Ctrl+C) + +The script will: + +1. Forward the signal to uvicorn +2. Wait up to 30 seconds for graceful shutdown +3. Force kill if shutdown doesn't complete in time +4. Exit with the same status code as uvicorn + +## Health Check + +The container includes a health check that monitors the application: + +- **Interval**: 30 seconds +- **Timeout**: 10 seconds +- **Start period**: 5 seconds +- **Retries**: 3 + +Check container health: + +```bash +docker ps +docker inspect | grep -A 10 Health +``` + +## Multi-Stage Build + +The Dockerfile uses a two-stage build: + +### Stage 1: Builder + +- Installs build dependencies (gcc, musl-dev, etc.) +- Creates a virtual environment +- Installs Python dependencies + +### Stage 2: Runtime + +- Uses minimal Alpine Linux +- Only includes runtime dependencies +- Copies the virtual environment from builder +- Runs as non-root user + +This results in a significantly smaller final image. + +## Security Features + +- **Non-root user**: Application runs as `appuser` (UID 1000) +- **Minimal base image**: Alpine Linux with only necessary packages +- **No build tools in runtime**: Compiler and build tools are not included in final image + +## Troubleshooting + +### Container exits immediately + +Check logs: + +```bash +docker logs +``` + +### Port already in use + +Change the port: + +```bash +docker run -p 3000:3000 -e PORT=3000 akern-genai:latest +``` + +### Health check failing + +Verify the application is running: + +```bash +docker exec -it wget -O- http://localhost:8000/ +``` + +### View real-time logs + +```bash +docker logs -f +``` + +## Image Size Comparison + +- **Before**: ~200-300 MB (single stage with build tools) +- **After**: ~100-150 MB (multi-stage, minimal runtime) + +## Production Considerations + +For production deployment: + +1. **Use specific image tags** instead of `latest` +2. **Set appropriate worker count** based on CPU cores +3. **Configure resource limits** in docker-compose.yml or Kubernetes +4. **Use a reverse proxy** (nginx, traefik) for SSL termination +5. **Set up logging aggregation** (ELK, Loki, etc.) +6. **Configure monitoring** (Prometheus, Grafana) + +Example docker-compose.yml with resource limits: + +```yaml +services: + app: + deploy: + resources: + limits: + cpus: "2" + memory: 1G + reservations: + cpus: "0.5" + memory: 512M +``` diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..1d603a1 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,82 @@ +# ============================================ +# Stage 1: Builder +# ============================================ +FROM python:3.13-alpine AS builder + +# Install build dependencies +RUN apk add --no-cache \ + gcc \ + musl-dev \ + libffi-dev \ + openssl-dev \ + cargo + +# Create virtual environment +RUN python -m venv /opt/venv +ENV PATH="/opt/venv/bin:$PATH" + +# Set working directory +WORKDIR /build + +# Copy requirements first for better caching +COPY requirements.txt . + +# Install dependencies +RUN pip install --no-cache-dir --upgrade pip && \ + pip install --no-cache-dir -r requirements.txt + +# ============================================ +# Stage 2: Runtime +# ============================================ +FROM python:3.13-alpine AS runtime + +# Install runtime dependencies only +RUN apk add --no-cache \ + libstdc++ \ + ca-certificates + +# Create non-root user for security +RUN addgroup -g 1000 appuser && \ + adduser -D -u 1000 -G appuser appuser + +# Copy virtual environment from builder +COPY --from=builder /opt/venv /opt/venv +ENV PATH="/opt/venv/bin:$PATH" + +# Create application directory +RUN mkdir /app && \ + chown -R appuser:appuser /app + +WORKDIR /app + +# Copy application files +COPY --chown=appuser:appuser app.py . +COPY --chown=appuser:appuser main.py . +COPY --chown=appuser:appuser static ./static + +# Copy and setup entrypoint script +COPY entrypoint.sh /usr/local/bin/entrypoint.sh +RUN chmod +x /usr/local/bin/entrypoint.sh + +# Switch to non-root user +USER appuser + +# Expose default port (can be overridden via PORT env var) +EXPOSE 8000 + +# Health check +HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \ + CMD wget --no-verbose --tries=1 --spider http://localhost:${PORT:-8000}/ || exit 1 + +# Set environment variables +ENV PYTHONUNBUFFERED=1 \ + PYTHONDONTWRITEBYTECODE=1 \ + PORT=8000 \ + HOST=0.0.0.0 \ + WORKERS=1 \ + LOG_LEVEL=info + +# Use entrypoint script +ENTRYPOINT ["/usr/local/bin/entrypoint.sh"] + + diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..79e2ef6 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,32 @@ +services: + app: + build: + context: . + dockerfile: Dockerfile + container_name: akern-genai-app + ports: + - "${PORT:-8000}:8000" + environment: + # Application configuration + - PORT=${PORT:-8000} + - HOST=${HOST:-0.0.0.0} + - WORKERS=${WORKERS:-1} + - LOG_LEVEL=${LOG_LEVEL:-info} + + # Google GenAI configuration (load from .env file) + - GOOGLE_APPLICATION_CREDENTIALS=${GOOGLE_APPLICATION_CREDENTIALS} + restart: unless-stopped + healthcheck: + test: + [ + "CMD", + "wget", + "--no-verbose", + "--tries=1", + "--spider", + "http://${HOST:-127.0.0.1}:${PORT:-8000}/", + ] + interval: 30s + timeout: 10s + retries: 3 + start_period: 5s diff --git a/entrypoint.sh b/entrypoint.sh new file mode 100644 index 0000000..d180517 --- /dev/null +++ b/entrypoint.sh @@ -0,0 +1,55 @@ +#!/bin/sh +set -e + +# Default values +PORT="${PORT:-8000}" +HOST="${HOST:-0.0.0.0}" +WORKERS="${WORKERS:-1}" +LOG_LEVEL="${LOG_LEVEL:-info}" + +# Display configuration +echo "==========================================" +echo "Starting FastAPI application" +echo "==========================================" +echo "Host: ${HOST}" +echo "Port: ${PORT}" +echo "Workers: ${WORKERS}" +echo "Log Level: ${LOG_LEVEL}" +echo "==========================================" + +# Trap signals for graceful shutdown +cleanup() { + echo "Received termination signal, shutting down gracefully..." + # Send SIGTERM to uvicorn process group + if [ -n "$UVICORN_PID" ]; then + kill -TERM "$UVICORN_PID" 2>/dev/null || true + # Wait for uvicorn to exit, but with a timeout + timeout 30 sh -c "while kill -0 $UVICORN_PID 2>/dev/null; do sleep 1; done" || { + echo "Uvicorn did not shut down gracefully, forcing exit..." + kill -KILL "$UVICORN_PID" 2>/dev/null || true + } + fi + exit 0 +} + +# Register signal handlers +trap cleanup SIGTERM SIGINT + +# Start uvicorn in the background to capture PID +uvicorn app:app \ + --host "${HOST}" \ + --port "${PORT}" \ + --workers "${WORKERS}" \ + --log-level "${LOG_LEVEL}" \ + --access-log \ + & + +UVICORN_PID=$! + +# Wait for uvicorn process +wait $UVICORN_PID +EXIT_STATUS=$? + +# Exit with the same status as uvicorn +echo "Uvicorn exited with status: ${EXIT_STATUS}" +exit ${EXIT_STATUS}