diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..8a51e1d --- /dev/null +++ b/.dockerignore @@ -0,0 +1,42 @@ +# Git +.git +.gitignore + +# Python +__pycache__/ +*.py[cod] +*$py.class +*.so +.Python +env/ +venv/ +ENV/ +.venv + +# IDEs +.vscode/ +.idea/ +*.swp +*.swo +*~ + +# OS +.DS_Store +Thumbs.db + +# Documentation +README.md +*.md + +# Docker +Dockerfile +.dockerignore +docker-compose.yml + +# Logs +*.log + +# Testing +.pytest_cache/ +.coverage +htmlcov/ diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..811f7e1 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,47 @@ +# Production Dockerfile for Sunday Comics + +FROM python:3.11-slim + +# Set working directory +WORKDIR /app + +# Install system dependencies +RUN apt-get update && apt-get install -y --no-install-recommends \ + gcc \ + && rm -rf /var/lib/apt/lists/* + +# Create non-root user +RUN useradd -m -u 1000 appuser && chown -R appuser:appuser /app + +# Copy requirements first for better caching +COPY --chown=appuser:appuser requirements.txt . + +# Install Python dependencies +RUN pip install --no-cache-dir -r requirements.txt gunicorn + +# Copy application code +COPY --chown=appuser:appuser . . + +# Switch to non-root user +USER appuser + +# Expose port +EXPOSE 3000 + +# Environment variables +ENV PYTHONUNBUFFERED=1 \ + PORT=3000 + +# Health check +HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \ + CMD python -c "import urllib.request; urllib.request.urlopen('http://localhost:3000').read()" + +# Run with Gunicorn +CMD gunicorn app:app \ + --bind 0.0.0.0:${PORT} \ + --workers 4 \ + --threads 2 \ + --worker-class gthread \ + --access-logfile - \ + --error-logfile - \ + --log-level info diff --git a/README.md b/README.md index 674f63e..83e987a 100644 --- a/README.md +++ b/README.md @@ -17,6 +17,9 @@ sunday/ ├── app.py # Main Flask application ├── comics_data.py # Comic data (edit this to add comics) ├── requirements.txt # Python dependencies +├── Dockerfile # Production Docker image +├── docker-compose.yml # Docker Compose configuration +├── .dockerignore # Docker build exclusions ├── scripts/ # Utility scripts │ ├── add_comic.py # Script to add new comic entries │ └── generate_rss.py # Script to generate RSS feed @@ -108,15 +111,54 @@ This creates/updates `static/feed.rss` ## Production Deployment -For production, you should **NOT** use Flask's built-in development server. Instead: +For production, you should **NOT** use Flask's built-in development server. Choose one of the following deployment methods: -### 1. Generate a Secure Secret Key +### Option 1: Docker (Recommended) + +**1. Generate a secure secret key:** +```bash +python -c "import secrets; print(secrets.token_hex(32))" +``` + +**2. Create a `.env` file:** +```bash +SECRET_KEY=your-generated-secret-key-here +``` + +**3. Build and run with Docker Compose:** +```bash +docker-compose up -d +``` + +**Or build and run manually:** +```bash +# Build the image +docker build -t sunday-comics . + +# Run the container +docker run -d \ + -p 3000:3000 \ + -e SECRET_KEY="your-secret-key" \ + -v $(pwd)/comics_data.py:/app/comics_data.py:ro \ + -v $(pwd)/static/images:/app/static/images:ro \ + --name sunday-comics \ + sunday-comics +``` + +**View logs:** +```bash +docker-compose logs -f +``` + +### Option 2: Manual Deployment with Gunicorn + +**1. Generate a Secure Secret Key** ```bash python -c "import secrets; print(secrets.token_hex(32))" ``` -### 2. Set Environment Variables +**2. Set Environment Variables** ```bash export SECRET_KEY="generated-secret-key-from-above" @@ -124,7 +166,7 @@ export DEBUG=False export PORT=3000 ``` -### 3. Use a Production WSGI Server +**3. Use a Production WSGI Server** **Install Gunicorn:** ```bash @@ -136,17 +178,17 @@ pip install gunicorn gunicorn app:app --bind 0.0.0.0:3000 --workers 4 ``` -### 4. Use a Reverse Proxy (Recommended) +### Using a Reverse Proxy (Recommended) -Set up Nginx or another reverse proxy in front of Gunicorn for: +Set up Nginx or another reverse proxy in front of your app for: - HTTPS/SSL termination - Static file serving - Load balancing - Better security -### 5. Additional Production Considerations +### Additional Production Considerations -- Use a process manager (systemd, supervisor) +- Use a process manager (systemd, supervisor) for non-Docker deployments - Set appropriate file permissions - Enable HTTPS with Let's Encrypt - Consider using a CDN for static assets diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..afcd75c --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,23 @@ +version: '3.8' + +services: + web: + build: . + ports: + - "3000:3000" + environment: + - SECRET_KEY=${SECRET_KEY:-please-change-this-secret-key} + - PORT=3000 + - DEBUG=False + volumes: + # Mount comics data for easy updates without rebuilding + - ./comics_data.py:/app/comics_data.py:ro + - ./static/images:/app/static/images:ro + - ./static/feed.rss:/app/static/feed.rss:ro + restart: unless-stopped + healthcheck: + test: ["CMD", "python", "-c", "import urllib.request; urllib.request.urlopen('http://localhost:3000').read()"] + interval: 30s + timeout: 3s + retries: 3 + start_period: 5s