Skip to content

Production Setup

In this section, we’ll go over the key things you need to do before putting your Django app into production. This includes setting up good logging in your Django REST views, managing your environment variables safely, and deploying your app using platforms like Heroku. By following these steps, you can make sure your Django app is solid, secure, and ready to handle real traffic once it’s live.

Django’s logging system gets set up inside settings.py, but it’s just as important to follow good logging habits inside your views too. Use a logger for each module, avoid logging anything sensitive, and use logger.exception() whenever you catch an error you weren’t expecting, so the full error details (traceback) get saved in your logs.

Reference: /backend/django/fundamentals#logging

from rest_framework.views import APIView
from rest_framework.response import Response
import logging
logger = logging.getLogger(__name__)
class MyAPIView(APIView):
def get(self, request):
logger.debug("Handling GET request")
logger.info("Returning hello message")
logger.warning("Example warning in GET")
logger.error("Example error in GET")
logger.critical("Example critical in GET")
return Response({"message": "Hello, world!"})
# Professional example of logging in a POST handler
def post(self, request):
try:
data = request.data
name = data.get("name")
if not name:
logger.warning("Missing 'name' in request", extra={"request_data": data})
return Response({"error": "Name field is required"}, status=400)
# Log only non-sensitive fields to avoid leaking data
logger.info("Received valid data", extra={"name": name})
return Response({"message": "Data received successfully"})
except ValueError as e:
logger.error("ValueError in MyAPIView.post", exc_info=True)
return Response({"error": str(e)}, status=400)
except Exception:
# Includes stack trace in the log
logger.exception("Unexpected error in MyAPIView.post")
return Response({"error": "An unexpected error occurred"}, status=500)

By sticking to these logging habits, your Django REST views will give you useful insight into how your app is behaving, while still staying secure and fast in production.

Environment variables are the backbone of a clean, secure Django setup. They let you keep your configuration separate from your code, and they make it easy to safely manage the differences between development and production.

Instead of hardcoding sensitive values directly in your code (things like secret keys, database URLs, or API keys), you store them as environment variables, and then read them whenever your project needs them.

Why this matters

  • Keeps your secrets out of your source code
  • Makes it easy to use different configs on different servers
  • Helps you avoid accidentally leaking secrets (especially on GitHub)
  • Helps keep your whole project structure clean

Using .env with python-dotenv

You can use python-dotenv to load your environment variables from a .env file while you’re developing.

Example .env file:

DEBUG=True
SECRET_KEY=your-secret-key
DJANGO_SETTINGS_MODULE=myproject.settings.development

Load the variables into your project like this:

from dotenv import load_dotenv
import os
load_dotenv()
DEBUG = os.getenv("DEBUG") == "True"
SECRET_KEY = os.getenv("SECRET_KEY")

Approach 1: Single settings file with conditions

Uses just one settings.py file, and switches its behavior based on environment variables.

Approach 2: Multiple settings files

Splits your settings into separate base, development, and production files.

Approach 1: Single Settings File with Conditional Logic

Section titled “Approach 1: Single Settings File with Conditional Logic”

This approach uses “if” conditions inside one single settings file.

import os
DEBUG = os.getenv("DEBUG") == "True"
PRODUCTION = "production"
ENVIRONMENT = os.getenv("ENVIRONMENT", PRODUCTION).lower()
if DEBUG and ENVIRONMENT != PRODUCTION:
ALLOWED_HOSTS = []
else:
ALLOWED_HOSTS = ["yourdomain.com"]
# Other settings...

This is the recommended approach if you want your project to scale well and be production-ready.

Folder structure:

myproject/
settings/
base.py
development.py
production.py

Step-by-step setup:

  1. Create a base settings file, and put all the shared configuration in it.
  2. Create a development settings file, import the base file, and override any development-specific values.
  3. Create a production settings file, import the base file, and override any production-specific values.
  4. Set DJANGO_SETTINGS_MODULE, and use an environment variable to choose which settings file gets loaded.

Base settings (base.py):

from pathlib import Path
BASE_DIR = Path(__file__).resolve().parent.parent.parent
INSTALLED_APPS = [
# shared apps
]
MIDDLEWARE = [
# shared middleware
]

Development settings (development.py):

from .base import *
DEBUG = True
ALLOWED_HOSTS = []
# Development-specific settings (e.g., SQLite, local email backend)

Production settings (production.py):

from .base import *
DEBUG = False
SECRET_KEY = os.getenv("SECRET_KEY")
ALLOWED_HOSTS = ["yourdomain.com"]
# Production-specific settings (e.g., PostgreSQL, real email backend)

