diff --git a/.containerignore b/.containerignore new file mode 100644 index 0000000..02949b2 --- /dev/null +++ b/.containerignore @@ -0,0 +1,38 @@ +# Git +.git +.gitignore + +# Python +__pycache__ +*.py[cod] +*$py.class +*.so +.Python +.venv +.env +*.egg-info +dist +build + +# Testing +.pytest_cache +.coverage +htmlcov +.tox +.mypy_cache +.ruff_cache + +# IDE +.idea +.vscode +*.swp +*.swo + +# Project specific +data/ +*.db +.claude +docs/ +tests/ +*.md +.pre-commit-config.yaml diff --git a/Containerfile b/Containerfile new file mode 100644 index 0000000..94f2bec --- /dev/null +++ b/Containerfile @@ -0,0 +1,51 @@ +# Build stage - install dependencies +FROM python:3.12-slim AS builder + +WORKDIR /app + +# Install uv for fast dependency management +COPY --from=ghcr.io/astral-sh/uv:latest /uv /usr/local/bin/uv + +# Copy dependency files +COPY pyproject.toml uv.lock ./ + +# Install production dependencies only +RUN uv sync --frozen --no-dev --no-install-project + +# Runtime stage - minimal image +FROM python:3.12-slim AS runtime + +WORKDIR /app + +# Create non-root user +RUN useradd --create-home --shell /bin/bash appuser + +# Copy virtual environment from builder +COPY --from=builder /app/.venv /app/.venv + +# Copy application code +COPY src/ ./src/ +COPY migrations/ ./migrations/ +COPY alembic.ini main.py ./ + +# Set environment variables +ENV PATH="/app/.venv/bin:$PATH" +ENV PYTHONUNBUFFERED=1 +ENV PYTHONDONTWRITEBYTECODE=1 +ENV FLASK_ENV=production + +# Create data directory and set permissions +RUN mkdir -p /app/data && chown -R appuser:appuser /app + +# Switch to non-root user +USER appuser + +# Expose port +EXPOSE 8000 + +# Health check +HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \ + CMD python -c "import urllib.request; urllib.request.urlopen('http://localhost:8000/health')" || exit 1 + +# Run with gunicorn +CMD ["gunicorn", "--bind", "0.0.0.0:8000", "--workers", "2", "--threads", "4", "main:app"] diff --git a/main.py b/main.py index b41f90b..46994bb 100644 --- a/main.py +++ b/main.py @@ -1,6 +1,14 @@ -def main(): - print("Hello from sneaky-klaus!") +"""Application entry point for Sneaky Klaus. +This module creates the Flask application instance for use with +gunicorn or the Flask development server. +""" + +from src.app import create_app + +# Create app instance for gunicorn (production) +app = create_app() if __name__ == "__main__": - main() + # Development server + app.run(host="0.0.0.0", port=5000, debug=True) diff --git a/src/routes/setup.py b/src/routes/setup.py index e9c0154..4433ce4 100644 --- a/src/routes/setup.py +++ b/src/routes/setup.py @@ -1,6 +1,6 @@ """Setup route for initial admin account creation.""" -from flask import Blueprint, abort, redirect, render_template, session, url_for +from flask import Blueprint, abort, jsonify, redirect, render_template, session, url_for from src.app import bcrypt, db from src.forms import SetupForm @@ -9,6 +9,21 @@ from src.models import Admin setup_bp = Blueprint("setup", __name__) +@setup_bp.route("/health") +def health(): + """Health check endpoint for container orchestration. + + Returns: + JSON response with health status. + """ + try: + # Check database connectivity + db.session.execute(db.text("SELECT 1")) + return jsonify({"status": "healthy", "database": "connected"}), 200 + except Exception as e: + return jsonify({"status": "unhealthy", "error": str(e)}), 503 + + @setup_bp.route("/setup", methods=["GET", "POST"]) def setup(): """Handle initial admin account setup.