OneBitDeploy
Deploying

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

requirements.txt — Django and gunicorn:

requirements.txt
Django==5.1.*
gunicorn==23.*

Three settings must come from the environment instead of being hard-coded. Change these lines in hello/settings.py:

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:

hello/views.py
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"})
hello/urls.py
from django.urls import path

from . import views

urlpatterns = [
    path("", views.index),
    path("health", views.health),
]

Push the repository to GitHub.

Deploy it

  1. In the console, on Apps, press Add app and open GitHub account (press Connect GitHub the first time).
  2. 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.
  3. Leave Port at 8000, check the Name and press Put it online. The Python version is 3.12 and the start command is empty; both are in the app's Settings → Build settings → Edit build settings.
  4. 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:8000

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

  1. Add whitenoise to requirements.txt.
  2. In settings.py add "whitenoise.middleware.WhiteNoiseMiddleware" right after SecurityMiddleware, set STATIC_ROOT = BASE_DIR / "staticfiles" and STORAGES = {"staticfiles": {"BACKEND": "whitenoise.storage.CompressedManifestStaticFilesStorage"}}.
  3. Add python manage.py collectstatic --noinput to the Build command in the app's Settings → Build settings → Edit build settings (it runs after pip install).

Environment variables

VariableUsed for
SECRET_KEYDjango's signing key — set a long random value, never the dev default
DEBUGleave unset (off) in production
ALLOWED_HOSTScomma-separated hostnames; * while you set up
PORTset by the platform to the container port (8000)
DATABASE_URL or your own DB_* namesthe 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 --workers in 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 Dockerfile we build from it instead.

Next steps

On this page