Deploy a Flask App 🌶️
Push your Flask project to GitHub or GitLab and Faable builds and runs it — no Dockerfile, no YAML, no gunicorn command to write. The builder detects Flask from your repo, installs your dependencies, and serves it behind automatic SSL at https://<app>.faable.link, hosted in Europe.
What Faable detects
Detection is file-based (full rules). For Flask the builder needs:
| It looks for | Which gives it |
|---|---|
requirements.txt, pyproject.toml or Pipfile | Your dependencies — installed into a virtualenv |
A module defining app = Flask(...) | The WSGI entrypoint → the start command below |
It searches for that module in this order, preferring the file that actually defines app:
main.py → app.py → asgi.py → wsgi.py → application.py → server.py → app/main.py → app/app.py → src/main.py
Then it runs, with no configuration from you:
gunicorn <module>:app --bind 0.0.0.0:$PORTgunicorn is installed automatically if it isn’t in your dependencies — though pinning it is good practice.
The one thing to get right: $PORT
Faable assigns your app a port at runtime and passes it as the PORT environment variable. The auto-detected command already binds 0.0.0.0:$PORT, so a standard project needs no change.
Note that app.run() is Flask’s development server — Faable never calls it, and you shouldn’t either in production. Keep it guarded for local use:
import os
if __name__ == "__main__":
# Local development only. On Faable, gunicorn serves the app.
app.run(host="0.0.0.0", port=int(os.environ.get("PORT", 5000)))A minimal deployable app
# app.py
import os
from flask import Flask
app = Flask(__name__)
@app.get("/")
def index():
return {"status": "ok"}
@app.get("/healthz")
def healthz():
return {"ok": True}# requirements.txt
Flask>=3.0
gunicornThat is a complete, deployable repo — two files.
A JSON API, end to end
Most Flask projects deployed on Faable are REST APIs — a service another app or a bot calls over HTTP. Here is a complete one with the pieces that are easy to get wrong in production: JSON errors instead of HTML, CORS for a browser client, config from the environment, and a health check.
# requirements.txt
Flask>=3.0
flask-cors>=5.0
gunicorn# app.py
import os
from flask import Flask, jsonify, request
from flask_cors import CORS
from werkzeug.exceptions import HTTPException
app = Flask(__name__)
# Only the origins you actually serve — never "*" on an API that takes a key.
CORS(app, resources={r"/v1/*": {"origins": os.environ.get("ALLOWED_ORIGINS", "").split(",")}})
API_KEY = os.environ["API_KEY"]
@app.before_request
def require_key():
if request.path.startswith("/v1/") and request.headers.get("X-API-Key") != API_KEY:
return jsonify(error="unauthorized"), 401
@app.get("/v1/items")
def list_items():
# Page from the query string, clamped — an unbounded limit is how APIs fall over.
limit = min(int(request.args.get("limit", 20)), 100)
return jsonify(items=fetch_items(limit), limit=limit)
@app.post("/v1/items")
def create_item():
body = request.get_json(silent=True) or {}
if not body.get("name"):
return jsonify(error="name is required"), 422
return jsonify(item=save_item(body)), 201
@app.get("/healthz")
def healthz():
return {"ok": True}
@app.errorhandler(HTTPException)
def json_errors(error):
"""Flask returns HTML error pages by default — clients want JSON."""
return jsonify(error=error.name, status=error.code), error.code
@app.errorhandler(Exception)
def unhandled(error):
app.logger.exception("unhandled error")
return jsonify(error="internal server error", status=500), 500Two details that matter once it’s live:
- Log to stdout.
app.loggergoes to the deployment logs in the dashboard and tofaable deploy logs. Don’t write log files — the filesystem is ephemeral. - Don’t hold state in memory. A module-level cache or counter is lost on every deploy, and on every wake from sleep. Use a database — see Databases.
Configuration and secrets
Read configuration from the environment, never from committed files:
import os
app.config["SECRET_KEY"] = os.environ["SECRET_KEY"]
app.config["SQLALCHEMY_DATABASE_URI"] = os.environ.get("DATABASE_URL")Set them with the CLI or the dashboard:
faable deploy secrets set SECRET_KEY=… DATABASE_URL=postgres://…Faable also injects PORT, FAABLE_APP_ID, FAABLE_RELEASE and FAABLE_GIT_COMMIT. See Environment & Releases.
Static files and templates
Flask serves its own static/ folder and Jinja templates from the app process, so both work unchanged — no extra configuration. For a bundled frontend, build it before you push and commit the output to static/: the Python build has no npm. Keep the frontend’s package.json in a subfolder such as frontend/, because a package.json at the repository root makes Faable build the project as a Node app.
Deploy
From the dashboard — the normal path:
- Create a Project and an App in the Faable Dashboard .
- Click Link repository and pick your Flask repo.
- Push to your release branch. Faable builds and takes it live.
Or from your laptop:
npm i -g @faable/faable
faable login
faable deployYour app is live at https://<app>.faable.link with automatic SSL and the WAF already inspecting traffic.
The application factory pattern
If you use create_app() instead of a module-level app, the builder can’t detect an entrypoint — it looks for an assigned app object. Either expose one:
# wsgi.py
from myapp import create_app
app = create_app()…or declare the start command yourself in faable.json:
{
"startCommand": "gunicorn 'myapp:create_app()' --bind 0.0.0.0:$PORT"
}Workers
Leave the worker count to Faable. The generated command runs a single gunicorn worker, and that is the right setting: don’t add --workers to your start command.
Every worker is a separate process that loads your whole app and its libraries, so each one multiplies the memory the app needs — two workers of an app that imports pandas or an AI SDK can be more than a small instance holds, and the app gets killed for running out of memory. If one worker isn’t enough, move to a larger instance. On the Free plan an app runs at most one worker — see Workers and memory.
Choosing the Python version
A new app gets the newest supported minor — Python 3.14 today — and keeps the minor of its first successful deploy from then on. Supported minors are 3.10, 3.11, 3.12, 3.13 and 3.14 — pin one with a runtime.txt:
python-3.12Or with .python-version, or requires-python in pyproject.toml. First match wins, in that order.
Troubleshooting
- Build fails with “no start command” — no module in the search list defines
app = Flask(...). Use the factory pattern above or setstartCommand. - The app runs locally but 502s on Faable — you’re relying on
app.run(). Production is served by gunicorn; make sure the app object is importable at module level. Working outside of application context— code touchingcurrent_appor the database runs at import time. Move it inside a request handler or an app-context block.ModuleNotFoundErrorat boot — the dependency isn’t inrequirements.txt.- Requests time out — the start command binds a hardcoded port instead of
$PORT.
FAQ
Do I need a Dockerfile to deploy Flask on Faable?
No. Faable finds the module that defines app = Flask(...) and generates the gunicorn command for you. A Dockerfile is the escape hatch for stacks the buildpacks don’t detect natively.
How do I deploy a Flask app that uses an application factory?
Expose a module-level app (for example in wsgi.py with app = create_app()), or set startCommand in faable.json to gunicorn 'myapp:create_app()' --bind 0.0.0.0:$PORT.
Does Faable use app.run() to start my Flask app?
No — app.run() is the development server. Faable starts your app with gunicorn bound to 0.0.0.0:$PORT. Keep app.run() behind an if __name__ == "__main__" guard for local development.
How many gunicorn workers should I run?
One — leave it to the start command Faable generates rather than setting --workers. Each worker loads its own copy of your app, so more workers mean proportionally more memory. For more capacity, pick a larger instance from the catalog.
Which Python versions does Faable support for Flask?
3.10, 3.11, 3.12, 3.13 and 3.14. A new app gets the newest (3.14 today) and keeps the minor of its first deploy; pin your choice with runtime.txt (python-3.13), .python-version, or requires-python in pyproject.toml.
Related
- What the Builder Expects — full detection and start-command rules
- Deploy a WhatsApp Bot · Deploy a Telegram Bot — webhook bots on this stack
- Deploy Next.js · Deploy Django · Deploy FastAPI · Deploy Node.js Express
- Environment & Releases · Custom domains · WAF
- Add authentication to your app — Faable Auth is included in the same subscription
Last updated on