How to deploy a Flask application DigitalOcean

How to deploy a Flask application DigitalOcean

We may earn an affiliate commission through purchases made from our guides and tutorials.

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

  1. Push your Flask app to GitHub/GitLab/Bitbucket with a proper requirements.txt and a Procfile (or specify a run command in the UI).
  2. In DigitalOcean, create an App, connect the repo, pick “Web Service,” select Python, and set the start command to Gunicorn.
  3. 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.

Was this helpful?

Thanks for your feedback!
Alex is the resident editor and oversees all of the guides published. His past work and experience include Colorlib, Stack Diary, Hostvix, and working with a number of editorial publications. He has been wrangling code and publishing his findings about it since the early 2000s.

Leave a comment

Your email address will not be published. Required fields are marked *