Deploy Flask
This guide takes a Flask application that runs on your machine and gets it running on Kuberns, with a production server, a managed PostgreSQL database, and migrations.
Most of the work is in your repository rather than in the dashboard. The AI agent works out the framework, the dependency file, the port, and the start command on its own. What it cannot do is turn flask run into something you should serve real traffic with. That part is below.
Before you start
- A Flask application in a GitHub repository that runs locally.
- The module and variable holding your app. This guide assumes
app.pycontainingapp = Flask(__name__). - A Kuberns account. The free trial is enough to follow along.
Prepare your Flask project
1. Add a production server
Flask's built-in server prints a warning every time it starts, and it means it: it is single-threaded and not built for real traffic. Add Gunicorn to requirements.txt:
gunicorn
python-dotenv
psycopg2-binary
Flask-SQLAlchemy
Flask-MigrateInclude only what your application actually uses. gunicorn is the one that is not optional.
2. Stop app.run() from being the entry point
A typical development file ends like this:
if __name__ == "__main__":
app.run(debug=True)Leave it if you like — it is how you run the app locally, and Gunicorn never executes it, because Gunicorn imports your module and takes the app object directly rather than running the file.
What matters is that the deployed process is Gunicorn. If your start command ends up as python app.py, you are serving production traffic from the development server.
3. Read configuration from the environment
Never hard-code a secret key, and never leave debug mode on in production. Debug mode exposes an interactive console to anyone who triggers an error:
import os
app.config["SECRET_KEY"] = os.environ["SECRET_KEY"]
app.config["DEBUG"] = os.environ.get("FLASK_DEBUG", "False") == "True"4. Read the database from DATABASE_URL
SQLite writes to local disk, and local disk does not survive a deploy. Point SQLAlchemy at the environment instead:
database_url = os.environ["DATABASE_URL"]
# SQLAlchemy 1.4 and later reject the "postgres://" scheme.
if database_url.startswith("postgres://"):
database_url = database_url.replace("postgres://", "postgresql://", 1)
app.config["SQLALCHEMY_DATABASE_URI"] = database_url
app.config["SQLALCHEMY_ENGINE_OPTIONS"] = {"pool_pre_ping": True}Those three lines about the scheme are worth keeping. Plenty of tools still hand out postgres:// URLs, and SQLAlchemy raises Can't load plugin: sqlalchemy.dialects:postgres when it gets one — an error that tells you nothing about what is actually wrong.
pool_pre_ping checks a connection before using it, which avoids stale-connection errors after an idle period.
5. Declare your process
Add a Procfile at the repository root:
web: gunicorn app:app --bind 0.0.0.0:$PORTThe format is module:variable. For app.py with app = Flask(__name__) that is app:app. If you use an application factory, point Gunicorn at the call instead:
web: gunicorn "myapp:create_app()" --bind 0.0.0.0:$PORTBinding to $PORT matters. A process listening on a hard-coded port, or on localhost instead of 0.0.0.0, deploys successfully and is then unreachable from outside its container.
Commit and push before continuing.
Deploy
Connect your repository
Start a new service and connect your Git provider.

If this is your first deployment, you are sent to GitHub to install the Kuberns app and choose which repositories it can see. Granting access to a single repository is enough, and is the safer default.

Back on Kuberns, pick the organization, repository, and branch, name the service, and choose a region and plan.

Add your environment variables
Use Add Env Vars to enter them as key and value pairs. If you already have a local .env, Upload .env file imports it in one step rather than retyping each line.

For this application you need at least:
SECRET_KEY = <a long random string>
FLASK_DEBUG = False
DATABASE_URL = postgres://<user>:<password>@<hostname>/<database>Generate a secret key with:
python -c "import secrets; print(secrets.token_urlsafe(50))"Leave DATABASE_URL out for now if you have not created the database yet. You can add it straight after, and saving a variable triggers a redeploy automatically.
Never commit these values. .env belongs in .gitignore.
Let the agent deploy
The agent analyzes the repository, reports what it detected, and runs the build.

Watch the detected build command in that log. If it reads python app.py rather than your Gunicorn command, the Procfile was not picked up — check that it is at the repository root and committed.
Check the detected configuration
Deploy Config holds everything the agent decided, and everything is editable.

Four things are worth checking here:
- Pre-build scripts should install your dependencies, normally
pip install --no-cache-dir -r requirements.txt. - Deploy config holds one command per process. The
webentry is your Gunicorn command. - Port configuration must match what your process binds to.
- Root directory, if your Flask app is not at the repository root.
Add a database and run migrations
From Resources, add a PostgreSQL datastore. Its overview page then shows the database name, username, password, and hostname — build DATABASE_URL from those and save it as an environment variable.
If you use Flask-Migrate, add the upgrade as a post-build command in deployment configuration:
flask db upgradePost-build commands run after every build, which is what you want here: flask db upgrade does nothing when there is nothing to apply.
Confirm it is running
The environment overview shows the live URL, the resources attached to it, and recent builds.

Open the URL. If the application loads, the deploy worked.
After the first deploy
Add your own domain
Add the hostname under Custom Domains and point DNS at the target shown. See Add a domain. SSL is provisioned once DNS verifies.
Add a background worker
If your application runs Celery or another queue consumer, add a Redis queue or cache resource and a background worker resource, then extend the Procfile:
web: gunicorn app:app --bind 0.0.0.0:$PORT
worker: celery -A app.celery worker -l infoPoint CELERY_BROKER_URL at the Redis resource. A worker with no resource on the environment simply does not run, and nothing reports an error when it does not.
Serve static files efficiently
Flask serves files from static/ on its own, so a small application needs no extra work. If you serve a lot of static assets, add WhiteNoise so they are compressed and cached rather than handled by Flask on every request.
Common problems
Can't load plugin: sqlalchemy.dialects:postgres. The connection string starts with postgres://. Apply the scheme replacement from step 4.
The deploy succeeds but the site is unreachable. Gunicorn is bound to a hard-coded port or to localhost. It must bind 0.0.0.0:$PORT, and the port must match the Deploy Config field.
Failed to find application object. The module:variable in your Procfile does not match your code. For app.py with app = Flask(__name__) it is app:app. For a factory, use "myapp:create_app()" with the quotes.
A development-server warning in the logs. The deployed process is python app.py, not Gunicorn. Check the Procfile and the deploy command.
Data disappears after a deploy. The application is still on SQLite. Confirm DATABASE_URL is set and being read.
relation does not exist. Migrations have not run. Add flask db upgrade as a post-build command.