How to host a FastAPI, Flask or Django app

To host a FastAPI, Flask or Django app, use a production server instead of the development one: Uvicorn for FastAPI, Gunicorn for Flask and Django. Listen on 0.0.0.0 and read secrets from environment variables.

Updated 6 min read

Learning objectives

After reading this article you will be able to:

  • Pick the right production server for each framework
  • Prepare and start a FastAPI, Flask or Django app outside your computer
  • Fix the most common errors in a newly published Python app
On this page

All three frameworks ship a built-in server that starts with one command, and that is where the most common mistake lives: taking that server to production. It is built for development. To publish, the app needs an application server (Uvicorn or Gunicorn) that listens on the right interface and reads its configuration from the environment.

This guide shows the minimum for each framework, the matching production command and the settings Django requires. The general reasoning for publishing an API is in how to host an API.

RequestHTTPSProxyon the hostHTTPGunicorn or UvicornFlaskDjangoFastAPIWorker 1your appWorker 2your appWorker 3your appDatabase
The proxy forwards the request to Gunicorn or Uvicorn, which hands it to one of the workers running your app.

Which production server does each framework use?#

The difference is the interface between the framework and the server:

Framework Interface Production server Typical command
FastAPI ASGI Uvicorn (comes with fastapi[standard]) fastapi run or uvicorn main:app
Flask WSGI Gunicorn gunicorn 'app:app'
Django WSGI (or ASGI) Gunicorn gunicorn mysite.wsgi

Gunicorn does not run on Windows (only under WSL). Production servers are usually Linux, so this only affects local testing.

How do you host a FastAPI app?#

Install it with the standard extras, which include Uvicorn:

bash
pip install "fastapi[standard]"

main.py:

python
from fastapi import FastAPI

app = FastAPI()


@app.get("/health")
def health():
    return {"status": "ok"}

In production, without --reload (the FastAPI docs warn that it uses more resources and is less stable):

bash
uvicorn main:app --host 0.0.0.0 --port "${PORT:-8000}" --workers 2

main:app means "the app object in the file main.py". Each worker is a separate process, and startup code runs once per worker. fastapi run main.py is the short alternative: it also listens on 0.0.0.0, on port 8000 by default.

How do you host a Flask app?#

bash
pip install flask gunicorn

app.py:

python
import os

from flask import Flask

app = Flask(__name__)
app.config["SECRET_KEY"] = os.environ["SECRET_KEY"]


@app.get("/health")
def health():
    return {"status": "ok"}

Start it with Gunicorn:

bash
gunicorn -w 4 -b "0.0.0.0:${PORT:-8000}" 'app:app'

The app:app argument follows the {module}:{variable} format. With the application factory pattern, use 'app:create_app()'. -w sets the number of processes, and the Flask docs suggest CPU * 2 as a starting point. Without -b, Gunicorn listens on 0.0.0.0 on the port in the PORT variable if it is set; without PORT, it listens only on 127.0.0.1:8000, and the app looks down. Passing -b makes the address explicit either way.

How do you host a Django app?#

Django needs more setup: with DEBUG on, it exposes code and configuration, and in production nobody serves static files for you.

bash
pip install django gunicorn whitenoise

In settings.py, read from the environment whatever differs between development and production:

python
import os
from pathlib import Path

BASE_DIR = Path(__file__).resolve().parent.parent

SECRET_KEY = os.environ["SECRET_KEY"]
DEBUG = os.environ.get("DEBUG") == "1"
ALLOWED_HOSTS = os.environ["ALLOWED_HOSTS"].split(",")
CSRF_TRUSTED_ORIGINS = [
    o for o in os.environ.get("CSRF_TRUSTED_ORIGINS", "").split(",") if o
]

STATIC_URL = "static/"
STATIC_ROOT = BASE_DIR / "staticfiles"
STORAGES = {
    "default": {"BACKEND": "django.core.files.storage.FileSystemStorage"},
    "staticfiles": {
        "BACKEND": "whitenoise.storage.CompressedManifestStaticFilesStorage"
    },
}

MIDDLEWARE = [
    "django.middleware.security.SecurityMiddleware",
    "whitenoise.middleware.WhiteNoiseMiddleware",  # right after SecurityMiddleware
    # ... other middleware
]

With ALLOWED_HOSTS=myapp.example.com and CSRF_TRUSTED_ORIGINS=https://myapp.example.com, the public domain is allowed. CSRF_TRUSTED_ORIGINS includes the scheme (https://) and applies to POST requests coming from another origin.

Before starting, collect the static files and run migrations:

bash
python manage.py collectstatic --noinput
python manage.py migrate
gunicorn mysite.wsgi -b "0.0.0.0:${PORT:-8000}"

WhiteNoise serves the files in STATIC_ROOT straight from the Python process, so you do not need a separate web server for them. Finally, run python manage.py check --deploy with your production settings: it points out what is missing.

