Netlify serves static files and runs short-lived functions. It does not run a long-lived Flask process, so there is no build command that turns a normal Flask app into a working Netlify deployment.
That is the answer behind almost every forum thread on this, and it is worth understanding rather than working around, because two of the three available workarounds are genuinely good and one of them is a trap. Which one applies to you depends entirely on whether your Flask app actually needs to be running when a request arrives.
Table of contents
- Why it does not work the way you expect
- Option one: freeze it to static, if the content is static
- Option two: serverless functions, for small dynamic pieces
- Option three: run it somewhere that runs processes
- Which one you actually need
- Common errors and what they mean
- How this fits the rest of the stack
- FAQ
Why it does not work the way you expect
A Flask app is a program. You start it, it binds a port, and it sits there waiting. When a request arrives, the process that was already running handles it, possibly touching a database, a session store, or a file on disk that it wrote earlier.
Netlify does not have a place for that program to sit. It is built around a build step that produces files and a CDN that serves them. Nothing is running between requests. There is no port to bind, no process to keep alive, and no local disk that survives.
This is why the logs in the support threads look so confusing. People push a repo with gunicorn in the build command, watch the build output show a server starting, and then get a 404. The server did start, briefly, on a build machine, and then the build machine was thrown away. Netlify took whatever files were in the publish directory and served those. Usually there were none.
So the question is not how to make Netlify run Flask. It is which of the following three things your app actually needs.
Option one: freeze it to static, if the content is static
A large share of small Flask apps are not dynamic at all. They render templates from data that only changes when the author changes it. A personal site, a portfolio, documentation, a blog. Flask is being used as a template engine with a nice routing syntax, not as an application server.
If that describes your app, you can render every route to an HTML file at build time and deploy the result. Frozen-Flask is the standard tool for this, and it is a small addition:
from flask_frozen import Freezer
from app import app
freezer = Freezer(app)
if __name__ == '__main__':
freezer.freeze()
Then the Netlify build config becomes ordinary:
[build]
command = "pip install -r requirements.txt && python freeze.py"
publish = "build"
[build.environment]
PYTHON_VERSION = "3.11"
Routes with URL parameters need a generator so Frozen-Flask knows which values to render:
@freezer.register_generator
def post_detail():
for slug in all_post_slugs():
yield {'slug': slug}
This works well and costs nothing. It stops working the moment you add a login, a form that writes somewhere, or a page whose content depends on who is asking. At that point you are not freezing a site, you are fighting one.
Option two: serverless functions, for small dynamic pieces
If the site is mostly static but needs a handful of dynamic endpoints, you can put those endpoints in serverless functions and leave the rest static. A contact form handler, a webhook receiver, a small API that reads from a database.
The thing to be clear about is that this is not deploying your Flask app. It is rewriting those endpoints as individual functions with a different entry point, a different request object, and a cold start on the first call. Some people wrap a whole WSGI app in a function adapter to avoid the rewrite, and it does technically run, but you inherit every constraint at once.
- Execution time limits, which are short. Anything slow is a timeout rather than a slow response.
- Cold starts. The first request after an idle period pays for the runtime and your imports to load.
- No shared in-process state. Caches, background threads and module-level connections do not persist the way they do in a running server.
- Database connections are the awkward one. A pool that assumes a long-lived process behaves badly when the process is created and destroyed per request.
The rule of thumb: serverless functions are good when the dynamic surface is small and bounded. They get expensive in complexity the moment the dynamic part is the actual product.
Option three: run it somewhere that runs processes
If your Flask app has a database, sessions, background jobs, file uploads, or anything that assumes a process is alive between requests, it needs a runtime. Not a workaround, a runtime.
This is not a failure. It is the normal architecture for a web application, and it has been for thirty years. The deployment is short:
web: gunicorn app:app --bind 0.0.0.0:$PORT --workers 3
Point a platform at the repository, give it that start command, set the environment variables, and attach a database. You get a build log, a live route, runtime logs, and a rollback if the deploy goes wrong.
There is a perfectly good hybrid here too, and it is the one most teams land on: the marketing site and docs stay static on a CDN, and the application runs as a service behind an API subdomain. You get the CDN speed where content is static and a real process where state lives, without pretending either one is the other.
On RunxBuild that is a Python service deployed from GitHub with gunicorn as the start command, a managed Postgres or MySQL beside it on the same private network, and both on the same plan ladder. The Dev plan is four dollars a month and the Basic plan is six, which is the usual starting point for something small that has to stay running.
Which one you actually need
Three questions settle it, in order:
- Does any page change based on who is looking at it, or on data written after the build? If no, freeze it to static and stop reading.
- Is the dynamic part a small number of endpoints with no shared state and no slow work? If yes, serverless functions are a reasonable fit.
- Otherwise, deploy it as a running service.
The trap is answering question one optimistically. People freeze a site, then add a comment form, then add a login, then add an admin page, and end up with a static site glued to four functions and a hosted database, which is more moving parts than the thing they were avoiding.
If you already know a database is coming, skip to three. An AI-generated form is cute until it asks where the submissions go, and the answer is never the CDN.
Common errors and what they mean
- Page not found after a successful build. Netlify built your project and found nothing in the publish directory. The Flask server started on the build machine and was discarded. Set publish to the directory your freeze step writes.
- The build log shows gunicorn listening, then the deploy ends. Expected. The build step finished, so the machine was torn down. A start command in the build command does not keep anything alive.
- Functions time out on database queries. Cold start plus connection setup plus the query exceeded the execution limit. Either the query needs work or the workload needs a running process.
- Sessions do not persist between requests. Nothing is running between requests, so in-memory sessions are gone. This needs external session storage, or a real server.
- Static assets 404 while pages render. Frozen-Flask did not copy the static directory. Check FREEZER_STATIC_IGNORE and that the assets are referenced through url_for.
How this fits the rest of the stack
The reason this question comes up so often is that static hosting looks free and a running process looks like a commitment, so people try very hard to make the free option cover a case it was never built for. It is worth pricing the honest version before deciding: a small Python service, a managed database, and the bandwidth to serve it are three separate numbers, and the RunxBuild hosting calculator shows them as three separate line items rather than one guess. Often the real figure is lower than the workaround was going to cost in time.
Useful related references:
- Django on Netlify: Why It Does Not Work, and What To Do Instead
- Branch Deploy on Netlify: How It Works and When to Use One
- Netlify Templates: What Starter Templates Give You and What They Do Not
- Python services on RunxBuild
FAQ
Can you deploy a Flask app on Netlify?
Not as a running server. Netlify serves static files and runs short-lived functions, so a long-lived Flask process has nowhere to live. You can freeze a static Flask site to HTML at build time, or rewrite individual endpoints as serverless functions, but neither is deploying the app as written.
What is Frozen-Flask and when should I use it?
It is an extension that walks your routes and writes each one to an HTML file, turning a Flask app into a static site. Use it when the content only changes when you rebuild. Do not use it if pages depend on who is logged in or on data written at runtime.
Why does my Netlify build succeed but the site shows 404?
The build ran your start command, the server came up on the build machine, and then that machine was discarded. Netlify published whatever was in the publish directory, which was empty. You need a build step that writes files, and the publish directory pointed at them.
Can I use serverless functions instead of a Flask server?
For a small, bounded set of endpoints, yes. You will be rewriting them against a different entry point and living with execution time limits, cold starts, and no shared in-process state. Database connection pooling is the usual pain point, since pools assume a process that stays alive.
Where should a Flask app with a database be deployed?
On something that runs processes: a platform that takes your repository, runs gunicorn as the start command, gives you environment variables and runtime logs, and lets you attach a managed database. A common split is to keep the marketing site static on a CDN and run the application as a separate service.