feat: add containerization support

- Add Containerfile with multi-stage build for minimal image
- Add .containerignore to exclude unnecessary files
- Add /health endpoint for container health checks
- Update main.py to expose Flask app for gunicorn

Uses Python 3.12-slim base, runs as non-root user, exposes port 8000.
This commit is contained in:
2025-12-22 13:10:47 -07:00
parent e8e30442d8
commit 6dbc84a04c
4 changed files with 116 additions and 4 deletions

38
.containerignore Normal file
View File

@@ -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

51
Containerfile Normal file
View File

@@ -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"]

14
main.py
View File

@@ -1,6 +1,14 @@
def main(): """Application entry point for Sneaky Klaus.
print("Hello from 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__": if __name__ == "__main__":
main() # Development server
app.run(host="0.0.0.0", port=5000, debug=True)

View File

@@ -1,6 +1,6 @@
"""Setup route for initial admin account creation.""" """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.app import bcrypt, db
from src.forms import SetupForm from src.forms import SetupForm
@@ -9,6 +9,21 @@ from src.models import Admin
setup_bp = Blueprint("setup", __name__) 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"]) @setup_bp.route("/setup", methods=["GET", "POST"])
def setup(): def setup():
"""Handle initial admin account setup. """Handle initial admin account setup.