TutorialApp Hosting

How to Deploy a Python App (Flask, FastAPI or Django)

Getting a Python web app online usually means wrestling with servers, virtual environments and process managers. On Chajio Cloud you push your code and the platform does the rest, provided your repository says two things: what to install and how to start. This guide shows exactly what that looks like for Flask, FastAPI and Django.

CChajio Cloud team September 25, 2026 10 min read
On this page

Before you start

  • A Chajio Cloud App Hosting plan. The Free plan is enough to follow along. Once you order, you get an email with your sign-in details, and the first time you sign in you choose your own password.
  • Your code on GitHub, in a repository you own or can access. A public repository you don't own works too.
  • A Python web app that runs on your computer, with its dependencies listed in requirements.txt (a pyproject.toml or Pipfile works too).

Step 1: Get your app ready

Chajio Cloud builds your app from three things in your repository:

  • requirements.txt lists your packages, including the server that runs your app in production: gunicorn for Flask and Django, uvicorn for FastAPI. The built-in development servers (flask run, manage.py runserver) aren’t meant for real traffic.
  • .python-version picks your Python version. Without it you get a default that may not match what you develop on. The file contains just the version, for example 3.12.
  • A Procfile says how to start your app. It’s a file named exactly Procfile, no extension, in the root of your repository, with one line starting web:. Django projects can skip it, as you’ll see below.

Whatever starts your app must listen on the port in the PORT environment variable, on all network interfaces (0.0.0.0). An app that listens only on 127.0.0.1 works on your laptop but can’t be reached once deployed.

Flask

requirements.txt
flask==3.1.0
gunicorn==23.0.0
app.py
from flask import Flask, jsonify

app = Flask(__name__)

@app.get("/")
def home():
    return jsonify(message="Hello from Chajio Cloud")

@app.get("/health")
def health():
    return {"ok": True}
Procfile
web: gunicorn app:app --bind 0.0.0.0:$PORT --workers 2

app:app means “the variable app in app.py”. If your file is main.py, use main:app. If you use an app factory, "app:create_app()" works too.

FastAPI

requirements.txt
fastapi==0.115.6
uvicorn[standard]==0.34.0
main.py
from fastapi import FastAPI

app = FastAPI()

@app.get("/")
def home():
    return {"message": "Hello from Chajio Cloud"}

@app.get("/health")
def health():
    return {"ok": True}
Procfile
web: uvicorn main:app --host 0.0.0.0 --port $PORT --proxy-headers --forwarded-allow-ips="*"

--proxy-headers lets FastAPI see that visitors arrived over HTTPS, so the URLs it builds, such as redirects, use https://. Without a Procfile, a FastAPI app is started with python main.py, which only works if that file starts the server itself.

Django

Django projects are recognised automatically, so no Procfile is needed. Your app is started with your migrations first, then gunicorn:

python manage.py migrate && gunicorn yourproject.wsgi

Add gunicorn to requirements.txt, and whitenoise to serve your static files. That default doesn’t collect your static files, so replace it with a Procfile that does:

Procfile
web: python manage.py collectstatic --noinput && python manage.py migrate && gunicorn yourproject.wsgi --workers 2

Gunicorn picks up PORT on its own, so no --bind is needed. Then make your settings read their production values from environment variables:

yourproject/settings.py
import os
import dj_database_url

SECRET_KEY = os.environ["DJANGO_SECRET_KEY"]
DEBUG = os.environ.get("DJANGO_DEBUG") == "1"

# Your free address and your domain, comma-separated.
ALLOWED_HOSTS = os.environ.get("DJANGO_ALLOWED_HOSTS", "").split(",")
CSRF_TRUSTED_ORIGINS = [f"https://{host}" for host in ALLOWED_HOSTS if host]

# HTTPS is handled before requests reach your app; trust the header that says so.
SECURE_PROXY_SSL_HEADER = ("HTTP_X_FORWARDED_PROTO", "https")

DATABASES = {"default": dj_database_url.config(conn_max_age=600)}

