479 lines
12 KiB
Markdown
479 lines
12 KiB
Markdown
# 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=<output of: python3 -c "import secrets; print(secrets.token_hex(32))">
|
|
DB_HOST=127.0.0.1
|
|
DB_PORT=3306
|
|
DB_NAME=webchecker
|
|
DB_USER=webchecker_user
|
|
DB_PASSWORD=YourStrongPasswordHere
|
|
CRYPTO_SECRET=<output of: python3 -c "from cryptography.fernet import Fernet; print(Fernet.generate_key().decode())">
|
|
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 - <<EOF
|
|
from app import create_app
|
|
app = create_app()
|
|
with app.app_context():
|
|
from config import initialize_database
|
|
initialize_database()
|
|
print("Database initialised successfully.")
|
|
EOF
|
|
'
|
|
```
|
|
|
|
### 4.6 Create the Gunicorn socket directory
|
|
|
|
```bash
|
|
sudo mkdir -p /run/webchecker
|
|
sudo chown webchecker:webchecker /run/webchecker
|
|
```
|
|
|
|
---
|
|
|
|
## 5. Gunicorn systemd Service
|
|
|
|
### 5.1 Create the service unit file
|
|
|
|
```bash
|
|
sudo nano /etc/systemd/system/webchecker.service
|
|
```
|
|
|
|
Paste the following:
|
|
|
|
```ini
|
|
[Unit]
|
|
Description=Website Checker — Gunicorn Application Server
|
|
After=network.target mysql.service
|
|
Requires=mysql.service
|
|
|
|
[Service]
|
|
Type=notify
|
|
User=webchecker
|
|
Group=webchecker
|
|
WorkingDirectory=/opt/webchecker
|
|
EnvironmentFile=/opt/webchecker/.env
|
|
ExecStart=/opt/webchecker/venv/bin/gunicorn \
|
|
--bind unix:/run/webchecker/webchecker.sock \
|
|
--workers 4 \
|
|
--worker-class sync \
|
|
--timeout 120 \
|
|
--access-logfile /var/log/webchecker/access.log \
|
|
--error-logfile /var/log/webchecker/error.log \
|
|
--log-level info \
|
|
wsgi:application
|
|
ExecReload=/bin/kill -s HUP $MAINPID
|
|
KillMode=mixed
|
|
TimeoutStopSec=10
|
|
PrivateTmp=true
|
|
Restart=on-failure
|
|
RestartSec=5
|
|
|
|
[Install]
|
|
WantedBy=multi-user.target
|
|
```
|
|
|
|
### 5.2 Create the log directory
|
|
|
|
```bash
|
|
sudo mkdir -p /var/log/webchecker
|
|
sudo chown webchecker:webchecker /var/log/webchecker
|
|
```
|
|
|
|
### 5.3 Enable and start the service
|
|
|
|
```bash
|
|
sudo systemctl daemon-reload
|
|
sudo systemctl enable webchecker
|
|
sudo systemctl start webchecker
|
|
sudo systemctl status webchecker
|
|
```
|
|
|
|
You should see `Active: active (running)`. The socket file `/run/webchecker/webchecker.sock` will be created automatically.
|
|
|
|
---
|
|
|
|
## 6. Nginx Configuration
|
|
|
|
### 6.1 Create the Nginx site configuration
|
|
|
|
```bash
|
|
sudo nano /etc/nginx/sites-available/webchecker
|
|
```
|
|
|
|
Paste (replace `your-domain.com` with your actual domain):
|
|
|
|
```nginx
|
|
upstream webchecker_app {
|
|
server unix:/run/webchecker/webchecker.sock fail_timeout=0;
|
|
}
|
|
|
|
server {
|
|
listen 80;
|
|
server_name your-domain.com www.your-domain.com;
|
|
|
|
# Security headers
|
|
add_header X-Frame-Options "SAMEORIGIN" always;
|
|
add_header X-Content-Type-Options "nosniff" always;
|
|
add_header X-XSS-Protection "1; mode=block" always;
|
|
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
|
|
|
|
# Client upload size (raise if users upload large files for AI analysis)
|
|
client_max_body_size 20M;
|
|
|
|
# Static files served directly by Nginx (fast path)
|
|
location /static/ {
|
|
alias /opt/webchecker/static/;
|
|
expires 30d;
|
|
access_log off;
|
|
add_header Cache-Control "public, immutable";
|
|
}
|
|
|
|
# Proxy everything else to Gunicorn
|
|
location / {
|
|
proxy_pass http://webchecker_app;
|
|
|
|
proxy_set_header Host $host;
|
|
proxy_set_header X-Real-IP $remote_addr;
|
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
|
proxy_set_header X-Forwarded-Proto $scheme;
|
|
|
|
proxy_connect_timeout 60s;
|
|
proxy_read_timeout 120s;
|
|
proxy_send_timeout 120s;
|
|
|
|
proxy_buffering on;
|
|
proxy_buffer_size 16k;
|
|
proxy_buffers 4 32k;
|
|
}
|
|
|
|
# Logging
|
|
access_log /var/log/nginx/webchecker_access.log;
|
|
error_log /var/log/nginx/webchecker_error.log;
|
|
}
|
|
```
|
|
|
|
### 6.2 Enable the site
|
|
|
|
```bash
|
|
sudo ln -s /etc/nginx/sites-available/webchecker /etc/nginx/sites-enabled/
|
|
sudo nginx -t # test config — must report "syntax is ok"
|
|
sudo systemctl reload nginx
|
|
```
|
|
|
|
---
|
|
|
|
## 7. SSL/TLS with Certbot
|
|
|
|
```bash
|
|
sudo certbot --nginx -d your-domain.com -d www.your-domain.com
|
|
```
|
|
|
|
Certbot will automatically:
|
|
- Obtain a Let's Encrypt certificate
|
|
- Modify the Nginx config to add HTTPS (port 443)
|
|
- Insert a redirect from HTTP → HTTPS
|
|
- Schedule automatic renewal
|
|
|
|
**Verify auto-renewal works:**
|
|
|
|
```bash
|
|
sudo certbot renew --dry-run
|
|
```
|
|
|
|
After Certbot finishes, add an HSTS header inside the `server` block that Certbot created for port 443:
|
|
|
|
```nginx
|
|
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
|
|
```
|
|
|
|
Then reload Nginx:
|
|
|
|
```bash
|
|
sudo nginx -t && sudo systemctl reload nginx
|
|
```
|
|
|
|
---
|
|
|
|
## 8. Firewall Rules
|
|
|
|
```bash
|
|
# Allow SSH (adjust port if you changed it)
|
|
sudo ufw allow 22/tcp
|
|
|
|
# Allow HTTP and HTTPS
|
|
sudo ufw allow 80/tcp
|
|
sudo ufw allow 443/tcp
|
|
|
|
# Enable the firewall
|
|
sudo ufw enable
|
|
|
|
# Verify
|
|
sudo ufw status verbose
|
|
```
|
|
|
|
> 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.*
|