If the host's proxy terminates HTTPS and forwards plain HTTP to Django, request.is_secure() returns False. The fix is SECURE_PROXY_SSL_HEADER = ("HTTP_X_FORWARDED_PROTO", "https"), but the Django docs only allow it if the proxy strips any X-Forwarded-Proto header sent by the client and sets it only on requests that arrived over HTTPS. Without that guarantee, keep it at None.

How do you know the deploy worked?#

Test from outside the server, using the public address:

bash
curl -i https://myapp.example.com/health

You expect HTTP/2 200 and the JSON {"status":"ok"}. In Django, add a simple view at /health or open the home page. If the response is different, read the process logs: the Python traceback shows the exact line.

What are the most common errors?#

The connection is refused or times out. The server listens on 127.0.0.1 or on a port different from the exposed one. Pass --host 0.0.0.0 (Uvicorn) or -b 0.0.0.0:PORT (Gunicorn).

Django returns Bad Request (400). The domain is not in ALLOWED_HOSTS. With DEBUG = False and a list that lacks the domain (or is empty), Django answers every request with 400.

A Django form fails with 403 (CSRF verification failed). The site's origin is not in CSRF_TRUSTED_ORIGINS, or Django does not know the original connection was HTTPS. Check the list and the proxy header.

The Django admin CSS does not load. Either collectstatic was skipped or WhiteNoise is missing from MIDDLEWARE. Run collectstatic on every deploy.

KeyError: 'SECRET_KEY'. The environment variable was not set on the server. The failure is on purpose: better not to start than to start without a secret. See what environment variables are.

ModuleNotFoundError. A dependency is missing from requirements.txt. Generate the file with pip freeze > requirements.txt inside the project's virtual environment.

The app restarts by itself or gets slow. Too many workers for too little RAM: each one is a process with its own copy of the app. Lower --workers or raise the memory.

What else matters before going public?#

  • Database: a local SQLite file next to the app is fragile with several workers and when the server's disk is replaced. For real use, prefer PostgreSQL or MySQL; see how to host a database.
  • Migrations: run migrate (Django) or your own migration tool on every deploy, before sending traffic to the new version.
  • CORS: in FastAPI, use CORSMiddleware with exact origins; in Flask and Django, the flask-cors and django-cors-headers packages.
  • Custom domain: point the domain to your app and update ALLOWED_HOSTS at the same time.

How to do this on Square Cloud#

The production commands in this guide become your application's start command on Square Cloud. You upload the project as a ZIP in the dashboard, with the CLI, through GitHub or with the VS Code extension; at start, the platform runs pip install -r requirements.txt and sets the PORT variable. The squarecloud.app file is optional: the dashboard generates it on deploy. In the deploy form's start command field, use your framework's server:

  • FastAPI: uvicorn main:app --host 0.0.0.0 --port $PORT --workers 2
  • Flask: gunicorn -w 2 -b "0.0.0.0:$PORT" 'app:app'
  • Django: python manage.py collectstatic --noinput && python manage.py migrate && gunicorn mysite.wsgi -b "0.0.0.0:$PORT"

Since there is no build step, Django collects its static files and applies migrations right there, on every start. Every plan gives a web application a *.squareweb.app subdomain with automatic TLS, and that host is what goes into Django's variables: ALLOWED_HOSTS=myapp.squareweb.app and CSRF_TRUSTED_ORIGINS=https://myapp.squareweb.app, next to SECRET_KEY. Variables live in the dashboard, already exist during the install and apply on the next restart, with no rebuild.

Each worker takes memory, and a web application uses at least 512 MB of the plan's RAM; Hobby starts at R$12.49 per month with 1 GB. If the workers run out of memory, the app stops until you raise its RAM or lower the worker count. Automatic restart, on by default for dashboard deploys, only covers processes that exit with an error. The disk persists across deploys, but SQLite is still fragile with several workers: PostgreSQL and MySQL hosting starts at Standard (from R$44.99 per month), in the same datacenter as your applications.

See the plans on API hosting and the runtime on Python.

API hosting

APIs in always-on containers, with no cold starts

Learn more

FAQs

Can I use Flask's development server or Django's runserver in production?

No. They are for development, not for real load or security. In production, use Gunicorn (Flask and Django) or Uvicorn (FastAPI).

How many workers should I use?

Start small and adjust from real usage. The Flask docs suggest starting at CPU * 2 with Gunicorn, but each worker is a process with its own memory, so your RAM limit matters too.

Why does Django return 400 (Bad Request) after deploy?

With DEBUG off, Django only accepts the hosts listed in ALLOWED_HOSTS. Add the host that appears in the URL to the list.

Do I need Nginx in front of Gunicorn?

On your own server (a VPS), the Flask docs recommend a reverse proxy in front of Gunicorn. On platforms that already provide HTTPS and routing, that layer is already in place.

Next articleHow to host a Python app or scriptTo host a Python app or script, pin the version, isolate dependencies in a venv, read secrets from the environment and handle SIGTERM. Step-by-step guide.