STATIC_ROOT = BASE_DIR / "staticfiles"
STORAGES = {
    "default": {"BACKEND": "django.core.files.storage.FileSystemStorage"},
    "staticfiles": {"BACKEND": "whitenoise.storage.CompressedManifestStaticFilesStorage"},
}

Add "whitenoise.middleware.WhiteNoiseMiddleware" to MIDDLEWARE, directly after SecurityMiddleware. You don’t need SECURE_SSL_REDIRECT: visitors who use http:// are already redirected to HTTPS for you.

Django needs a health check that skips ALLOWED_HOSTS

The platform checks your app is healthy by requesting a page from inside the network. That request isn’t addressed to your domain, so a Django app with a proper ALLOWED_HOSTS rejects it with “Bad Request (400)” and never counts as healthy. Don’t loosen ALLOWED_HOSTS to fix it. Answer the health check before Django looks at the host instead:

yourproject/health.py
from django.http import HttpResponse


def health_check(get_response):
    def middleware(request):
        if request.path == "/healthz":
            return HttpResponse("ok")
        return get_response(request)

    return middleware
yourproject/settings.py
MIDDLEWARE = [
    "yourproject.health.health_check",  # first, before SecurityMiddleware
    "django.middleware.security.SecurityMiddleware",
    "whitenoise.middleware.WhiteNoiseMiddleware",
    # ... the rest of your middleware
]

Step 2: Create a project

A project is the home for one app, along with its database and domain. In your dashboard, open Projects and click New project:

Name
Lowercase letters, numbers and hyphens, up to 24 characters. It becomes part of your app’s free web address and can’t be changed later, so pick something short, like shop or api.
Plan
The plan the project draws its resources from.
Resources
CPU, memory and storage for this project, pre-filled with what your plan has left. The defaults suit most apps.

Click Create project. Setting it up takes about a minute, and you land on the deploy guide straight away, which walks you through three short steps: Source, Configure and Deploy. You can pick your repository while the project finishes setting up.

Step 3: Connect your repository

On the Source step, choose where your code comes from:

Your repository
Click Connect GitHub and approve access, which lets Chajio Cloud read your code and hear about your pushes. Then choose the repository and the branch to deploy, usually main. From now on, every push to that branch deploys automatically.
Public repository
Paste the URL of any public GitHub repository, no GitHub sign-in needed. Since you don’t own it, pushes can’t trigger deploys: deploy by hand, or tick Auto-deploy new releases to follow the project’s published releases.

If your app lives in a subfolder of the repository, as in a monorepo, set Build context to that folder, for example apps/web. Otherwise leave it as ., the repository root. Then click Connect repository and Continue.

Step 4: Configure

The Configure step has two runtime settings, both changeable later without a rebuild, and your environment variables.

Container port
Leave it at 3000. Your start command reads PORT, so it follows whatever you set here.
Health check path
/health for the Flask and FastAPI examples above, /healthz for Django. Any path that answers quickly with a success response works, but avoid one that needs a login or a database query.

Add the variables your app reads. For the Django example:

DJANGO_SECRET_KEY=a-long-random-string
DJANGO_ALLOWED_HOSTS=yourapp-xxxx.live.chajio.cloud,www.example.com

You’ll see your free address after the first deploy. Deploy once, then add it to DJANGO_ALLOWED_HOSTS. In Python, read variables with os.environ["NAME"] for required values and os.environ.get("NAME", "default") for optional ones.

Variables live on the project's Environment tab. Add each one as a name and a value, then click Save & restart: your app restarts with the new values in moments, no rebuild needed.

Your build receives the variables too. A Dockerfile gets the ones it declares with an ARG line, and an auto-detected build gets all of them. A variable that went into your current build is marked Build. Change one of those and the dashboard offers to Redeploy, because only a new build picks up the new value.

PORT is set by the platform from the container port, so you can't set it yourself. If you attach a database, its connection variable appears here too, locked, because the platform manages it for you.

Click Save and continue.

