Deploy a Django app (Python)
Deploy a Django project on OneBit Deploy from GitHub — gunicorn on port 8000, ALLOWED_HOSTS and SECRET_KEY from environment variables, migrations and static files. Python is in early access.
Python is in early access on OneBit Deploy. A Django project deploys straight from
a GitHub repository: we build a python:3.12-slim image, install requirements.txt,
find your project's wsgi.py and start gunicorn on port 8000.
This page uses the exact files we run as our own canary —
onebit-client/deploy-e2e-django:
django-admin startproject hello . plus one JSON view.
The project
Start from a standard project layout (manage.py at the repository root):
manage.py
requirements.txt
hello/
__init__.py
settings.py
urls.py
views.py
wsgi.py
asgi.pyrequirements.txt — Django and gunicorn:
Django==5.1.*
gunicorn==23.*Three settings must come from the environment instead of being hard-coded. Change
these lines in hello/settings.py:
import os
from pathlib import Path
SECRET_KEY = os.environ.get("SECRET_KEY", "dev-only-change-me-on-deploy")
DEBUG = os.environ.get("DEBUG", "").lower() in ("1", "true", "yes")
# Deploy serves the app on <name>-<id>.app.onebit.ng and any custom domain you attach.
ALLOWED_HOSTS = os.environ.get("ALLOWED_HOSTS", "*").split(",")
CSRF_TRUSTED_ORIGINS = ["https://*.app.onebit.ng"]ALLOWED_HOSTS is the one that bites: with the default empty list Django answers
every request with 400 Bad Request once DEBUG is off. * is fine to start;
narrow it to your hostnames in the app's environment later. Add your custom domain
to CSRF_TRUSTED_ORIGINS when you attach one.
A view and a URL so the root answers:
import os
from django.http import JsonResponse
def index(request):
return JsonResponse({"app": "deploy-e2e-django", "framework": "django", "runtime": "python", "port": os.environ.get("PORT", "8000"), "ok": True})
def health(request):
return JsonResponse({"status": "ok"})from django.urls import path
from . import views
urlpatterns = [
path("", views.index),
path("health", views.health),
]Push the repository to GitHub.
Deploy it
- In the console, on Apps, press Add app and open GitHub account (press Connect GitHub the first time).
- Pick the repository, choose the Branch and press Use this project. The console says "A Python app."; if it does not, open Build settings and set Framework to Python.
- Leave Port at
8000, check the Name and press Put it online. The Python version is3.12and the start command is empty; both are in the app's Settings → Build settings → Edit build settings. - On the app's Environment tab, add
SECRET_KEY(a long random string) and, when you are ready,ALLOWED_HOSTS. Saving restarts the app so it picks them up.
The build finds manage.py and hello/wsgi.py and runs:
gunicorn -b 0.0.0.0:8000 --workers 2 --threads 4 --timeout 60 --access-logfile - hello.wsgi:application(The build log prints it as Start command: ….) About a minute later the app answers
on https://<name>-<id>.app.onebit.ng/ with automatic HTTPS.
Migrations
The image does not run migrate for you. Run it before the server starts with a
Start command (app Settings → Build settings → Edit build settings):
python manage.py migrate --noinput && gunicorn hello.wsgi:application -b 0.0.0.0:8000The default SQLite file lives inside the container and is lost on every deploy and
restart. For anything real, point DATABASES at a managed PostgreSQL or MySQL
instance (Hobby plan and above) through an environment variable — see
Managed databases and
Connecting to your database.
Static files
runserver serves static files for you; gunicorn does not. The simplest production
setup is WhiteNoise:
- Add
whitenoisetorequirements.txt. - In
settings.pyadd"whitenoise.middleware.WhiteNoiseMiddleware"right afterSecurityMiddleware, setSTATIC_ROOT = BASE_DIR / "staticfiles"andSTORAGES = {"staticfiles": {"BACKEND": "whitenoise.storage.CompressedManifestStaticFilesStorage"}}. - Add
python manage.py collectstatic --noinputto the Build command in the app's Settings → Build settings → Edit build settings (it runs afterpip install).
Environment variables
| Variable | Used for |
|---|---|
SECRET_KEY | Django's signing key — set a long random value, never the dev default |
DEBUG | leave unset (off) in production |
ALLOWED_HOSTS | comma-separated hostnames; * while you set up |
PORT | set by the platform to the container port (8000) |
DATABASE_URL or your own DB_* names | the managed database connection |
Add them on the app's Environment tab — see Environment variables.
Good to know
- Free plan apps sleep when idle and wake on the next request; Starter and above stay always-on — see Scaling & idle.
- Two gunicorn workers with four threads each is the default. Pass your own
--workersin a start command if the app needs more (mind the memory you allocated). - Django REST Framework, Wagtail and friends need nothing special — they are Django apps.
- Your own Dockerfile wins. If the repository carries a
Dockerfilewe build from it instead.