Always use DJANGO_SETTINGS_MODULE to control exactly which settings file Django loads.

DJANGO_SETTINGS_MODULE=myproject.settings.development
DJANGO_SETTINGS_MODULE=myproject.settings.production

If you want your development setup to work without manually setting environment variables every time, you can set a default value inside manage.py and other files that reference your settings, like wsgi.py, asgi.py, celery.py, and so on:

import os
import sys
def main():
"""Run administrative tasks."""
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'myproject.settings.development')
try:
from django.core.management import execute_from_command_line
except ImportError as exc:
raise ImportError(
"Couldn't import Django. Are you sure it's installed and "
"available on your PYTHONPATH environment variable? Did you "
"forget to activate a virtual environment?"
) from exc
execute_from_command_line(sys.argv)
if __name__ == '__main__':
main()
ApproachSimplicityScalabilityRecommended For
Single fileVery easyLowBeginners, small apps
Multiple filesModerateHighProfessional projects

When you deploy your app, you should use a production-ready web server like Gunicorn or Waitress (these are called WSGI servers), or Uvicorn (an ASGI server). These servers are built to handle many requests at the same time, and are made to be fast and reliable. They usually work alongside a reverse proxy like Nginx, which can serve static files, handle SSL (secure connections), and add extra layers of security.

Gunicorn is a popular WSGI server for Python apps, including Django. It’s built to be simple, fast, and to work with many different web frameworks. Gunicorn doesn’t support Windows, but it’s widely used on Linux.

To use this server, first install it:

Terminal window
uv add gunicorn

Then run your Django app with Gunicorn:

Terminal window
gunicorn myproject.wsgi:application
# or
gunicorn myproject.wsgi:application --bind 0.0.0.0:8000

Waitress is a production-quality WSGI server, written entirely in Python, and it performs quite well. It’s built to be easy to use, and it works with a wide range of web frameworks, including Django. Waitress works across different operating systems and runs nicely on Windows, which makes it a solid choice if you need a WSGI server that can run on multiple platforms.

To use Waitress, first install it:

Terminal window
uv add waitress

Then run your Django app with Waitress:

Terminal window
waitress-serve --port=8000 myproject.wsgi:application
# or
waitress-serve --listen=0.0.0.0:8000 myproject.wsgi:application

Uvicorn is a very fast ASGI server, built using uvloop and httptools. It’s designed to be lightweight and efficient, which makes it a great choice for running asynchronous web apps built with frameworks like Django Channels or FastAPI. Uvicorn works well across Windows, Linux, and macOS.

To use Uvicorn, first install it:

Terminal window
uv add uvicorn

Then run your Django app with Uvicorn:

Terminal window
uvicorn myproject.asgi:application --host=0.0.0.0 --port=8000

DRF Spectacular automatically builds production-grade OpenAPI 3.0 documentation (schemas) for your Django REST Framework APIs. Unlike writing documentation by hand, it actually looks through your code to create accurate, up-to-date API documentation, without you having to put in extra effort. Other tools, frontend developers, and API clients can all use this documentation to understand and work with your API, generate code, and run tests against it.

Docs: https://github.com/tfranzel/drf-spectacular/

To get started with DRF Spectacular, first install it:

Terminal window
uv add drf-spectacular

Once it’s installed, you can set it up in your Django settings. Add it to INSTALLED_APPS, so Django recognizes the package and its management commands.

INSTALLED_APPS = [
# ... other installed apps
'drf_spectacular',
]

Tell DRF to use Spectacular’s upgraded schema generator instead of the default one. This lets Spectacular take a deep look at your viewsets, serializers, and authentication setup, so it can build a complete OpenAPI specification:

REST_FRAMEWORK = {
# ... other DRF settings
"DEFAULT_SCHEMA_CLASS": "drf_spectacular.openapi.AutoSchema",
}

Set up the OpenAPI schema’s basic info. This shows up at the top of your API documentation:

SPECTACULAR_SETTINGS = {
"TITLE": "your project API",
"DESCRIPTION": "A detailed description of your API",
"VERSION": "1.0.0",
"SERVE_INCLUDE_SCHEMA": False,
}

Finally, expose the API schema through a few URL endpoints. These give developers different ways to access your documentation:

  • api/schema/ - The raw OpenAPI schema (in JSON format, meant for tools to read)
  • api/schema/swagger-ui/ - Interactive Swagger UI documentation (easy to browse in a browser)
  • api/schema/redoc/ - ReDoc documentation (a cleaner, alternative way to view it)
