I needed a good guide on deploying the PFUG stack as I now do that often (it helps me with rapid prototyping and MVP).
This guide provides comprehensive instructions for deploying a Python/Flask application in production using uv for dependency management and Gunicorn as the WSGI server.
Prerequisites #
- Ubuntu/Debian-based server (adaptable to other Linux distributions)
- Python 3.8 or higher
- Sudo access
- Domain name (optional but recommended)
1. System Preparation #
Update system packages #
sudo apt update && sudo apt upgrade -y
Install required system dependencies #
sudo apt install -y python3-dev python3-pip build-essential nginx supervisor curl
Install uv (Python package manager) #
curl -LsSf https://astral.sh/uv/install.sh | sh
source $HOME/.cargo/env
2. Application Setup #
Create application user and directory #
# Create dedicated user for the application
sudo useradd -m -s /bin/bash flaskapp
sudo usermod -aG sudo flaskapp
# Create application directory
sudo mkdir -p /opt/flask-app
sudo chown flaskapp:flaskapp /opt/flask-app
Switch to application user and setup directory #
sudo -u flaskapp -i
cd /opt/flask-app
Clone your application (replace with your repository) #
# Example - replace with your actual repository
git clone https://github.com/yourusername/your-flask-app.git .
# OR upload your application files to this directory
3. Python Environment Setup with uv #
Initialize uv project and install dependencies #
# Initialize uv in the project directory
uv init --no-readme
# Install Flask and Gunicorn
uv add flask gunicorn
# Install your application dependencies
# If you have a requirements.txt:
uv pip install -r requirements.txt
# Or install specific packages:
# uv add requests psycopg2-binary redis etc.
Create pyproject.toml for dependency management #
[project]
name = "flask-app"
version = "1.0.0"
description = "Production Flask Application"
requires-python = ">=3.8"
dependencies = [
"flask>=2.3.0",
"gunicorn>=21.2.0",
# Add your other dependencies here
]
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"
4. Application Configuration #
Create Gunicorn configuration file #
Create /opt/flask-app/gunicorn_config.py
:
import multiprocessing
import os
# Server socket
bind = "127.0.0.1:8000"
backlog = 2048
# Worker processes
workers = multiprocessing.cpu_count() * 2 + 1
worker_class = "sync"
worker_connections = 1000
timeout = 30
keepalive = 2
# Restart workers after this many requests, to help prevent memory leaks
max_requests = 1000
max_requests_jitter = 100
# Process naming
proc_name = 'flask-app'
# Logging
accesslog = '/var/log/flask-app/access.log'
errorlog = '/var/log/flask-app/error.log'
loglevel = 'info'
# Process identification
pidfile = '/opt/flask-app/gunicorn.pid'
# User and group
user = 'flaskapp'
group = 'flaskapp'
# Preload application for better performance
preload_app = True
# Environment variables
raw_env = [
'FLASK_ENV=production',
'PYTHONPATH=/opt/flask-app',
]
Create environment configuration #
Create /opt/flask-app/.env
:
# Flask Configuration
FLASK_APP=app.py
FLASK_ENV=production
SECRET_KEY=your-super-secret-key-change-this
# Database Configuration (if using database)
DATABASE_URL=postgresql://user:password@localhost/dbname
# Other environment variables
# REDIS_URL=redis://localhost:6379/0
# MAIL_SERVER=smtp.gmail.com
Create log directories #
sudo mkdir -p /var/log/flask-app
sudo chown flaskapp:flaskapp /var/log/flask-app
5. Supporting Scripts #
Deployment Script #
Create /opt/flask-app/deploy.sh
:
#!/bin/bash
# Flask Application Deployment Script
set -e
APP_DIR="/opt/flask-app"
APP_USER="flaskapp"
echo "Starting deployment..."
# Navigate to app directory
cd $APP_DIR
# Pull latest changes (if using git)
echo "Pulling latest changes..."
sudo -u $APP_USER git pull origin main
# Update dependencies
echo "Updating dependencies..."
sudo -u $APP_USER uv sync
# Run database migrations (if applicable)
# echo "Running database migrations..."
# sudo -u $APP_USER uv run flask db upgrade
# Collect static files (if applicable)
# echo "Collecting static files..."
# sudo -u $APP_USER uv run python manage.py collectstatic --noinput
# Restart services
echo "Restarting services..."
sudo supervisorctl restart flask-app
sudo systemctl reload nginx
echo "Deployment completed successfully!"
Health Check Script #
Create /opt/flask-app/health_check.sh
:
#!/bin/bash
# Health check script for Flask application
APP_URL="http://127.0.0.1:8000"
HEALTH_ENDPOINT="/health" # Implement this endpoint in your Flask app
# Check if Gunicorn process is running
if ! pgrep -f "gunicorn.*flask-app" > /dev/null; then
echo "ERROR: Gunicorn process not found"
exit 1
fi
# Check HTTP response
HTTP_CODE=$(curl -s -o /dev/null -w "%{http_code}" ${APP_URL}${HEALTH_ENDPOINT} || echo "000")
if [ "$HTTP_CODE" = "200" ]; then
echo "OK: Application is healthy"
exit 0
else
echo "ERROR: HTTP $HTTP_CODE - Application health check failed"
exit 1
fi
Backup Script #
Create /opt/flask-app/backup.sh
:
#!/bin/bash
# Backup script for Flask application
BACKUP_DIR="/opt/backups"
APP_DIR="/opt/flask-app"
DATE=$(date +%Y%m%d_%H%M%S)
# Create backup directory
mkdir -p $BACKUP_DIR
# Backup application files
echo "Backing up application files..."
tar -czf "$BACKUP_DIR/flask-app-$DATE.tar.gz" -C /opt flask-app
# Backup database (PostgreSQL example)
# echo "Backing up database..."
# sudo -u postgres pg_dump your_database > "$BACKUP_DIR/database-$DATE.sql"
# Clean old backups (keep last 7 days)
find $BACKUP_DIR -name "flask-app-*.tar.gz" -mtime +7 -delete
# find $BACKUP_DIR -name "database-*.sql" -mtime +7 -delete
echo "Backup completed: flask-app-$DATE.tar.gz"
Make scripts executable:
chmod +x /opt/flask-app/deploy.sh
chmod +x /opt/flask-app/health_check.sh
chmod +x /opt/flask-app/backup.sh
6. Process Management with Supervisor #
Install and configure Supervisor #
sudo apt install supervisor -y
Create /etc/supervisor/conf.d/flask-app.conf
:
[program:flask-app]
command=/home/flaskapp/.local/bin/uv run gunicorn -c gunicorn_config.py app:app
directory=/opt/flask-app
user=flaskapp
autostart=true
autorestart=true
redirect_stderr=true
stdout_logfile=/var/log/flask-app/supervisor.log
environment=PATH="/home/flaskapp/.local/bin:/usr/local/bin:/usr/bin:/bin"
Note: Do not daemonise gunicorn with –daemon. That will be handled by Supervisor.
Start and enable Supervisor #
sudo systemctl enable supervisor
sudo systemctl start supervisor
sudo supervisorctl reread
sudo supervisorctl update
sudo supervisorctl start flask-app
7. Nginx Reverse Proxy Setup #
Create /etc/nginx/sites-available/flask-app
:
server {
listen 80;
server_name your-domain.com www.your-domain.com; # Replace with your domain
# Security headers
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-XSS-Protection "1; mode=block" always;
add_header X-Content-Type-Options "nosniff" always;
add_header Referrer-Policy "no-referrer-when-downgrade" always;
# Rate limiting
limit_req_zone $binary_remote_addr zone=api:10m rate=10r/s;
limit_req zone=api burst=20 nodelay;
# Static files (if you have them)
location /static {
alias /opt/flask-app/static;
expires 1y;
add_header Cache-Control "public, immutable";
}
# Main application
location / {
proxy_pass http://127.0.0.1:8000;
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;
# Timeouts
proxy_connect_timeout 60s;
proxy_send_timeout 60s;
proxy_read_timeout 60s;
# Buffer settings
proxy_buffering on;
proxy_buffer_size 128k;
proxy_buffers 4 256k;
proxy_max_temp_file_size 1024m;
}
# Health check endpoint
location /health {
proxy_pass http://127.0.0.1:8000;
access_log off;
}
}
Enable the site #
sudo ln -s /etc/nginx/sites-available/flask-app /etc/nginx/sites-enabled/
sudo nginx -t
sudo systemctl restart nginx
8. SSL Certificate with Certbot (Optional but Recommended) #
# Install Certbot
sudo apt install certbot python3-certbot-nginx -y
# Obtain SSL certificate
sudo certbot --nginx -d your-domain.com -d www.your-domain.com
# Auto-renewal is set up automatically
sudo certbot renew --dry-run
9. Monitoring and Maintenance #
Set up log rotation #
Create /etc/logrotate.d/flask-app
:
/var/log/flask-app/*.log {
daily
missingok
rotate 52
compress
delaycompress
notifempty
create 644 flaskapp flaskapp
postrotate
supervisorctl restart flask-app
endscript
}
Cron jobs for maintenance #
Add to crontab (sudo crontab -e
):
# Daily backup at 2 AM
0 2 * * * /opt/flask-app/backup.sh
# Health check every 5 minutes
*/5 * * * * /opt/flask-app/health_check.sh
# Log rotation
0 0 * * * /usr/sbin/logrotate /etc/logrotate.d/flask-app
10. Deployment Commands #
Initial deployment #
# Start services
sudo supervisorctl start flask-app
sudo systemctl start nginx
# Check status
sudo supervisorctl status
sudo systemctl status nginx
Updates and maintenance #
# Deploy updates
./deploy.sh
# Check application status
sudo supervisorctl status flask-app
# View logs
sudo supervisorctl tail -f flask-app
tail -f /var/log/flask-app/error.log
# Restart application
sudo supervisorctl restart flask-app
Troubleshooting #
Common issues and solutions #
-
Permission denied errors: Ensure correct ownership of files
sudo chown -R flaskapp:flaskapp /opt/flask-app
-
Port already in use: Check for conflicting processes
sudo lsof -i :8000
-
502 Bad Gateway: Check Gunicorn is running and accessible
curl http://127.0.0.1:8000
-
View application logs:
tail -f /var/log/flask-app/error.log sudo supervisorctl tail -f flask-app
Security Considerations #
-
Firewall configuration:
sudo ufw allow ssh sudo ufw allow 'Nginx Full' sudo ufw enable
-
Keep system updated:
sudo apt update && sudo apt upgrade -y
-
Regular security audits:
uv pip audit
-
Environment variables: Never commit secrets to version control
-
Database security: Use strong passwords and limit access
This production setup provides a robust, scalable foundation for your Flask application with proper process management, reverse proxy, SSL termination, and monitoring capabilities.