# 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
| Concern | Development | Production |
|---|---|---|
| Server | runserver | Gunicorn / Uvicorn |
| DEBUG | True | False |
| Database | SQLite | Managed PostgreSQL |
| Static files | Auto-served | Collected + WhiteNoise/CDN |
| Secrets | settings.py | Environment 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 60A 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 --noinputStep 4: migrations as a release step
Run migrations before new code serves traffic — never inside the request path:
python manage.py migrate --noinputMany 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 = TrueRun 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 3Common 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).