BPD Scraper - Rocky Linux 10 Deployment Guide
Last Updated: May 16, 2026
Target OS: Rocky Linux 10
Author: Tyler Lehane
For day-to-day deploys: push to
master— GitLab CI/CD handles it automatically.
This guide covers manual/from-scratch setup. Seesetup/README.mdfor the quick-reference version.
Table of Contents
- Prerequisites
- System Setup
- Database Installation
- Application Deployment
- Web Server Configuration
- Daily Automation
- Troubleshooting
- Verification Checklist
Prerequisites
Hardware Requirements
- CPU: 2+ cores
- RAM: 2GB minimum (4GB recommended for 5k+ records)
- Storage: 20GB minimum for database + logs
- Network: Internet access for web scraping + API calls
Software Requirements
- Rocky Linux 10 (tested on 10.0.26200)
- SSH access with sudo privileges
- Git (optional, for cloning repo)
Network Ports
- 5432: PostgreSQL (LAN only — pgAdmin4 access; not exposed to internet)
- 8000: Flask/Gunicorn (localhost only)
- 80: Nginx (LAN + Cloudflare Tunnel)
- 443: Cloudflare Tunnel only (no local cert needed)
System Setup
1. Update System
sudo dnf update -y
sudo dnf install -y git wget curl vim htop
2. Create Application User
sudo useradd -m -s /bin/bash flaskapp
sudo usermod -aG wheel flaskapp # Add to sudoers if needed
3. Set Up SSH Key Access (Optional)
sudo -u flaskapp mkdir -p /home/flaskapp/.ssh
sudo chown flaskapp:flaskapp /home/flaskapp/.ssh
sudo chmod 700 /home/flaskapp/.ssh
# Copy public key to authorized_keys
# sudo vi /home/flaskapp/.ssh/authorized_keys
4. Create Application Directory
sudo mkdir -p /opt/flaskapp
sudo chown flaskapp:flaskapp /opt/flaskapp
sudo chmod 755 /opt/flaskapp
5. Clone/Copy Application Code
cd /opt/flaskapp
# Option A: Git clone
git clone <repo-url> .
# Option B: Copy from local
scp -r ./* [email protected]:/opt/flaskapp/
# Set permissions
sudo chown -R flaskapp:flaskapp /opt/flaskapp
Database Installation
1. Install PostgreSQL 15
sudo dnf install -y postgresql15-server postgresql15-contrib postgresql15-libs
2. Initialize Database Cluster
sudo /usr/pgsql-15/bin/postgresql-15-setup initdb
sudo systemctl enable postgresql-15
sudo systemctl start postgresql-15
3. Configure PostgreSQL Network Access
Edit /var/lib/pgsql/15/data/postgresql.conf:
sudo vi /var/lib/pgsql/15/data/postgresql.conf
Find and modify:
listen_addresses = '*' # Listen on all interfaces
port = 5432
shared_buffers = 256MB # Adjust for 4GB RAM systems
4. Configure pg_hba.conf (Authentication)
Edit /var/lib/pgsql/15/data/pg_hba.conf:
sudo vi /var/lib/pgsql/15/data/pg_hba.conf
Add this line to allow network connections from your IP:
host all all 192.168.68.0/24 md5
host all all 0.0.0.0/0 md5 # For development only
5. Restart PostgreSQL
sudo systemctl restart postgresql-15
6. Create Database and User
sudo -u postgres psql << 'EOF'
CREATE DATABASE bpd;
CREATE USER bpd_user WITH PASSWORD 'YOUR_SECURE_PASSWORD_HERE';
ALTER ROLE bpd_user SET client_encoding TO 'utf8';
ALTER ROLE bpd_user SET default_transaction_isolation TO 'read committed';
ALTER ROLE bpd_user SET default_transaction_deferrable TO on;
ALTER ROLE bpd_user SET default_transaction_read_committed TO on;
GRANT ALL PRIVILEGES ON DATABASE bpd TO bpd_user;
\c bpd
GRANT ALL PRIVILEGES ON SCHEMA public TO bpd_user;
ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT ALL ON TABLES TO bpd_user;
ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT ALL ON SEQUENCES TO bpd_user;
ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT ALL ON FUNCTIONS TO bpd_user;
EOF
7. Create Database Schema
sudo -u postgres psql -d bpd << 'EOF'
CREATE TABLE incidents (
id SERIAL PRIMARY KEY,
incident_date TEXT,
incident_time TEXT,
case_number TEXT,
location TEXT,
incident_type TEXT,
description TEXT,
scraped_at TEXT,
query_start TEXT,
query_end TEXT,
parse_status TEXT,
raw_extra TEXT,
location_lat NUMERIC(11,8),
location_lon NUMERIC(11,8),
geocode_status VARCHAR(20) DEFAULT 'unknown',
UNIQUE(case_number, incident_date)
);
CREATE INDEX idx_incident_date ON incidents(incident_date);
CREATE INDEX idx_case_number ON incidents(case_number);
CREATE INDEX idx_geocode_status ON incidents(geocode_status);
EOF
8. Verify PostgreSQL Connection
PGPASSWORD='YOUR_SECURE_PASSWORD_HERE' psql -U bpd_user -d bpd -h localhost -c "SELECT version();"
Application Deployment
1. Install Python 3.12
sudo dnf install -y python3.12 python3.12-devel python3.12-pip
2. Create Virtual Environment
cd /opt/flaskapp
sudo -u flaskapp python3.12 -m venv .venv
sudo -u flaskapp .venv/bin/pip install --upgrade pip setuptools wheel
3. Install Python Dependencies
cd /opt/flaskapp
sudo -u flaskapp .venv/bin/pip install -r requirements.txt
Important: Create requirements.txt if it doesn't exist:
Flask==2.3.0
psycopg2-binary==2.9.0
python-dotenv==1.0.0
gunicorn==25.0.0
playwright>=1.40.0
beautifulsoup4>=4.12.0
lxml>=5.0.0
requests>=2.28.0
4. Install Playwright Browsers
sudo -u flaskapp .venv/bin/playwright install chromium
5. Create Environment Configuration
sudo -u flaskapp tee /opt/flaskapp/.env > /dev/null << 'EOF'
PG_HOST=localhost
PG_PORT=5432
PG_DB=bpd
PG_USER=bpd_user
PG_PASSWORD=YOUR_SECURE_PASSWORD_HERE
FLASK_ENV=production
FLASK_DEBUG=0
EOF
sudo chmod 600 /opt/flaskapp/.env
6. Set Up Data Directories
sudo -u flaskapp mkdir -p /opt/flaskapp/data/{raw,parsed,output}
sudo -u flaskapp mkdir -p /opt/flaskapp/logs
sudo chmod 755 /opt/flaskapp/data/{raw,parsed,output}
sudo chmod 777 /opt/flaskapp/logs
Web Server Configuration
1. Install Nginx
sudo dnf install -y nginx
sudo systemctl enable nginx
2. Create Nginx Configuration
sudo tee /etc/nginx/conf.d/flaskapp.conf > /dev/null << 'EOF'
upstream flask_app {
server 127.0.0.1:8000;
}
server {
listen 80;
server_name _;
client_max_body_size 10M;
location / {
proxy_pass http://flask_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_redirect off;
proxy_connect_timeout 60s;
proxy_send_timeout 60s;
proxy_read_timeout 60s;
}
location /static/ {
alias /opt/flaskapp/bpd_dashboard/static/;
expires 30d;
}
}
EOF
3. Test Nginx Configuration
sudo nginx -t
4. Start Nginx
sudo systemctl restart nginx
Flask Application Service
1. Create Systemd Service File
sudo tee /etc/systemd/system/flaskapp.service > /dev/null << 'EOF'
[Unit]
Description=Flask app via gunicorn (TCP port 8000)
After=network.target postgresql.service
Wants=postgresql.service
[Service]
Type=simple
User=flaskapp
Group=flaskapp
WorkingDirectory=/opt/flaskapp
UMask=0002
Environment="PG_HOST=localhost"
Environment="PG_PORT=5432"
Environment="PG_DB=bpd"
Environment="PG_USER=bpd_user"
Environment="PG_PASSWORD=YOUR_SECURE_PASSWORD_HERE"
Environment="SECRET_KEY=REPLACE_WITH_64_CHAR_HEX"
Environment="TURNSTILE_SITEKEY=0x4AAAAAADQNyPv4m4YYHmbx"
Environment="TURNSTILE_SECRET=REPLACE_WITH_REAL_SECRET"
ExecStart=/opt/flaskapp/.venv/bin/gunicorn --workers 3 --bind 127.0.0.1:8000 --env SCRIPT_NAME=/dashboard dashboard.app:app
Restart=on-failure
RestartSec=2
[Install]
WantedBy=multi-user.target
EOF
sudo systemctl daemon-reload
sudo systemctl enable flaskapp
Generate SECRET_KEY if needed:
python3 -c "import secrets; print(secrets.token_hex(32))"
2. Start Flask Service
sudo systemctl start flaskapp
sudo systemctl status flaskapp
3. Verify Service
curl -s -o /dev/null -w "%{http_code}" http://localhost:8000/dashboard/incidents
# Returns 302 (redirect to Turnstile gate) — this is correct
Daily Automation (Cron)
run_daily_win.py in bpd_scraper/ is the cron entry point — it runs scraper → parse → geocode in sequence and logs to bpd_scraper/logs/cron.log.
1. Add Crontab Entry
sudo -u flaskapp crontab -e
Add this line (runs daily at 7:30 AM):
30 7 * * * /opt/flaskapp/.venv/bin/python /opt/flaskapp/bpd_scraper/run_daily_win.py >> /opt/flaskapp/bpd_scraper/logs/cron.log 2>&1
2. Verify Cron Setup
sudo -u flaskapp crontab -l
tail -f /opt/flaskapp/bpd_scraper/logs/cron.log
SELinux Configuration (Production)
Option A: Disable SELinux (Development/Testing)
sudo setenforce 0
To make permanent, edit /etc/selinux/config:
sudo vi /etc/selinux/config
# Set: SELINUX=disabled
Option B: Configure SELinux Policy (Production)
# Allow Nginx to connect to Flask app
sudo setsebool -P httpd_can_network_connect on
# Allow PostgreSQL network access
sudo semanage port -a -t postgresql_port_t -p tcp 5432
Firewall Configuration
1. Open Firewall Ports
sudo firewall-cmd --permanent --add-service=http
sudo firewall-cmd --permanent --add-service=https
sudo firewall-cmd --reload
2. Verify Firewall Rules
sudo firewall-cmd --list-all
Verification Checklist
Run through this after deployment:
-
[ ] PostgreSQL Running
bash sudo systemctl status postgresql-15 PGPASSWORD='PASSWORD' psql -U bpd_user -d bpd -h localhost -c "SELECT COUNT(*) FROM incidents;" -
[ ] Flask Service Running
bash sudo systemctl status flaskapp # Expects 302 (Turnstile gate redirect — correct behavior): curl -s -o /dev/null -w "%{http_code}" http://localhost:8000/dashboard/incidents -
[ ] Nginx Running
bash sudo systemctl status nginx curl http://localhost/ -
[ ] Cron Job Configured
bash sudo -u flaskapp crontab -l -
[ ] Logs Directory Writable
bash ls -la /opt/flaskapp/logs/ touch /opt/flaskapp/logs/test.txt && rm /opt/flaskapp/logs/test.txt -
[ ] Database Accessible from Flask
bash # API endpoints still require an authenticated session cookie; # a 302 response here means the gate is working correctly: curl -s -o /dev/null -w "%{http_code}" http://localhost:8000/dashboard/api/incidents
Troubleshooting
Flask Service Won't Start
# Check service logs
sudo journalctl -u flaskapp -n 50
sudo systemctl status flaskapp -l
# Check Flask logs
tail -50 /opt/flaskapp/logs/error.log
# Common issues:
# - PostgreSQL not running: sudo systemctl start postgresql-15
# - .env file permissions: sudo chmod 600 /opt/flaskapp/.env
# - Port already in use: sudo lsof -i :8000
Database Connection Fails
# Test connection directly
PGPASSWORD='PASSWORD' psql -U bpd_user -d bpd -h localhost -c "SELECT 1;"
# Check PostgreSQL is listening
sudo netstat -tlnp | grep 5432
# Check pg_hba.conf for connection rules
sudo cat /var/lib/pgsql/15/data/pg_hba.conf
Nginx 502 Bad Gateway
# Verify Flask is running
curl http://127.0.0.1:8000/
# Check Nginx logs
sudo tail -50 /var/log/nginx/error.log
# Check Flask is binding to correct port/address
sudo lsof -i :8000
Cron Job Not Running
# Check cron logs
sudo journalctl -u crond -n 50
# Verify crontab is set
sudo -u flaskapp crontab -l
# Test script manually
sudo -u flaskapp /opt/flaskapp/.venv/bin/python /opt/flaskapp/bpd_scraper/run_daily_win.py
Playwright Browser Not Found / Scraper Records 0 Incidents
Symptom in bpd_scraper/logs/scraper_YYYY-MM-DD.log:
Playwright failed: BrowserType.launch: Executable doesn't exist at .../chromium_headless
The scraper silently falls back and writes a 0-record JSONL file — no parse errors, but no data.
Fix:
sudo -u flaskapp /opt/flaskapp/.venv/bin/playwright install chromium
Verify the binary works afterward:
/opt/flaskapp/.cache/ms-playwright/chromium_headless_shell-*/chrome-headless-shell-linux64/chrome-headless-shell --version
The CI/CD pipeline (playwright install chromium step) handles this automatically on future deploys.
Performance Tuning
For 5,000+ Records
Increase PostgreSQL Buffer
sudo vi /var/lib/pgsql/15/data/postgresql.conf
# Increase: shared_buffers = 512MB (for 4GB RAM)
# Then restart: sudo systemctl restart postgresql-15
Reduce Gunicorn Workers if Memory Constrained
# Edit /etc/systemd/system/flaskapp.service
# Change: --workers 1 (instead of 2)
# Then: sudo systemctl daemon-reload && sudo systemctl restart flaskapp
Add Database Indexes
sudo -u postgres psql -d bpd << 'EOF'
CREATE INDEX idx_location ON incidents(location);
CREATE INDEX idx_location_lat ON incidents(location_lat);
CREATE INDEX idx_incident_type ON incidents(incident_type);
VACUUM ANALYZE;
EOF
Backup & Recovery
Backup Database
sudo -u postgres pg_dump -d bpd > /backup/bpd_$(date +%Y-%m-%d).sql
Restore Database
sudo -u postgres psql -d bpd < /backup/bpd_YYYY-MM-DD.sql
Backup Application Code
sudo tar -czf /backup/flaskapp_$(date +%Y-%m-%d).tar.gz /opt/flaskapp/
Next Steps
- Enable HTTPS — Use Let's Encrypt with certbot
- Set up Monitoring — Monitor Flask logs and PostgreSQL performance
- Add Alerting — Email/Slack notifications for cron job failures
- Scale Database — Optimize queries and add proper indexing
- Load Balancing — Run multiple Flask workers if needed
Support
For issues, check:
- Flask logs: /opt/flaskapp/logs/error.log
- Cron logs: /opt/flaskapp/logs/cron.log
- Nginx logs: /var/log/nginx/error.log
- PostgreSQL logs: sudo journalctl -u postgresql-15 -n 50
For detailed service status:
# All services
sudo systemctl status postgresql-15 flaskapp nginx
# Full logs
sudo journalctl -u flaskapp --all -n 100