Docs
Get Started

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.py containing app = 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-Migrate

Include 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:$PORT

The 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:$PORT

Binding 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.

The Kuberns Connect Your Repository screen before a provider is linked, with a Connect and Configure button, service name and region fields, and a Deploy button

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.

The GitHub app installation screen for kuberns-prod, showing its read and write permissions and the choice between all repositories and selected repositories

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

The Connect Your Repository screen filled in: organization, repository, and branch selected, a service name entered, a region chosen, and a Starter plan showing 2 CPU and 512 MB RAM

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.

The Environment Variables dialog with empty key and value fields, an Add New button, and an Upload .env file option

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.

The agent deployment screen showing Environment Setup, System Configuration, and AI-Driven Deployment all complete, with a live log reporting the detected build command and a Proceed to Dashboard button

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.

The Deploy Config screen showing pre-build scripts, per-process deploy commands, post-build scripts, a root directory field, the detected port, and the generated Dockerfile

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 web entry 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 upgrade

Post-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.

The environment overview showing the deployed URL, a running server resource, memory and storage usage, analytics, and build history

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 info

Point 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.