Step 5: Deploy

The Deploy step shows a summary of the repository, branch and build context. Click Deploy now and the build log starts streaming: your code is fetched, dependencies are installed and your app is built and started. The deployment moves through Queued, Building and Deploying to Success.

The first build takes a few minutes. Later builds reuse the parts that didn't change, so they are usually quicker. When it finishes, your app is live on a free live.chajio.cloud address with HTTPS already switched on. You'll find the address on the project's Overview.

If the deployment fails, the dashboard names the stage that broke and why, and the build log shows the details. Our troubleshooting guide covers the usual causes.

Adding a database

On plans that include one, create a managed PostgreSQL or MariaDB database from the project’s Databases tab and give it the variable name DATABASE_URL. The connection string is then handed to your app automatically, and dj_database_url.config() in the Django settings above picks it up with no further changes. Add a driver to requirements.txt: psycopg[binary] for PostgreSQL, or mysqlclient or PyMySQL for MariaDB. SQLAlchemy users need one small change to the URL, covered in How to Add a Database to Your App.

Connect your domain

Your app already works on its free address. To serve it from your own domain, open the project's Domains tab:

  1. Enter your domain, for example www.example.com, and click Add domain.
  2. The dashboard shows two DNS records to add where your domain's DNS is managed. A TXT record proves the domain is yours. A CNAME record routes visitors to your app, or an A or ALIAS record for a root domain like example.com. Copy the values exactly as shown.
  3. Click Verify. Once both records check out, a free HTTPS certificate is issued automatically, usually in under a minute, and your app is live on your domain.

DNS changes can take a while to spread, so if Verify doesn't find a record yet, give it a few minutes; the dashboard also re-checks on its own. The full walkthrough, including root domains and Cloudflare, is in How to connect your domain to your app.

For Django, add your domain to DJANGO_ALLOWED_HOSTS and save before you verify it, or requests to your new domain get a 400 error.

After launch

  • Every push deploys. Push to your branch and a new deployment starts on its own. Your current version keeps serving visitors until the new one passes its health check, so updates don't cause downtime. You can also deploy by hand from the Deployments tab.
  • Roll back in seconds. If a release misbehaves, open Deployments and click Rollback to this version on an earlier successful deployment. Nothing is rebuilt; the previous version simply comes back.
  • Watch it run. The Logs tab streams your app's output live, with secrets hidden. The Overview shows your app's status and address, with Redeploy, Restart and Stop buttons.

Your app runs as two copies

Every plan runs two copies of your app, so a crash or an update never takes it offline. That has one consequence worth designing for: anything an app keeps only in its own memory or on its own disk isn't shared with the other copy, and it's gone after a redeploy.

Keep sessions in your database or a cache service rather than in local memory, and store uploaded files in object storage. SQLite is a file on local disk, so each copy would have its own database: use a managed database instead.

Common problems

The build log says “No start command could be found”

The platform couldn’t work out how to start your app. Add a Procfile with a web: line, as in the Flask and FastAPI examples above.

The deployment fails because the app “did not become healthy”

Check the Logs tab. The usual causes are listening on 127.0.0.1 or a fixed port instead of 0.0.0.0:$PORT, a health check path your app doesn’t have, and, for Django, the host check described above.

Django shows “Bad Request (400)” on your domain

The domain isn’t in ALLOWED_HOSTS. Add it to DJANGO_ALLOWED_HOSTS on the Environment tab and save. The app restarts with the new value.

CSS and images are missing in Django

Static files aren’t being served. Check WhiteNoise is in MIDDLEWARE, STATIC_ROOT is set and your start command runs collectstatic.

ModuleNotFoundError when the app starts

A package you use isn’t in requirements.txt. Add it, commit and push. The deploy starts on its own.

For anything else, Deployment Failed? How to Find and Fix the Cause explains how to read a failed deployment.

Keep reading

Ready to deploy?

Start on the Free plan: your first month is on us, and every app runs two copies for high availability.

View App Hosting plans