from drf_spectacular.views import (
SpectacularAPIView,
SpectacularRedocView,
SpectacularSwaggerView,
)
from django.urls import path
urlpatterns = [
# ... other URL patterns
path("api/schema/", SpectacularAPIView.as_view(), name="schema"),
path("api/schema/swagger-ui/", SpectacularSwaggerView.as_view(url_name="schema"), name="swagger-ui"),
path("api/schema/redoc/", SpectacularRedocView.as_view(url_name="schema"), name="redoc"),
]

Heroku is a platform as a service (PaaS), which basically means it handles the servers for you, so you don’t have to manage them yourself. This makes deploying Django much easier. It’s a great fit for learning, portfolio projects, and plenty of small-to-medium production apps too.

This section is written to stay beginner-friendly, but the default setup shown here is also production-aware, meaning it won’t lead you astray once your app goes live.

The Heroku CLI is a command-line tool that lets you create apps, set environment variables, manage add-ons, and run one-off commands.

Install it from the official docs:

https://devcenter.heroku.com/articles/heroku-cli

After installing it, check that it works and log in:

Terminal window
heroku --version
heroku login

From inside your Django project folder, run:

Terminal window
heroku create your-app-name

Heroku will give you back:

  • An app URL (usually https://your-app-name.herokuapp.com/)
  • A Git remote address (https://git.heroku.com/your-app-name.git)

Update your Django settings to get ready for production.

Set ALLOWED_HOSTS to include your Heroku domain:

ALLOWED_HOSTS = ["your-app-name.herokuapp.com"]

Also make sure your project is using:

  • WhiteNoise (or some other method) to serve static files
  • A production-ready app server, like Gunicorn
  • DEBUG = False in your production settings

Heroku keeps your app’s configuration stored as environment variables, which it calls “Config Vars.”

Set the core ones like this:

Terminal window
heroku config:set SECRET_KEY='your-secret-key'
heroku config:set DJANGO_SETTINGS_MODULE='myproject.settings.production'
# etc.

You’ll usually end up adding more variables too (like a database URL, Redis URL, API keys, email login details, and so on). Just make sure none of your secrets ever end up in version control (like Git).

Heroku uses a file called a Procfile to know which processes it should run.

Create a Procfile in your project’s root folder:

release: python manage.py migrate
web: gunicorn myproject.wsgi:application
worker: celery -A myproject worker

Notes:

  • Keep the web line, since it runs your main web server.
  • Keep the release line if you want migrations to run automatically on every deploy.
  • Only keep the worker line if you’re actually using Celery.

PostgreSQL is the recommended default database for running Django on Heroku, but you’re free to use MySQL or something else if you’d rather.

Install a helper package that reads database URLs for you:

Terminal window
uv add dj-database-url

Then use it to read the database URL inside your settings:

import dj_database_url
DATABASES = {
"default": dj_database_url.config(
conn_max_age=600,
ssl_require=True,
)
}

This expects a DATABASE_URL environment variable to already be set.

Set up Heroku Postgres:

Terminal window
heroku addons:create heroku-postgresql:<plan>
# example plan: essential-0 (check current plans in your region/account)
# docs: https://devcenter.heroku.com/articles/heroku-postgres-plans

Heroku will automatically set DATABASE_URL for you. You can check it like this:

Terminal window
heroku config

You should see output like this:

=== your-app-name Config Vars
DATABASE_URL: postgres://username:password@hostname:port/database_name
SECRET_KEY: your-secret-key
DJANGO_SETTINGS_MODULE: myproject.settings.production

We recommend using JawsDB if you want MySQL on Heroku. It gives you a managed MySQL database with a simple setup, and it’s a popular choice for Django apps that need MySQL instead of PostgreSQL.

If you’re using JawsDB, you can either read straight from JAWSDB_URL, or copy its value over to DATABASE_URL to keep things consistent.

Terminal window
heroku addons:create jawsdb:<plan>
# example plan: kitefin (check current plans in your region/account)
# docs: https://elements.heroku.com/addons/jawsdb

JawsDB usually gives you a variable called JAWSDB_URL.

Update settings.py to read directly from JAWSDB_URL:

import dj_database_url
DATABASES = {
"default": dj_database_url.config(
env="JAWSDB_URL",
conn_max_age=600,
ssl_require=True,
)
}

To check your variables:

Terminal window
heroku config

You should see either JAWSDB_URL or DATABASE_URL in the output:

=== your-app-name Config Vars
JAWSDB_URL: mysql://username:password@hostname:port/database_name
DATABASE_URL: mysql://username:password@hostname:port/database_name
SECRET_KEY: your-secret-key
DJANGO_SETTINGS_MODULE: myproject.settings.production

You can use a single Redis instance for both Django caching and Celery, all at once.

Set up Heroku Redis:

Terminal window
heroku addons:create heroku-redis:<plan>
# example plan: mini (check current plans in your region/account)
# docs: https://elements.heroku.com/addons/heroku-key-value-store
# (it is not free )
# https://elements.heroku.com/addons/rediscloud (it has a free plan)
# docs: https://elements.heroku.com/addons/rediscloud

Heroku will set REDIS_URL for you automatically. Check it like this:

Terminal window
heroku config

You should see something like this in the output:

=== your-app-name Config Vars
REDIS_URL: redis://username:password@hostname:port
# other environment variables...

Use REDIS_URL for both caching and Celery:

import os
REDIS_URL = os.getenv("REDIS_URL")
# Configure Django caching
CACHES = {
"default": {
"BACKEND": "django_redis.cache.RedisCache",
"LOCATION": REDIS_URL,
"OPTIONS": {
"CLIENT_CLASS": "django_redis.client.DefaultClient",
},
}
}
# Configure Celery broker/result backend
CELERY_BROKER_URL = REDIS_URL
CELERY_RESULT_BACKEND = REDIS_URL

Mailgun is a common choice for sending transactional emails (like password resets or order confirmations) from Django apps.

Set up the Mailgun add-on:

Terminal window
heroku addons:create mailgun:<plan>
# example plan: starter (check current plans in your region/account)
# docs: https://elements.heroku.com/addons/mailgun

This sets up config variables like MAILGUN_SMTP_LOGIN, MAILGUN_SMTP_PASSWORD, MAILGUN_SMTP_PORT, and MAILGUN_SMTP_SERVER.

Check them like this:

Terminal window
heroku config

Example output:

=== your-app-name Config Vars
MAILGUN_PUBLIC_KEY: your-mailgun-public-key
MAILGUN_SMTP_LOGIN: your-mailgun-smtp-login
MAILGUN_SMTP_PASSWORD: your-mailgun-smtp-password
MAILGUN_SMTP_PORT: your-mailgun-smtp-port
MAILGUN_SMTP_SERVER: your-mailgun-smtp-server
# other environment variables...

Django settings:

import os
EMAIL_HOST = os.getenv("MAILGUN_SMTP_SERVER")
EMAIL_PORT = int(os.getenv("MAILGUN_SMTP_PORT", "587"))
EMAIL_HOST_USER = os.getenv("MAILGUN_SMTP_LOGIN")
EMAIL_HOST_PASSWORD = os.getenv("MAILGUN_SMTP_PASSWORD")
EMAIL_USE_TLS = True

Once everything is configured, you can deploy using Git:

Terminal window
git add .
git commit -m "Prepare for Heroku deployment"

If you haven’t added the Heroku remote yet:

Terminal window
git remote add heroku https://git.heroku.com/your-app-name.git

Push your code:

Terminal window
git push heroku main

After deploying, it’s a good habit to run:

Terminal window
heroku logs --tail

This helps you quickly spot and fix any startup problems.

Production Terminal Access and Running Commands

Section titled “Production Terminal Access and Running Commands”

You can run one-off commands directly on Heroku for admin tasks.

Terminal window
heroku run bash

This opens up a shell on a temporary, one-off dyno (Heroku’s term for a running instance of your app). From here, you can run Django management commands.

Terminal window
python manage.py createsuperuser

You can use this terminal for things like creating an admin user, getting shell access, fixing data, or running other commands by hand.

Terminal window
exit

This closes the dyno’s shell and takes you back to your own local terminal.

We can use Docker to package up our Django app into a container, which makes it much easier to deploy across different environments. Docker lets you bundle your app together with everything it depends on into one single package (a container), so it behaves the same way no matter where you run it, and deployment becomes much simpler.

To learn more about putting Django into Docker, check out the official Docker documentation: https://docs.docker.com/reference/samples/django/

If Heroku isn’t quite right for you, here are some other popular options. Pricing and free plans change over time, so it’s always worth double-checking the provider’s pricing page before you decide.

ProviderWhat it providesHomepageFree planPaid only
RenderManaged web services, background workers, PostgreSQL, Redis, cron jobsrender.comYes (limited)No
RailwayEasy app deploys, databases, private networking, templatesrailway.comYes (trial/credits)No
Fly.ioGlobal VM/container deploys close to users, Postgres option, private networkingfly.ioYes (limited)No
KoyebServerless containers, autoscaling, global regionskoyeb.comYes (limited)No
DigitalOcean App PlatformPaaS deploy from Git, managed DB integration, workersdigitalocean.comNoYes
PythonAnywhereBeginner-friendly Python hosting, easy Django setup, scheduled taskspythonanywhere.comYes (limited)No
HerokuMature Django workflow, add-ons marketplace, one-off dyno commandsheroku.comYes (limited)Yes
AWS Elastic BeanstalkManaged deployment orchestration on AWS infrastructureaws.amazon.comYes (AWS Free Tier limits)No
Azure App ServiceManaged web app hosting with scaling and CI/CD integrationazure.microsoft.comYes (limited/trial based)No
Google Cloud RunContainer-based serverless runtime with automatic scalingcloud.google.comYes (always free usage tier)No
Google App EngineManaged platform for app deployment with autoscalingcloud.google.comYes (limited)No

Recommended for beginners:

  • Render, PythonAnywhere, or Railway are the easiest places to start.
  • Heroku is great if you want a polished collection of add-ons (usually paid).
  • Cloud Run is a good fit if you’re already comfortable with Docker/containers.
ProviderWhat it providesHomepageFree planPaid only
Neon (PostgreSQL)Serverless Postgres with branching and autoscaling storage/computeneon.techYesNo
Supabase (PostgreSQL)Managed Postgres + auth + storage + realtime APIssupabase.comYesNo
AWS RDSManaged PostgreSQL/MySQL/MariaDB and more on AWSaws.amazon.comYes (Free Tier constraints)No
Google Cloud SQLManaged PostgreSQL/MySQL/SQL Server on GCPcloud.google.comNo (typically paid, trial credits possible)Yes
Azure Database for PostgreSQL/MySQLManaged relational databases on Azureazure.microsoft.comNo (typically paid, trial credits possible)Yes
Heroku PostgresManaged Postgres tightly integrated with Herokuelements.heroku.comNo (generally paid)Yes
JawsDB (MySQL)Managed MySQL add-on commonly used with Herokuelements.heroku.comUsually no (plan-dependent)Usually yes
TiDB Cloud (MySQL-compatible)Distributed SQL database with MySQL compatibilitytidbcloud.comYesNo
PlanetScale (MySQL-compatible)Serverless MySQL-compatible platform focused on scaling/workflowsplanetscale.comNo (currently paid plans)Yes

Beginner tip:

  • If you’re using Django with PostgreSQL, Neon or Supabase are usually the quickest to set up.
  • If you need MySQL, JawsDB is handy on Heroku, while TiDB Cloud and other managed providers are good options outside Heroku.
ProviderWhat it providesHomepageFree planPaid only
Upstash RedisServerless Redis with REST and pay-per-usage modelupstash.comYesNo
Redis CloudManaged Redis by Redis, Inc. with multiple plansredis.ioYes (limited)No
Heroku Key-Value Store (Heroku Redis)Redis for Heroku apps with tight platform integrationelements.heroku.comNo (generally paid)Yes
AWS ElastiCacheManaged Redis/Valkey on AWS for production workloadsaws.amazon.comNo (typically paid)Yes
Azure Cache for RedisManaged Redis on Azure with enterprise featuresazure.microsoft.comNo (typically paid)Yes
Google Cloud MemorystoreManaged Redis on GCP for low-latency cachingcloud.google.comNo (typically paid)Yes

Beginner tip:

  • Start out with just one Redis instance shared between cache and Celery.
  • Split into separate instances later, once your workload starts to grow.
ProviderWhat it providesHomepageFree planPaid only
ResendModern transactional email API focused on developer UXresend.comYesNo
MailgunTransactional email APIs + SMTP relay + analyticsmailgun.comYes (trial/limited)No
SendGridTransactional and marketing email servicessendgrid.comYes (limited)No
PostmarkTransactional email with strong deliverability focuspostmarkapp.comNo free tier (trial available)Yes
Amazon SESCost-effective bulk and transactional email serviceaws.amazon.comYes (conditions apply)No

Beginner tip:

  • For the quickest setup, go with Resend, Mailgun, or SendGrid.
  • If you’re sending emails at large scale and want lower cost, SES is often the better choice once you’re past the initial setup.