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.
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(apyproject.tomlorPipfileworks too).
Step 1: Get your app ready
Chajio Cloud builds your app from three things in your repository:
requirements.txtlists your packages, including the server that runs your app in production:gunicornfor Flask and Django,uvicornfor FastAPI. The built-in development servers (flask run,manage.py runserver) aren’t meant for real traffic..python-versionpicks your Python version. Without it you get a default that may not match what you develop on. The file contains just the version, for example3.12.- A
Procfilesays how to start your app. It’s a file named exactlyProcfile, no extension, in the root of your repository, with one line startingweb:. 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
flask==3.1.0
gunicorn==23.0.0from 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}web: gunicorn app:app --bind 0.0.0.0:$PORT --workers 2app: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
fastapi==0.115.6
uvicorn[standard]==0.34.0from fastapi import FastAPI
app = FastAPI()
@app.get("/")
def home():
return {"message": "Hello from Chajio Cloud"}
@app.get("/health")
def health():
return {"ok": True}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.wsgiAdd 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:
web: python manage.py collectstatic --noinput && python manage.py migrate && gunicorn yourproject.wsgi --workers 2Gunicorn picks up PORT on its own, so no --bind is needed. Then make your settings read their production values from environment variables:
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:
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 middlewareMIDDLEWARE = [
"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
/healthfor the Flask and FastAPI examples above,/healthzfor 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.comYou’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:
- Enter your domain, for example
www.example.com, and click Add domain. - 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. - 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.
Learning path
Deploy your first app
- 1
Deploy your app
- 2
- 3
- 4
Keep reading
How to Add a Database to Your App
Create a managed PostgreSQL or MariaDB database next to your app and have its connection details handed to your app automatically, with examples for Node.js, Python and Django.
How to Connect Your Domain to Your App (with Free HTTPS)
Put your app on your own domain in three DNS steps. Covers www versus root domains, what each DNS record does, Cloudflare, and what to check when a domain will not verify.
How to Deploy Any App with a Dockerfile
Go, PHP, .NET, Rust or anything else: if it runs from a Dockerfile, it runs on Chajio Cloud. The rules your Dockerfile needs to follow, build-time variables, and a worked Go example.
Deployment Failed? How to Find and Fix the Cause
Every failed deployment tells you which stage broke and why. How to read it, and the fixes for the failures we see most: missing start commands, port mix-ups, health checks and memory.
Ready to deploy?
Start on the Free plan: your first month is on us, and every app runs two copies for high availability.