Deployment Guide
Single canonical guide for running iDrv5-MyFR8 locally for development and deploying it to production.
Single canonical guide for running iDrv5-MyFR8 locally for development and deploying it to production. Replaces the four earlier guides (DEPLOYMENT_GUIDE_PRODUCTION.md, DEVELOPMENT_DEPLOYMENT_GUIDE.md, docs/DEPLOYMENT_GUIDE.md, exports/PRODUCTION_DEPLOYMENT_GUIDE.md), which are now thin pointers to this file.
Quick Reference
Section titled “Quick Reference”| Concern | Value |
|---|---|
| Production app path | /var/www/idrv5 (per deploy.sh) |
| Production user | webapp |
| WSGI app | main_standalone:app (per gunicorn.conf.py) |
| Gunicorn bind | 0.0.0.0:${PORT:-5000} |
| Worker class (default) | gevent |
| Reverse proxy | Apache2 (primary) — Nginx config also committed (alternative) |
| Database (dev) | Supabase PostgreSQL — see .env.dev |
| Database (prod) | Amazon RDS PostgreSQL 15+ — see .env.production |
| Service unit | idrv5.service (systemd) |
There are committed Apache and Nginx templates under apache2/ and nginx/. The canonical production stack is Apache2 + Gunicorn + RDS; the Nginx config exists for environments that already standardize on Nginx.
Part 1 — Local Development
Section titled “Part 1 — Local Development”Requirements
Section titled “Requirements”- Python 3.11 (3.8+ works, 3.11 recommended)
- Git
- Either a local PostgreSQL 15+ install, Docker, or just network access to the Supabase dev DB credentials in
.env.dev
Quick Start (uses the shared Supabase dev DB)
Section titled “Quick Start (uses the shared Supabase dev DB)”# 1. Clone and entergit clone <repo-url> idrv5 && cd idrv5
# 2. Virtual environmentpython3.11 -m venv venvsource venv/bin/activate # Windows: venv\Scripts\activate
# 3. Install dependenciespip install --upgrade pippip install -r REQUIREMENTS_DEVELOPMENT.txt
# 4. Environmentcp .env.dev .env
# 5. Run the operations portalpython main.py --port 5002
# Or run all portals via the app factorypython -c "from app_factory import create_app; app = create_app(); app.run(debug=True, port=5000)"The dev DATABASE_URL lives in .env.dev and points at Supabase. No local PostgreSQL needed unless you want to run isolated.
Optional: Local PostgreSQL via Docker
Section titled “Optional: Local PostgreSQL via Docker”docker run -d \ --name idrv5-postgres \ -e POSTGRES_DB=idrv5_dev \ -e POSTGRES_USER=dev_user \ -e POSTGRES_PASSWORD=dev_password \ -p 5432:5432 \ postgres:15
# Apply schemapsql postgresql://dev_user:dev_password@localhost:5432/idrv5_dev \ -f exports/01_core_schema.sqlpsql postgresql://dev_user:dev_password@localhost:5432/idrv5_dev \ -f exports/02_transport_schema.sqlpsql postgresql://dev_user:dev_password@localhost:5432/idrv5_dev \ -f exports/10_sample_data.sql
# Override DATABASE_URL in .env to point at this instanceDev Login Shortcuts
Section titled “Dev Login Shortcuts”/super-admin— auto-login as super admin (Baba/3695)/direct-operations— auto-login as operations user (test/test123)/direct-compliance— auto-login as compliance officer
Smoke Tests
Section titled “Smoke Tests”curl http://localhost:5002/api/rate-cards/curl http://localhost:5002/healthCommon Issues
Section titled “Common Issues”Port already in use:
lsof -i :5002 # macOS/Linuxnetstat -an | findstr :5002 # WindowsModule import errors: make sure your venv is active (which python should resolve inside venv/).
Database connection errors: confirm DATABASE_URL in .env is reachable (psql "$DATABASE_URL" -c 'select 1;').
Part 2 — Production Deployment
Section titled “Part 2 — Production Deployment”Target stack: Amazon Linux 2023 (or any RHEL-family distro) + Apache2 + Gunicorn + Amazon RDS PostgreSQL.
2.1 Server Preparation
Section titled “2.1 Server Preparation”sudo dnf update -ysudo dnf install -y wget curl git unzip \ python3.11 python3.11-pip python3.11-devel \ postgresql15-devel \ gcc gcc-c++ make \ libffi-devel openssl-devel zlib-devel \ libjpeg-turbo-devel freetype-devel lcms2-devel openjpeg2-devel \ libtiff-devel libwebp-devel \ tesseract poppler-utils wkhtmltopdf \ httpd mod_sslsudo systemctl enable httpdOpen the firewall:
sudo firewall-cmd --permanent --add-service=httpsudo firewall-cmd --permanent --add-service=httpssudo firewall-cmd --reload2.2 Application User and Directories
Section titled “2.2 Application User and Directories”sudo useradd -r -s /bin/bash webappsudo usermod -a -G apache webappsudo mkdir -p /var/www/idrv5sudo chown webapp:webapp /var/www/idrv5
sudo mkdir -p /var/log/idrv5sudo chown webapp:apache /var/log/idrv5sudo chmod 755 /var/log/idrv52.3 Upload Code
Section titled “2.3 Upload Code”Use the project’s deploy.sh (which expects PROD_USER=webapp and PROD_DIR=/var/www/idrv5), or rsync directly:
rsync -av \ --exclude='venv/' --exclude='__pycache__/' --exclude='*.pyc' \ --exclude='.git/' --exclude='instance/' --exclude='.env.dev' \ ./ webapp@<server>:/var/www/idrv5/2.4 Database (Amazon RDS)
Section titled “2.4 Database (Amazon RDS)”aws rds create-db-instance \ --db-instance-identifier idrv5-production \ --db-instance-class db.t3.micro \ --engine postgres \ --engine-version 15.4 \ --master-username idrv5_admin \ --master-user-password '<SECURE_PASSWORD>' \ --allocated-storage 20 \ --storage-type gp2 \ --db-name idrv5_production \ --vpc-security-group-ids sg-xxxxxxxxx \ --backup-retention-period 7 \ --multi-az \ --publicly-accessibleApply schema once the instance is reachable:
export DATABASE_URL='postgresql://idrv5_admin:<PASSWORD>@<RDS_ENDPOINT>:5432/idrv5_production'psql "$DATABASE_URL" -f exports/01_core_schema.sqlpsql "$DATABASE_URL" -f exports/02_transport_schema.sqlpsql "$DATABASE_URL" -f exports/03_asset_management_schema.sqlpsql "$DATABASE_URL" -f exports/11_reference_data.sql # optionalpsql "$DATABASE_URL" -f exports/indexes_and_constraints.sql # if present2.5 Production Environment File
Section titled “2.5 Production Environment File”cd /var/www/idrv5cp production_env_template.txt .env.productionsudo chmod 600 .env.productionsudo chown webapp:webapp .env.productionEdit .env.production with at minimum:
FLASK_ENV=productionFLASK_DEBUG=FalseFLASK_SECRET_KEY=$(openssl rand -hex 32)
DATABASE_URL=postgresql://idrv5_admin:<PASSWORD>@<RDS_ENDPOINT>:5432/idrv5_productionPGHOST=<RDS_ENDPOINT>PGPORT=5432PGUSER=idrv5_adminPGPASSWORD=<PASSWORD>PGDATABASE=idrv5_production
SERVER_NAME=your-domain.comPREFERRED_URL_SCHEME=httpsPORT=5000
# Optional — Gunicorn knobs (gunicorn.conf.py reads these)GUNICORN_WORKERS=GUNICORN_WORKER_CLASS=geventGUNICORN_TIMEOUT=30LOG_LEVEL=info2.6 Virtual Environment + Dependencies
Section titled “2.6 Virtual Environment + Dependencies”sudo -u webapp -icd /var/www/idrv5python3.11 -m venv venvsource venv/bin/activatepip install --upgrade pippip install -r REQUIREMENTS_PRODUCTION.txtexit2.7 Gunicorn
Section titled “2.7 Gunicorn”The repo ships gunicorn.conf.py. It binds 0.0.0.0:${PORT:-5000}, uses gevent workers, and serves main_standalone:app. All runtime knobs (GUNICORN_WORKERS, GUNICORN_WORKER_CLASS, GUNICORN_TIMEOUT, etc.) come from environment variables — set them in .env.production. Do not duplicate the config inline in systemd or in this guide.
Smoke-test it directly before moving on:
sudo -u webapp -icd /var/www/idrv5source venv/bin/activateset -a && source .env.production && set +agunicorn --config gunicorn.conf.py main_standalone:app# expect: Listening at: http://0.0.0.0:50002.8 Apache2 Reverse Proxy (Primary)
Section titled “2.8 Apache2 Reverse Proxy (Primary)”The repo ships apache2/sites-available/idrv5-myfr8.conf as a starting template (note: that template’s DocumentRoot is /opt/idrv5-myfr8/static — adjust to /var/www/idrv5/static for this deployment).
Minimal site config at /etc/httpd/conf.d/idrv5.conf:
<VirtualHost *:80> ServerName your-domain.com ServerAlias www.your-domain.com RewriteEngine On RewriteCond %{HTTPS} off RewriteRule ^(.*)$ https://%{HTTP_HOST}%{REQUEST_URI} [R=301,L]</VirtualHost>
<VirtualHost *:443> ServerName your-domain.com ServerAlias www.your-domain.com
SSLEngine on SSLCertificateFile /etc/letsencrypt/live/your-domain.com/cert.pem SSLCertificateKeyFile /etc/letsencrypt/live/your-domain.com/privkey.pem SSLCertificateChainFile /etc/letsencrypt/live/your-domain.com/chain.pem
Header always set Strict-Transport-Security "max-age=31536000; includeSubDomains" Header always set X-Frame-Options "SAMEORIGIN" Header always set X-Content-Type-Options "nosniff" Header always set Referrer-Policy "strict-origin-when-cross-origin"
Alias /static /var/www/idrv5/static <Directory /var/www/idrv5/static> Require all granted ExpiresActive On ExpiresDefault "access plus 1 month" Header append Cache-Control "public" </Directory>
ProxyPreserveHost On ProxyPass /static ! ProxyPass / http://127.0.0.1:5000/ ProxyPassReverse / http://127.0.0.1:5000/
LimitRequestBody 52428800 # 50 MB
ErrorLog /var/log/httpd/idrv5_error.log CustomLog /var/log/httpd/idrv5_access.log combined</VirtualHost>Enable and test:
sudo dnf install -y mod_sslsudo httpd -tsudo systemctl restart httpd2.9 SSL via Let’s Encrypt
Section titled “2.9 SSL via Let’s Encrypt”sudo dnf install -y certbot python3-certbot-apachesudo certbot --apache -d your-domain.com -d www.your-domain.comsudo certbot renew --dry-run2.10 systemd Unit
Section titled “2.10 systemd Unit”/etc/systemd/system/idrv5.service:
[Unit]Description=iDrv5-MyFR8 Logistics Management PlatformAfter=network.target
[Service]Type=notifyUser=webappGroup=webappWorkingDirectory=/var/www/idrv5Environment=PATH=/var/www/idrv5/venv/binEnvironmentFile=/var/www/idrv5/.env.productionExecStart=/var/www/idrv5/venv/bin/gunicorn --config gunicorn.conf.py main_standalone:appExecReload=/bin/kill -s HUP $MAINPIDRestart=alwaysRestartSec=10KillMode=mixedTimeoutStopSec=5PrivateTmp=true
[Install]WantedBy=multi-user.targetEnable:
sudo systemctl daemon-reloadsudo systemctl enable idrv5sudo systemctl start idrv5sudo systemctl status idrv52.11 Permissions
Section titled “2.11 Permissions”sudo chown -R webapp:webapp /var/www/idrv5sudo find /var/www/idrv5 -type d -exec chmod 755 {} \;sudo find /var/www/idrv5 -type f -exec chmod 644 {} \;sudo chmod 600 /var/www/idrv5/.env.production
sudo mkdir -p /var/www/idrv5/static/uploadssudo chown webapp:apache /var/www/idrv5/static/uploadssudo chmod 775 /var/www/idrv5/static/uploads2.12 Verification
Section titled “2.12 Verification”# Service healthsudo systemctl status idrv5 httpd
# Local proxycurl -I http://127.0.0.1:5000/curl -I http://localhost/curl -I https://your-domain.com/
# DB connectivity from the appsudo -u webapp -i bash -c ' cd /var/www/idrv5 && source venv/bin/activate && \ python3 -c "import osfrom sqlalchemy import create_engine, texte = create_engine(os.environ[\"DATABASE_URL\"])print(e.connect().execute(text(\"select version()\")).scalar()) "'Alternative: Nginx instead of Apache
Section titled “Alternative: Nginx instead of Apache”The repo also ships nginx/conf.d/idrv5.conf. If your environment standardizes on Nginx:
- Skip section 2.8 (Apache install/config).
sudo dnf install -y nginx.- Adapt
nginx/conf.d/idrv5.conf:- Replace
/usr/share/nginx/html/staticwith/var/www/idrv5/static. - Replace
server_name _;with your real hostname. - Point
proxy_passathttp://127.0.0.1:5000.
- Replace
- SSL via certbot’s nginx plugin:
sudo dnf install -y certbot python3-certbot-nginx && sudo certbot --nginx -d your-domain.com. sudo systemctl enable --now nginx.
Everything else (Gunicorn, systemd unit, RDS, env, permissions) is identical.
sudo journalctl -u idrv5 -f # systemd / appsudo tail -f /var/log/httpd/idrv5_error.log # Apachesudo tail -f /var/log/httpd/idrv5_access.log# Gunicorn logs go to stdout/stderr (per gunicorn.conf.py) — captured by journalctlBackups
Section titled “Backups”sudo tee /usr/local/bin/idrv5-backup.sh > /dev/null << 'EOF'#!/bin/bashset -euo pipefailDATE=$(date +%Y%m%d_%H%M%S)DIR=/var/backups/idrv5mkdir -p "$DIR"source /var/www/idrv5/.env.productionpg_dump "$DATABASE_URL" > "$DIR/db_$DATE.sql"tar -czf "$DIR/app_$DATE.tar.gz" -C /var/www/idrv5 .find "$DIR" -name 'db_*.sql' -mtime +7 -deletefind "$DIR" -name 'app_*.tar.gz' -mtime +7 -deleteEOFsudo chmod +x /usr/local/bin/idrv5-backup.shsudo crontab -e # add: 0 2 * * * /usr/local/bin/idrv5-backup.shMaintenance
Section titled “Maintenance”sudo systemctl restart idrv5 # restart appsudo systemctl restart httpd # restart web serversudo certbot renew # renew SSLsudo httpd -t # validate Apache configsudo httpd -M | grep -E 'proxy|rewrite|ssl'Troubleshooting
Section titled “Troubleshooting”Gunicorn won’t start. Run it in the foreground to see the actual error:
sudo -u webapp -icd /var/www/idrv5 && source venv/bin/activateset -a && source .env.production && set +agunicorn --config gunicorn.conf.py main_standalone:appApache 502/proxy errors. Check that Gunicorn is bound to 127.0.0.1:5000 (or whatever PORT you set), and that SELinux isn’t blocking proxying:
sudo setsebool -P httpd_can_network_connect 1sudo setsebool -P httpd_can_network_connect_db 1DB connection failures. Verify reachability and creds:
psql "$DATABASE_URL" -c 'select 1;'Static files 404. Confirm the Alias /static /var/www/idrv5/static line and that /var/www/idrv5/static exists with apache group read permission.
Permission errors after deploy. Re-run section 2.11.
Known Inconsistencies in the Repo
Section titled “Known Inconsistencies in the Repo”The committed reverse-proxy templates were authored against an older /opt/idrv5-myfr8 layout while deploy.sh and current systemd target /var/www/idrv5. When using the templates in apache2/ or nginx/ directly, search-and-replace /opt/idrv5-myfr8 → /var/www/idrv5 before applying. The canonical paths in this guide reflect what deploy.sh actually does.