Python deployments go wrong in a small number of predictable ways: the wrong server type, the wrong host binding, and an in-process database that outgrows the memory limit.
Choose the server to match your framework
Flask and Django are WSGI. FastAPI, Starlette and Socket.IO are ASGI. They are not interchangeable.
| Framework | Server | Command |
|---|---|---|
| Flask | Gunicorn | gunicorn --bind 0.0.0.0:$PORT app:app |
| Django | Gunicorn | gunicorn --bind 0.0.0.0:$PORT project.wsgi |
| FastAPI | Uvicorn | uvicorn main:app --host 0.0.0.0 --port $PORT |
| Socket.IO | Uvicorn | uvicorn main:app --host 0.0.0.0 --port $PORT |
Set this as the startup command on the Startup tab. Note --host 0.0.0.0 on Uvicorn: its default is 127.0.0.1, and the symptom is identical to every other runtime — a healthy-looking log and no external connectivity.
Why not the development server
Never run flask run or uvicorn without a production server in front of it. The Flask development server is explicitly not built for concurrent traffic: it handles requests one at a time, so a slow endpoint blocks every other request. The result is a service that works fine in testing and appears to hang under real load.
Container vs process count
Concurrency in Python costs memory linearly. Four Gunicorn workers on a 512 MB plan is four interpreter baselines plus your dependencies, which is how a service that fits on Basic dies on Starter.
As a starting point on a 512 MB container: one worker. On 1 GB: two. Use --workers 1 plus an async server instead of many sync workers where the framework supports it — async handles concurrency in one process, which is usually a much better fit for a small container.
Dependencies
Use a pinned requirements file, and install before starting:
pip install --no-cache-dir -r requirements.txt
gunicorn --bind 0.0.0.0:$PORT app:app
Pin exact versions. Unpinned dependencies mean a restart can install a newer release than the one you tested against, and Python releases do not hold a major version steady the way Node does.
--no-cache-dir is worth the flag on a small container: pip caches every wheel it downloads, and that cache consumes storage you need for your own files.
Do not host the database in the same container
Running MySQL or PostgreSQL inside your app's container is the most common cause of a service that restarts unpredictably. The database reserves memory well beyond its working set — the buffer pool alone can be sized to a large fraction of available RAM — so the two processes compete for the same limit and the kernel kills whichever is larger.
Provision a managed database from the Databases tab instead and connect over the network. It isolates the memory, survives an app restart, and keeps your data when you redeploy. Working with databases covers the connection setup.
If you must self-host a database, do it in a separate service and size that service for the database, not for your app.