diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..10c0724 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,63 @@ +# Python +__pycache__/ +*.py[cod] +*$py.class +*.so +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +*.egg-info/ +.installed.cfg +*.egg + +# Virtual environments +.venv/ +venv/ +ENV/ +env/ + +# IDE +.vscode/ +.idea/ +*.swp +*.swo + +# OS +.DS_Store +Thumbs.db + +# Git +.git/ +.gitignore + +# Logs +logs/ +*.log + +# Cache +cache/ + +# Testing +.pytest_cache/ +.coverage +htmlcov/ + +# Documentation +docs/_build/ + +# Docker +Dockerfile* +docker-compose*.yml +.dockerignore +README-Docker.md +deploy.sh diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..74c7c47 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,48 @@ +# Use Python 3.11 slim image +FROM python:3.11-slim + +# Set environment variables +ENV PYTHONUNBUFFERED=1 +ENV PYTHONDONTWRITEBYTECODE=1 +ENV FLASK_ENV=production +ENV FLASK_SECRET=production-secret-change-me + +# Install system dependencies +RUN apt-get update && apt-get install -y \ + gcc \ + default-libmysqlclient-dev \ + pkg-config \ + curl \ + && rm -rf /var/lib/apt/lists/* + +# Create app directory +WORKDIR /app + +# Copy requirements first for better caching +COPY requirements.txt . + +# Install Python dependencies +RUN pip install --no-cache-dir -r requirements.txt + +# Copy application code +COPY . . + +# Create necessary directories +RUN mkdir -p cache logs + +# Expose port +EXPOSE 8000 + +# Health check +HEALTHCHECK --interval=30s --timeout=30s --start-period=5s --retries=3 \ + CMD curl -f http://localhost:8000/ || exit 1 + +# Copy entrypoint script +COPY docker-entrypoint.sh /usr/local/bin/ +RUN chmod +x /usr/local/bin/docker-entrypoint.sh + +# Set entrypoint +ENTRYPOINT ["docker-entrypoint.sh"] + +# Run gunicorn +CMD ["gunicorn", "--config", "gunicorn.conf.py", "web.app:app"] diff --git a/README-Docker.md b/README-Docker.md new file mode 100644 index 0000000..b0dfc15 --- /dev/null +++ b/README-Docker.md @@ -0,0 +1,176 @@ +# Jobs App - Docker Deployment + +This application is a Craigslist job scraper with a Flask web interface. + +## Quick Start with Docker + +### Prerequisites + +- Docker +- Docker Compose + +### Deployment + +1. **Clone the repository and navigate to the project directory** + + ```bash + cd /path/to/jobs-app/jobs + ``` + +2. **Start the application** + + ```bash + docker-compose up --build -d + ``` + +3. **Wait for services to be ready** (about 30 seconds) + + ```bash + # Check if the app is running + curl http://localhost:8000 + ``` + +4. **Access the application** + - Main app: http://localhost:8000 + - Admin interface: http://localhost:8000/admin/users + - Username: `admin` + - Password: `M11ffpgm.` + - Scraper interface: http://localhost:8000/scrape-page + +## Docker Architecture + +### Services + +- **jobs-app**: Flask application with Gunicorn WSGI server +- **mysql**: MySQL 8.0 database + +### Ports + +- 8000: Flask application +- 3306: MySQL database (exposed for external access if needed) + +### Volumes + +- `mysql_data`: Persistent MySQL data storage + +## Configuration + +### Environment Variables + +- `FLASK_ENV`: Set to `production` +- `FLASK_SECRET`: Secret key for Flask sessions + +### Database Configuration + +The database configuration is in `config/settings.json`: + +```json +{ + "database": { + "mysql": { + "host": "mysql", + "user": "jobs", + "password": "jobdb", + "database": "jobs", + "port": 3306 + } + } +} +``` + +## Useful Commands + +```bash +# View logs +docker-compose logs -f + +# Stop services +docker-compose down + +# Restart services +docker-compose restart + +# Rebuild and restart +docker-compose up --build + +# View running containers +docker-compose ps + +# Execute commands in the app container +docker-compose exec jobs-app bash + +# Check database +docker-compose exec mysql mysql -u jobs -p jobs +``` + +## Production Considerations + +1. **Security**: + + - Change default passwords in `docker-compose.yml` + - Use environment variables for secrets + - Configure proper firewall rules + +2. **Scaling**: + + - Adjust Gunicorn workers in `gunicorn.conf.py` + - Consider using a reverse proxy (nginx) + - Implement proper logging and monitoring + +3. **Database**: + + - Use external MySQL for production + - Configure backups + - Set up connection pooling + +4. **Networking**: + - Use proper domain names + - Configure SSL/TLS + - Set up load balancing if needed + +## Troubleshooting + +### Common Issues + +1. **Port conflicts**: Change ports in `docker-compose.yml` +2. **Database connection**: Ensure MySQL container is healthy +3. **Memory issues**: Increase Docker memory limits +4. **Permission issues**: Check file permissions in mounted volumes + +### Logs + +```bash +# Application logs +docker-compose logs jobs-app + +# Database logs +docker-compose logs mysql + +# All logs +docker-compose logs +``` + +## Development + +For development with hot reload: + +```bash +# Run in development mode +docker-compose -f docker-compose.dev.yml up --build +``` + +Create `docker-compose.dev.yml`: + +```yaml +version: "3.8" +services: + jobs-app: + build: . + ports: + - "8000:8000" + environment: + - FLASK_ENV=development + volumes: + - .:/app + command: ["flask", "run", "--host", "0.0.0.0", "--port", "8000"] +``` diff --git a/deploy.sh b/deploy.sh new file mode 100644 index 0000000..c425c9d --- /dev/null +++ b/deploy.sh @@ -0,0 +1,41 @@ +#!/bin/bash +# Deployment script for Jobs App + +set -e + +echo "🚀 Starting Jobs App deployment..." + +# Check if Docker is installed +if ! command -v docker &> /dev/null; then + echo "❌ Docker is not installed. Please install Docker first." + exit 1 +fi + +# Check if docker-compose is installed +if ! command -v docker-compose &> /dev/null; then + echo "❌ docker-compose is not installed. Please install docker-compose first." + exit 1 +fi + +echo "📦 Building and starting services..." +docker-compose up --build -d + +echo "⏳ Waiting for services to be ready..." +sleep 30 + +echo "🔍 Checking service health..." +if curl -f http://localhost:8000/ &> /dev/null; then + echo "✅ Jobs App is running successfully!" + echo "🌐 Access the application at: http://localhost:8000" + echo "📊 Admin interface: http://localhost:8000/admin/users (login: admin/M11ffpgm.)" + echo "🔄 Scraper interface: http://localhost:8000/scrape-page" +else + echo "❌ Jobs App failed to start. Check logs with: docker-compose logs" + exit 1 +fi + +echo "📋 Useful commands:" +echo " View logs: docker-compose logs -f" +echo " Stop services: docker-compose down" +echo " Restart: docker-compose restart" +echo " Rebuild: docker-compose up --build" diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..dbc42b6 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,39 @@ +version: "3.8" + +services: + jobs-app: + build: . + ports: + - "8000:8000" + environment: + - FLASK_ENV=production + - FLASK_SECRET=production-secret-change-me + depends_on: + - mysql + networks: + - jobs-network + restart: unless-stopped + + mysql: + image: mysql:8.0 + environment: + - MYSQL_ROOT_PASSWORD=rootpassword + - MYSQL_DATABASE=jobs + - MYSQL_USER=jobs + - MYSQL_PASSWORD=jobdb + ports: + - "3306:3306" + volumes: + - mysql_data:/var/lib/mysql + - ./mysql-init:/docker-entrypoint-initdb.d + networks: + - jobs-network + restart: unless-stopped + command: --default-authentication-plugin=mysql_native_password + +volumes: + mysql_data: + +networks: + jobs-network: + driver: bridge diff --git a/docker-entrypoint.sh b/docker-entrypoint.sh new file mode 100644 index 0000000..d47708e --- /dev/null +++ b/docker-entrypoint.sh @@ -0,0 +1,35 @@ +#!/bin/bash +# Docker entrypoint script for Jobs App + +set -e + +echo "🚀 Starting Jobs App container..." + +# Wait for MySQL to be ready +echo "⏳ Waiting for MySQL to be ready..." +while ! mysqladmin ping -h mysql -u jobs -pjobdb --silent; do + echo "MySQL is not ready, waiting..." + sleep 2 +done + +echo "✅ MySQL is ready!" + +# Run database setup +echo "🗄️ Setting up database..." +python setup.py mysql-init + +# Seed initial data if needed +echo "🌱 Seeding initial data..." +python -c " +from web.db import db_init +from web.utils import initialize_users_from_settings +db_init() +try: + initialize_users_from_settings() + print('✅ Users seeded successfully') +except Exception as e: + print(f'⚠️ User seeding failed: {e}') +" + +echo "🎯 Starting Gunicorn server..." +exec "$@" diff --git a/gunicorn.conf.py b/gunicorn.conf.py new file mode 100644 index 0000000..53d9b95 --- /dev/null +++ b/gunicorn.conf.py @@ -0,0 +1,38 @@ +# Gunicorn configuration file +import multiprocessing + +# Server socket +bind = "0.0.0.0:8000" +backlog = 2048 + +# Worker processes +workers = multiprocessing.cpu_count() * 2 + 1 +worker_class = "sync" +worker_connections = 1000 +max_requests = 1000 +max_requests_jitter = 50 +timeout = 30 +keepalive = 2 + +# Logging +loglevel = "info" +accesslog = "-" +errorlog = "-" + +# Process naming +proc_name = "jobs_app" + +# Server mechanics +daemon = False +pidfile = "/tmp/gunicorn.pid" +user = None +group = None +tmp_upload_dir = None + +# SSL (if needed) +keyfile = None +certfile = None + +# Application +wsgi_module = "web.app:app" +callable = "app"