# Website Checker — Production Deployment Guide ### Ubuntu Server · Nginx · Gunicorn · MySQL · systemd --- ## Table of Contents 1. [Prerequisites](#1-prerequisites) 2. [Server Initial Setup](#2-server-initial-setup) 3. [MySQL Database](#3-mysql-database) 4. [Application Deployment](#4-application-deployment) 5. [Gunicorn systemd Service](#5-gunicorn-systemd-service) 6. [Nginx Configuration](#6-nginx-configuration) 7. [SSL/TLS with Certbot](#7-ssltls-with-certbot) 8. [Firewall Rules](#8-firewall-rules) 9. [Post-Deployment Verification](#9-post-deployment-verification) 10. [Ongoing Maintenance](#10-ongoing-maintenance) 11. [Troubleshooting](#11-troubleshooting) --- ## 1. Prerequisites | Item | Requirement | |------|-------------| | OS | Ubuntu 22.04 LTS or 24.04 LTS | | RAM | ≥ 1 GB (2 GB recommended) | | Disk | ≥ 10 GB | | Access | Root or sudo user | | Domain | A DNS A-record pointing to your server's IP | | Python | 3.10+ (pre-installed on Ubuntu 22.04+) | --- ## 2. Server Initial Setup ### 2.1 Update the system ```bash sudo apt update && sudo apt upgrade -y ``` ### 2.2 Install system dependencies ```bash sudo apt install -y \ python3 python3-pip python3-venv \ mysql-server \ nginx \ certbot python3-certbot-nginx \ git curl ufw ``` ### 2.3 Create a dedicated application user ```bash sudo useradd --system --shell /bin/bash --create-home webchecker ``` --- ## 3. MySQL Database ### 3.1 Secure the MySQL installation ```bash sudo mysql_secure_installation ``` Follow the prompts: set a root password, remove anonymous users, disallow remote root login, remove test database. ### 3.2 Create the database and application user ```bash sudo mysql -u root -p ``` Inside the MySQL shell: ```sql CREATE DATABASE webchecker CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; CREATE USER 'webchecker_user'@'127.0.0.1' IDENTIFIED BY 'YourStrongPasswordHere'; GRANT ALL PRIVILEGES ON webchecker.* TO 'webchecker_user'@'127.0.0.1'; FLUSH PRIVILEGES; EXIT; ``` > **Security note:** Use `127.0.0.1` (not `localhost`) so the connector uses TCP rather than the UNIX socket, which matches the `DB_HOST=127.0.0.1` setting in `.env`. --- ## 4. Application Deployment ### 4.1 Transfer the application files Option A — copy from your workstation: ```bash scp -r ./webchecker_web/ youruser@your-server-ip:/tmp/webchecker_web ``` Option B — clone from a private Git repository: ```bash sudo -u webchecker git clone https://github.com/your-org/webchecker.git /opt/webchecker ``` Then move files into place (Option A): ```bash sudo mv /tmp/webchecker_web /opt/webchecker sudo chown -R webchecker:webchecker /opt/webchecker ``` ### 4.2 Create the Python virtual environment ```bash sudo -u webchecker python3 -m venv /opt/webchecker/venv ``` ### 4.3 Install Python dependencies ```bash sudo -u webchecker /opt/webchecker/venv/bin/pip install --upgrade pip sudo -u webchecker /opt/webchecker/venv/bin/pip install -r /opt/webchecker/requirements.txt ``` ### 4.4 Configure environment variables ```bash sudo cp /opt/webchecker/.env.example /opt/webchecker/.env sudo nano /opt/webchecker/.env ``` Fill in every value — especially: ```dotenv SECRET_KEY= DB_HOST=127.0.0.1 DB_PORT=3306 DB_NAME=webchecker DB_USER=webchecker_user DB_PASSWORD=YourStrongPasswordHere CRYPTO_SECRET= FLASK_ENV=production ``` Secure the file so only the application user can read it: ```bash sudo chown webchecker:webchecker /opt/webchecker/.env sudo chmod 600 /opt/webchecker/.env ``` ### 4.5 Initialise the database schema The application auto-creates all tables on first startup via `initialize_database()`. Run it once manually to verify: ```bash sudo -u webchecker bash -c ' cd /opt/webchecker source venv/bin/activate python - < MySQL port 3306 is intentionally **not** opened — the application connects via `127.0.0.1` (localhost), so no external exposure is required. --- ## 9. Post-Deployment Verification ### 9.1 Check the Gunicorn service ```bash sudo systemctl status webchecker sudo journalctl -u webchecker -n 50 --no-pager ``` ### 9.2 Check Nginx ```bash sudo systemctl status nginx sudo tail -f /var/log/nginx/webchecker_error.log ``` ### 9.3 Smoke-test the application ```bash curl -I https://your-domain.com/auth/login # Expect: HTTP/2 200 ``` Open a browser and navigate to `https://your-domain.com`. You should see the login page. ### 9.4 First login Log in with the default admin credentials set during `initialize_database()`. **Change the password immediately** via Admin → Change Password. --- ## 10. Ongoing Maintenance ### Deploy a new version ```bash # 1. Copy/pull new files to /opt/webchecker # 2. Install any new dependencies sudo -u webchecker /opt/webchecker/venv/bin/pip install -r /opt/webchecker/requirements.txt # 3. Reload Gunicorn (zero-downtime — workers are replaced one by one) sudo systemctl reload webchecker # 4. If Nginx config changed: sudo nginx -t && sudo systemctl reload nginx ``` ### View application logs ```bash # Gunicorn access log sudo tail -f /var/log/webchecker/access.log # Gunicorn error log sudo tail -f /var/log/webchecker/error.log # systemd journal (includes startup errors) sudo journalctl -u webchecker -f ``` ### Backup the database ```bash mysqldump -u webchecker_user -p webchecker | gzip > ~/webchecker_$(date +%Y%m%d).sql.gz ``` Automate with a daily cron: ```bash sudo crontab -e # Add: 0 2 * * * mysqldump -u webchecker_user -pYourPassword webchecker | gzip > /var/backups/webchecker_$(date +\%Y\%m\%d).sql.gz ``` ### Rotate logs Create `/etc/logrotate.d/webchecker`: ``` /var/log/webchecker/*.log { daily missingok rotate 14 compress delaycompress notifempty sharedscripts postrotate systemctl reload webchecker > /dev/null 2>&1 || true endscript } ``` --- ## 11. Troubleshooting | Symptom | Likely Cause | Fix | |---------|--------------|-----| | `502 Bad Gateway` | Gunicorn not running or socket missing | `sudo systemctl restart webchecker`; check `journalctl -u webchecker` | | `connect() to unix:/run/webchecker/webchecker.sock failed (13: Permission denied)` | Nginx user can't read socket | Add `nginx` to `webchecker` group: `sudo usermod -aG webchecker www-data`, then `sudo systemctl restart nginx` | | `OperationalError: Access denied for user` | Wrong DB credentials in `.env` | Verify `DB_USER`/`DB_PASSWORD` match what was set in MySQL | | `ImportError` or `ModuleNotFoundError` | Dependency not installed in venv | Run `pip install -r requirements.txt` with the venv active | | `cryptography.fernet.InvalidToken` | `CRYPTO_SECRET` changed after credentials were stored | Restore the original key from backup; never rotate without re-encrypting stored data | | Sessions expiring immediately | `SECRET_KEY` changed between restarts | Keep `SECRET_KEY` stable in `.env`; never regenerate it on a live system | | `413 Request Entity Too Large` | File upload exceeds `client_max_body_size` | Increase `client_max_body_size` in the Nginx config | | Static files returning 404 | Wrong `alias` path in Nginx | Confirm `/opt/webchecker/static/` exists and the `alias` directive ends with `/` | --- *Generated for Website Checker Web — Flask/MySQL/Nginx/Gunicorn/Ubuntu deployment.*