Here’s a practical, battle-tested way to get a Flask app running on DigitalOcean—two paths, same destination:
- the “managed” way (DigitalOcean App Platform), great if you prefer clicks over configs;
- the “own the box” way (a Droplet with Ubuntu, Gunicorn, Nginx, and Let’s Encrypt).
I’ll walk you through both. You’ll need a DigitalOcean account to follow along—sign in first so you’re not juggling that mid-setup.
Flask, if you’re new to it, is a lightweight Python web framework. It stays out of your way: you bring the tools you want and bolt on only what you need. In production you don’t run Flask’s built-in server; you put it behind a real WSGI server (like Gunicorn) and a front web server (like Nginx). That’s the industry-standard pattern and it’s what we’ll use.
Path A — Deploy Flask on DigitalOcean App Platform (managed)
If you want to ship fast and skip server maintenance, App Platform is clean and straightforward.
High-level flow
- Push your Flask app to GitHub/GitLab/Bitbucket with a proper
requirements.txt
and a Procfile (or specify a run command in the UI). - In DigitalOcean, create an App, connect the repo, pick “Web Service,” select Python, and set the start command to Gunicorn.
- App Platform will build, deploy, and give you a URL; you can add your custom domain and HTTPS in a couple of clicks.
DigitalOcean’s sample shows the moving pieces (and where to put the run command). Use Gunicorn as the entrypoint; it’s the correct production server.
Minimal files you should have in your repo
requirements.txt
flask==3.0.3
gunicorn==23.0.0
Procfile
(if you prefer this over entering the command in the UI)
web: gunicorn app:app --workers 2 --bind 0.0.0.0:$PORT
If your app file isn’t app.py
or your Flask instance isn’t named app
, adjust app:app
accordingly (e.g., myapp:create_app()
→ gunicorn "myapp:create_app()" ...
). See Flask’s deployment docs for the patterns.
Environment variables
In App Platform → Settings → Environment Variables, set secrets like FLASK_ENV=production
, database URLs, API keys, etc. (No code change required; App Platform injects them at runtime.)
Path B — Deploy Flask on a DigitalOcean Droplet (DIY, Nginx + Gunicorn)
If you want full control (and the responsibility that comes with it), this is the classic, stable stack.
Create a Droplet and connect
Pick an Ubuntu LTS image (22.04 or 24.04 both work; I’ll assume 24.04), assign an SSH key, and create the Droplet. Then SSH in:
ssh root@YOUR_DROPLET_IP
DigitalOcean’s tutorials for Flask + Nginx + Gunicorn lay out this pattern; we’ll modernize it for current Ubuntu.
Basic system prep
Create a non-root user, add sudo, and harden a bit. If DO already created one for you, adapt accordingly.
adduser deploy
usermod -aG sudo deploy
rsync -a ~/.ssh /home/deploy
chown -R deploy:deploy /home/deploy/.ssh
Optional but smart: enable the uncomplicated firewall (UFW) for only SSH/HTTP/HTTPS.
apt update && apt -y upgrade
apt -y install ufw
ufw allow OpenSSH
ufw allow http
ufw allow https
ufw --force enable
Install Python toolchain and Nginx
apt -y install python3-pip python3-venv python3-dev build-essential nginx
Nginx will be our reverse proxy in front of Gunicorn, as recommended by both Flask and Gunicorn docs.
Switch to the deploy user:
su - deploy
Create your app directory and virtualenv
mkdir -p ~/apps/flaskapp
cd ~/apps/flaskapp
python3 -m venv .venv
source .venv/bin/activate
Add a minimal Flask app (or clone your repo)
If you’re testing the pipeline, start small:
app.py
from flask import Flask
app = Flask(__name__)
@app.get("/")
def hello():
return "It works. 🚀"
if __name__ == "__main__":
app.run()
requirements.txt
flask==3.0.3
gunicorn==23.0.0
Install deps:
pip install -r requirements.txt
Try Gunicorn locally (you should NOT expose this directly in prod long-term, this is just a smoke test):
.venv/bin/gunicorn -b 127.0.0.1:8000 app:app
Hit it from the Droplet:
curl -i http://127.0.0.1:8000/
Create a systemd unit for Gunicorn
We’ll make Gunicorn start on boot and restart on failure.
Exit your virtualenv if needed and note its absolute path (/home/deploy/apps/flaskapp/.venv/...
). Then as root:
sudo nano /etc/systemd/system/flaskapp.service
Paste:
[Unit]
Description=Gunicorn for Flask (flaskapp)
After=network.target
[Service]
User=deploy
Group=www-data
WorkingDirectory=/home/deploy/apps/flaskapp
Environment="PATH=/home/deploy/apps/flaskapp/.venv/bin"
ExecStart=/home/deploy/apps/flaskapp/.venv/bin/gunicorn --workers 2 --bind unix:/run/flaskapp.sock app:app
Restart=always
RestartSec=3
[Install]
WantedBy=multi-user.target
Start and enable:
sudo systemctl daemon-reload
sudo systemctl start flaskapp
sudo systemctl enable flaskapp
sudo systemctl status flaskapp --no-pager
We’re binding Gunicorn to a Unix socket for Nginx to proxy—an efficient, well-trod setup in the Gunicorn docs.
Configure Nginx as a reverse proxy
Create an Nginx server block for your domain.
sudo nano /etc/nginx/sites-available/flaskapp
Paste:
server {
listen 80;
server_name yourdomain.com www.yourdomain.com;
access_log /var/log/nginx/flaskapp_access.log;
error_log /var/log/nginx/flaskapp_error.log;
location / {
include proxy_params;
proxy_pass http://unix:/run/flaskapp.sock;
}
}
Enable it and test:
sudo ln -s /etc/nginx/sites-available/flaskapp /etc/nginx/sites-enabled/
sudo nginx -t
sudo systemctl restart nginx
This is the canonical Nginx → Gunicorn pattern.
Point DNS to your Droplet
Create an A record for yourdomain.com
to the Droplet’s IPv4 address (and AAAA for IPv6 if you want). Wait for propagation (usually quick on DO). Then request HTTPS.
Add HTTPS with Let’s Encrypt (Certbot)
Install the Certbot Nginx plugin, issue the certificate, and set up automatic renewal.
sudo apt -y install certbot python3-certbot-nginx
sudo certbot --nginx -d yourdomain.com -d www.yourdomain.com
Follow the prompts to redirect HTTP → HTTPS. Certbot handles renewals via system timers; you can test with:
sudo certbot renew --dry-run
Environment variables and secrets
Put runtime config in a file systemd can read, then include it in your unit. For example:
sudo nano /etc/flaskapp.env
Add lines like:
FLASK_ENV=production
SECRET_KEY=replace_me
Reference it in your service:
EnvironmentFile=/etc/flaskapp.env
Then in your app:
import os
SECRET_KEY = os.environ.get("SECRET_KEY")
Reload:
sudo systemctl daemon-reload
sudo systemctl restart flaskapp
(You can also use DO’s managed Secrets if you’re on App Platform.)
Zero-downtime deploys (simple version)
Pull the latest code, install any new requirements, reload Gunicorn:
cd /home/deploy/apps/flaskapp
git pull origin main
source .venv/bin/activate
pip install -r requirements.txt
sudo systemctl reload flaskapp || sudo systemctl restart flaskapp
reload
sends a SIGHUP so Gunicorn gracefully swaps workers; restart
is a blunt fallback. For heavier traffic, consider socket activation or a blue-green layout, but this gets you far.
Worker tuning and logs
Start with workers = 2 * CPU + 1
as a rule of thumb. If your Droplet has 2 vCPUs, try 5. You can set this in a Gunicorn config file and reference it from the systemd unit. Gunicorn and Nginx both log to /var/log/...
; use them when something acts up. Gunicorn’s own docs cover the recommended deployments and why Nginx belongs in front.
Example gunicorn.conf.py
:
bind = "unix:/run/flaskapp.sock"
workers = 5
timeout = 30
graceful_timeout = 30
Update your unit’s ExecStart
to:
ExecStart=/home/deploy/apps/flaskapp/.venv/bin/gunicorn -c /home/deploy/apps/flaskapp/gunicorn.conf.py app:app
Common gotchas (and quick fixes)
- 502 Bad Gateway right after you change something? Check that the systemd service is running and the socket file path matches Nginx.
sudo systemctl status flaskapp --no-pager sudo journalctl -u flaskapp -n 100 --no-pager
- Permissions on the socket can break proxying. We set
Group=www-data
so Nginx (www-data) can read the socket; keep that consistent. - Firewall blocking ports? Ensure 80/443 are allowed (we did with UFW).
- SSL renewals failing? Run a dry-run with Certbot and check Nginx server_name/blocks.
When to choose which path
- App Platform: you value velocity, automatic HTTPS, autoscaling, and fewer moving parts. You don’t want to babysit Nginx or system updates. DigitalOcean’s sample and community tutorials cover the exact flow.
- Droplet (Nginx + Gunicorn): you want tight control, custom OS-level tweaks, and predictable costs. DO’s Flask + Nginx guides are tried-and-true, and the pattern maps cleanly from Django to Flask.
Those are the nuts and bolts. Keep Flask behind Gunicorn, keep Gunicorn behind Nginx, and keep Nginx behind HTTPS. That’s the stack that’s aged well—and it’ll keep serving you reliably.