Back to Blog
Tutorial9 min read2026-07-18

Deploy a Django Ninja API on PandaStack (2026)

Django Ninja gives you FastAPI-style typed, async, auto-documented endpoints on top of Django's ORM, admin, and auth. Here's how to containerize one and deploy it to PandaStack with managed PostgreSQL.

Ajay Kumar
Ajay Kumar
Founder & DevOps, PandaStack

There's a false choice developers keep making: FastAPI for the nice typed API, or Django for the ORM, admin, migrations, and auth that you'd otherwise rebuild by hand. Django Ninja (https://django-ninja.dev) refuses the choice. It bolts a FastAPI-style routing layer — Pydantic schemas, async views, automatic OpenAPI docs — onto a real Django project. You get Swagger at /api/docs and python manage.py migrate in the same codebase.

This guide deploys a Django Ninja API to PandaStack with a managed PostgreSQL database.

Why Django Ninja over plain FastAPI

  • Django ORM — mature migrations, a huge ecosystem, select_related/prefetch_related that actually work.
  • Django admin — a free CRUD backend for your data, no extra code.
  • Django auth — sessions, permissions, password hashing, all solved.
  • Ninja on top — typed request/response schemas, async endpoints, and OpenAPI docs generated from your type hints.

If you're already a FastAPI shop with SQLModel and no need for the admin, plain FastAPI is fine. If you keep wishing FastAPI had Django's ORM and admin, this is your framework.

Step 1: A minimal Django Ninja app

api/views.py:

from ninja import NinjaAPI, Schema
from django.contrib.auth.models import User

api = NinjaAPI()

class UserOut(Schema):
    id: int
    username: str
    email: str

@api.get("/health")
def health(request):
    return {"status": "ok"}

@api.get("/users", response=list[UserOut])
def list_users(request):
    return list(User.objects.all())

@api.get("/users/{user_id}", response=UserOut)
def get_user(request, user_id: int):
    return User.objects.get(id=user_id)

Wire it in urls.py:

from django.urls import path
from api.views import api

urlpatterns = [
    path("api/", api.urls),   # docs live at /api/docs automatically
]

Step 2: Production settings from environment variables

In settings.py, read everything from the environment — never hardcode secrets:

import os
import dj_database_url

SECRET_KEY = os.environ["SECRET_KEY"]
DEBUG = os.environ.get("DEBUG", "False") == "True"
ALLOWED_HOSTS = os.environ.get("ALLOWED_HOSTS", "*").split(",")

DATABASES = {
    "default": dj_database_url.config(default=os.environ["DATABASE_URL"])
}

Step 3: requirements.txt

django>=5.0
django-ninja>=1.3
gunicorn>=21.0
uvicorn[standard]>=0.29
psycopg[binary]>=3.1
dj-database-url>=2.1
whitenoise>=6.6

Note uvicorn — Django Ninja's async endpoints need an ASGI server to actually run concurrently.

Step 4: Dockerfile

FROM python:3.12-slim

WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY . .

RUN python manage.py collectstatic --noinput

EXPOSE 8000

# Gunicorn with uvicorn workers = ASGI (async) in production
CMD ["gunicorn", "myproject.asgi:application", \
     "-k", "uvicorn.workers.UvicornWorker", \
     "--bind", "0.0.0.0:8000", "--workers", "2"]

Replace myproject with the package that contains asgi.py. Using wsgi:application instead would silently make your async endpoints run synchronously — a classic performance mystery.

Step 5: Create the managed PostgreSQL database

  1. 1Dashboard (https://dashboard.pandastack.io) → Databases → New Database → PostgreSQL.
  2. 2When you attach it to your app, DATABASE_URL is injected automatically — you don't paste the connection string anywhere.

Step 6: Deploy

  1. 1Push to GitHub.
  2. 2New App → Container App, connect the repo (PandaStack detects the Dockerfile), container port 8000.
  3. 3Attach the PostgreSQL database.
  4. 4Set environment variables:
VariableValue
SECRET_KEYpython -c "import secrets; print(secrets.token_urlsafe(50))"
DEBUGFalse
ALLOWED_HOSTSyour-api.pandastack.io
  1. 1Deploy. Every push to your default branch redeploys.

CLI path:

npm install -g @pandastack/cli
panda login
panda deploy

Step 7: Run migrations

The reliable pattern is a release step so migrations run once before the new version serves traffic. An entrypoint script:

#!/bin/sh
python manage.py migrate --noinput
exec gunicorn myproject.asgi:application \
  -k uvicorn.workers.UvicornWorker --bind 0.0.0.0:8000 --workers 2

Or run one-off via the CLI:

panda exec --command "python manage.py migrate"

Step 8: Verify

Your API is live at https://your-api.pandastack.io:

  • Interactive docs: https://your-api.pandastack.io/api/docs
  • Health: curl https://your-api.pandastack.io/api/health{"status":"ok"}

Create a superuser to reach the Django admin:

panda exec --command "python manage.py createsuperuser"

Then https://your-api.pandastack.io/admin/ gives you a full CRUD backend for free.

Performance notes

  • Match --workers roughly to available CPU. For async-heavy workloads, fewer workers with more concurrency often beats many workers.
  • Add Redis (managed on PandaStack) for caching and to back Django's cache framework.
  • Serve static files with WhiteNoise (already in requirements) — no separate static host needed.

Wrap-up

Django Ninja is the "why not both" answer: typed, async, documented endpoints with Django's ORM, admin, and auth underneath. On PandaStack it's a container app + managed Postgres + a migrate step, deployed on Git push. Docs: https://docs.pandastack.io. Start free at https://dashboard.pandastack.io.

Ready to deploy?

Start free on PandaStack.

Start free on PandaStack

More in Tutorial

Browse all Tutorial articles →

See also