I built my first Flask app in maybe a weekend. Got it working locally, felt good about it. Then I went to actually put it online and realized I had no idea what I was doing.
flask run is for development. What runs it in production? What handles HTTPS? What happens when the server restarts — does the app start back up? I had to answer all of those questions one by one.
Turns out the answers aren't complicated, but nobody explains them together in one place. This guide is that: Gunicorn as the WSGI server, Nginx as the reverse proxy, systemd to keep it running, and Let's Encrypt for HTTPS. The complete picture.
I run this on Tencent Cloud Lighthouse with Ubuntu 22.04. Lighthouse suits Python app deployment well for a few reasons: instances spin up in under 2 minutes with a clean Ubuntu image (no conflicting pre-installed packages), OrcaTerm lets you manage the server from a browser without local SSH setup, and the snapshot feature means you can take a full server backup before a deployment in seconds. If your app grows, you can upgrade the instance spec (more RAM/CPU) from the control panel — no re-provisioning required.
Key Takeaways
- Never run Flask's development server in production — use Gunicorn
- Use
(2 × CPU cores) + 1as the starting point for Gunicorn worker count- systemd keeps Gunicorn running after server reboots and crash restarts
- Nginx handles HTTPS and static files; Gunicorn handles dynamic Python requests
- Set environment variables in the systemd service file, not in the app code
Internet → Nginx (port 80/443) → Gunicorn (Unix socket) → Flask app
| Component | Role |
|---|---|
| Nginx | Terminates SSL, serves static files, proxies dynamic requests |
| Gunicorn | WSGI server — runs multiple Flask worker processes |
| Flask | Your application code |
| systemd | Keeps Gunicorn running, restarts on crash |
Flask's built-in development server (flask run) is not suitable for production — it's single-threaded and not designed for real traffic. Gunicorn handles concurrent requests properly.
| Requirement | Notes |
|---|---|
| Cloud server | Tencent Cloud Lighthouse Ubuntu 22.04 |
| Non-root user with sudo | See initial server setup guide |
| Domain name (optional) | For HTTPS — not required for IP-only testing |
| A Flask application | We'll create a minimal example if you don't have one |
ssh ubuntu@YOUR_SERVER_IP
Update packages:
sudo apt update && sudo apt upgrade -y
sudo apt install -y python3 python3-pip python3-venv git nginx
Open required firewall ports:
sudo ufw allow 'Nginx Full'
sudo ufw allow ssh
sudo ufw enable
Ubuntu 22.04 ships with Python 3.10. Always use a virtual environment to isolate your app's dependencies:
# Create app directory
mkdir -p ~/apps/myflaskapp
cd ~/apps/myflaskapp
# Create a virtual environment
python3 -m venv venv
# Activate it
source venv/bin/activate
# Verify Python version
python --version
Install Flask and Gunicorn:
pip install flask gunicorn
git clone https://github.com/your-username/your-flask-app.git .
source venv/bin/activate
pip install -r requirements.txt
If you're following along without an existing app:
cat > ~/apps/myflaskapp/app.py << 'EOF'
from flask import Flask
app = Flask(__name__)
@app.route('/')
def index():
return '<h1>Flask is running on Tencent Cloud Lighthouse.</h1>'
@app.route('/health')
def health():
return {'status': 'ok'}
if __name__ == '__main__':
app.run()
EOF
Test it locally first:
source venv/bin/activate
flask --app app run --host=0.0.0.0 --port=5000
# Visit http://YOUR_SERVER_IP:5000 to verify
# Press Ctrl+C to stop
Gunicorn is your WSGI server. Test it manually first to make sure it can find and run your app:
cd ~/apps/myflaskapp
source venv/bin/activate
# Test Gunicorn with your app
# Format: gunicorn module:app_variable
gunicorn --bind 0.0.0.0:5000 app:app
Visit http://YOUR_SERVER_IP:5000 — you should see your Flask app.
Press Ctrl+C to stop.
For production, use more workers and a Unix socket instead of a TCP port:
gunicorn \
--workers 3 \
--bind unix:/run/myflaskapp.sock \
--access-logfile /var/log/myflaskapp/access.log \
--error-logfile /var/log/myflaskapp/error.log \
app:app
Worker count rule of thumb: (2 × CPU cores) + 1. For a 2-core server, use 5 workers.
sudo mkdir -p /var/log/myflaskapp
sudo chown ubuntu:ubuntu /var/log/myflaskapp
sudo nano /etc/systemd/system/myflaskapp.service
[Unit]
Description=Gunicorn instance for myflaskapp
After=network.target
[Service]
User=ubuntu
Group=www-data
WorkingDirectory=/home/ubuntu/apps/myflaskapp
Environment="PATH=/home/ubuntu/apps/myflaskapp/venv/bin"
ExecStart=/home/ubuntu/apps/myflaskapp/venv/bin/gunicorn \
--workers 3 \
--bind unix:/run/myflaskapp.sock \
--access-logfile /var/log/myflaskapp/access.log \
--error-logfile /var/log/myflaskapp/error.log \
app:app
# Restart on failure
Restart=on-failure
RestartSec=5s
[Install]
WantedBy=multi-user.target
sudo systemctl daemon-reload
sudo systemctl start myflaskapp
sudo systemctl enable myflaskapp
# Verify it's running
sudo systemctl status myflaskapp
# Check the socket was created
ls /run/myflaskapp.sock
sudo nano /etc/nginx/sites-available/myflaskapp
server {
listen 80;
server_name yourdomain.com www.yourdomain.com;
# Serve static files directly (skip Gunicorn)
location /static/ {
root /home/ubuntu/apps/myflaskapp;
expires 30d;
}
# Proxy everything else to Gunicorn
location / {
include proxy_params;
proxy_pass http://unix:/run/myflaskapp.sock;
}
access_log /var/log/nginx/myflaskapp_access.log;
error_log /var/log/nginx/myflaskapp_error.log;
}
sudo ln -s /etc/nginx/sites-available/myflaskapp /etc/nginx/sites-enabled/
sudo nginx -t
sudo systemctl reload nginx
Visit http://yourdomain.com — your Flask app should be live.
sudo apt install -y certbot python3-certbot-nginx
sudo certbot --nginx -d yourdomain.com -d www.yourdomain.com
Choose option 2 (redirect HTTP to HTTPS) when prompted.
Test auto-renewal:
sudo certbot renew --dry-run
Never hardcode API keys, database passwords, or secret keys in your code. Use environment variables:
sudo nano /etc/systemd/system/myflaskapp.service
Add environment variables in the [Service] section:
[Service]
...
Environment="SECRET_KEY=your-secret-key-here"
Environment="DATABASE_URL=postgresql://user:password@localhost/mydb"
Environment="FLASK_ENV=production"
Or use an environment file (more secure):
sudo nano /etc/myflaskapp.env
SECRET_KEY=your-secret-key-here
DATABASE_URL=postgresql://user:password@localhost/mydb
FLASK_ENV=production
sudo chmod 600 /etc/myflaskapp.env
In the service file:
EnvironmentFile=/etc/myflaskapp.env
Reload and restart:
sudo systemctl daemon-reload
sudo systemctl restart myflaskapp
The most common error when first running Gunicorn:
Failed to find application: 'app:app'
This means either:
app.py → module app)app = Flask(__name__) → variable app)WorkingDirectory in systemd must point to your app directoryCheck the full path:
which gunicorn # If venv is active, shows venv path
/home/ubuntu/apps/myflaskapp/venv/bin/gunicorn --version
# After any code change, restart Gunicorn
sudo systemctl restart myflaskapp
# View application logs
sudo journalctl -u myflaskapp -f
tail -f /var/log/myflaskapp/error.log
# View Nginx logs
tail -f /var/log/nginx/myflaskapp_error.log
# Check service status
sudo systemctl status myflaskapp
| Issue | Likely Cause | Fix |
|---|---|---|
| Connection refused | Service not running or wrong port | Check systemctl status SERVICE and verify firewall rules |
| Permission denied | Wrong file ownership or permissions | Check file ownership with ls -la and use chown/chmod to fix |
| 502 Bad Gateway | Backend service not running | Restart the backend service; check logs with journalctl -u SERVICE |
| SSL certificate error | Certificate expired or domain mismatch | Run sudo certbot renew and verify domain DNS points to server IP |
| Service not starting | Config error or missing dependency | Check logs with journalctl -u SERVICE -n 50 for specific error |
| Out of disk space | Logs or data accumulation | Run df -h to identify usage; clean logs or attach CBS storage |
| High memory usage | Too many processes or memory leak | Check with htop; consider upgrading instance plan if consistently high |
| Firewall blocking traffic | Port not open in UFW or Lighthouse console | Open port in Lighthouse console firewall AND sudo ufw allow PORT |
What is Gunicorn and why is it needed for Flask?
Flask's built-in flask run server is single-threaded and not designed for production traffic. Gunicorn is a production WSGI server that runs multiple Flask worker processes, handles concurrent requests, and integrates with Nginx.
How many Gunicorn workers should I configure?
A common formula: (2 × CPU cores) + 1. For a 2-vCPU server, use 5 workers. More workers = more concurrent requests handled, but also more RAM usage.
How do I check if my Flask app is running on the server?
Check the systemd service status: sudo systemctl status gunicorn. View logs with sudo journalctl -u gunicorn -n 50. Test the socket directly with curl --unix-socket /run/gunicorn.sock http://localhost/.
How do I deploy updates to my Flask app?
Push your code to the server (via git or scp), restart Gunicorn: sudo systemctl restart gunicorn. With GitHub Actions, this can be automated on every push.
Deploy your Flask app today:
👉 Tencent Cloud Lighthouse — Python-ready Ubuntu VPS
👉 View current pricing and promotions
👉 Explore all active deals and offers