Back to Blog
Tutorial11 min read2026-06-27

How to Deploy a Django REST Framework API to Production

A working DRF API on localhost is not production-ready. This tutorial covers Gunicorn, static files, database config, migrations, and security settings to ship your Django REST Framework API safely.

Ajay Kumar
Ajay Kumar
Founder & DevOps, PandaStack

# How to Deploy a Django REST Framework API to Production

Django REST Framework (DRF) is the go-to for building APIs in Django, but python manage.py runserver is a development server — it should never face the internet. Production needs a real WSGI server, proper static handling, secure settings, and a managed database. This tutorial walks through shipping a DRF API correctly.

What changes between dev and prod

ConcernDevelopmentProduction
ServerrunserverGunicorn / Uvicorn
DEBUGTrueFalse
DatabaseSQLiteManaged PostgreSQL
Static filesAuto-servedCollected + WhiteNoise/CDN
Secretssettings.pyEnvironment variables
Hosts*Explicit ALLOWED_HOSTS

Step 1: settings driven by environment

Never hardcode secrets or environment-specific config. Read everything from the environment:

# settings.py
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.parse(
        os.environ["DATABASE_URL"], conn_max_age=600, ssl_require=True
    )
}

dj-database-url parses a single DATABASE_URL string into Django's config dict — perfect for platforms that inject one.

Step 2: run with Gunicorn

Gunicorn is the standard production WSGI server. Size the worker count to your CPUs:

pip install gunicorn
gunicorn myproject.wsgi:application \
  --bind 0.0.0.0:${PORT:-8080} \
  --workers 3 \
  --timeout 60

A common rule of thumb is (2 × CPU cores) + 1 workers. If your API does heavy async I/O, consider an ASGI setup with Uvicorn workers instead.

Step 3: static files

DRF's browsable API and the admin both need static files. Collect them at build time and serve with WhiteNoise so you do not need a separate web server:

MIDDLEWARE = [
    "django.middleware.security.SecurityMiddleware",
    "whitenoise.middleware.WhiteNoiseMiddleware",  # right after security
    # ...
]
STATIC_ROOT = BASE_DIR / "staticfiles"
STATIC_URL = "/static/"
STORAGES = {
    "staticfiles": {"BACKEND": "whitenoise.storage.CompressedManifestStaticFilesStorage"},
}
python manage.py collectstatic --noinput

Step 4: migrations as a release step

Run migrations before new code serves traffic — never inside the request path:

python manage.py migrate --noinput

Many platforms support a "release" or pre-deploy command for exactly this. Keep migrations backward-compatible so a rolling deploy with old and new code briefly coexisting does not break.

Step 5: lock down security settings

With DEBUG = False, turn on the production security flags:

SECURE_SSL_REDIRECT = True
SECURE_PROXY_SSL_HEADER = ("HTTP_X_FORWARDED_PROTO", "https")
SESSION_COOKIE_SECURE = True
CSRF_COOKIE_SECURE = True
SECURE_HSTS_SECONDS = 31536000
SECURE_HSTS_INCLUDE_SUBDOMAINS = True

Run python manage.py check --deploy — it audits your settings and flags anything unsafe. Treat its warnings as a checklist.

Step 6: DRF-specific production tweaks

  • Disable the browsable API in prod (or keep it behind auth) to avoid exposing your API surface:
REST_FRAMEWORK = {
    "DEFAULT_RENDERER_CLASSES": ["rest_framework.renderers.JSONRenderer"],
    "DEFAULT_THROTTLE_RATES": {"anon": "60/min", "user": "1000/min"},
}
  • Enable throttling to protect against abuse.
  • Set pagination defaults so large list endpoints do not dump everything at once.

Step 7: the container

FROM python:3.12-slim
ENV PYTHONUNBUFFERED=1
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY . .
RUN python manage.py collectstatic --noinput
EXPOSE 8080
CMD gunicorn myproject.wsgi:application --bind 0.0.0.0:${PORT:-8080} --workers 3

Common pitfalls

  • DEBUG left True. Leaks stack traces and settings. The number one DRF prod mistake.
  • Missing ALLOWED_HOSTS. Django returns 400 for every request.
  • collectstatic forgotten. Admin and browsable API render unstyled or 404.
  • SQLite in production. It does not handle concurrent writes; use Postgres.
  • Migrations not run. ProgrammingError: relation does not exist.

Deploying on PandaStack

PandaStack auto-detects Python apps (or builds from your Dockerfile) and deploys the container described above. Provision a managed PostgreSQL (14.x or 16.x) and PandaStack injects DATABASE_URL, which dj-database-url reads directly — no manual connection string wiring. Add your custom domain and SSL is automatic, satisfying SECURE_SSL_REDIRECT. Live build and app logs let you watch collectstatic and migrate run, and rollbacks plus deploy history cover you if a migration misbehaves. The free tier (5 web services + 1 database) comfortably hosts a DRF API plus its database.

References

  • [Django deployment checklist](https://docs.djangoproject.com/en/stable/howto/deployment/checklist/)
  • [Django REST Framework documentation](https://www.django-rest-framework.org/)
  • [Gunicorn documentation](https://docs.gunicorn.org/en/stable/)
  • [WhiteNoise documentation](https://whitenoise.readthedocs.io/)
  • [dj-database-url](https://github.com/jazzband/dj-database-url)

---

A production DRF API is mostly correct settings plus a real WSGI server. PandaStack handles the build, the managed Postgres, and SSL so you can focus on the API — deploy free at [dashboard.pandastack.io](https://dashboard.pandastack.io).

Ready to deploy?

Start free on PandaStack.

Start free on PandaStack

More in Tutorial

Browse all Tutorial articles →

See also