Skip to content

Production Setup

In this section, we will cover essential practices for preparing your Django application for production. This includes configuring logging for your Django REST views, managing environment variables securely, and deploying your application using platforms like Heroku. By following these guidelines, you can ensure that your Django application is robust, secure, and ready to handle real-world traffic in a production environment.

Django’s logging framework is configured in settings.py, but it’s important to follow logging best practices inside your views. Use module-level loggers, avoid logging sensitive data, and prefer logger.exception() when catching unexpected errors so tracebacks are included in 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 following these logging practices, you can ensure that your Django REST views provide valuable insights into application behavior while maintaining security and performance in production environments.

Environment variables are the foundation of a clean and secure Django setup. They allow you to separate configuration from code and safely manage differences between development and production.

Instead of hardcoding sensitive values (like secret keys, database URLs, API keys), you store them in environment variables and access them dynamically in your project.

Why this matters

  • Keeps secrets out of source code
  • Makes deployment flexible (different configs per server)
  • Prevents accidental leaks (especially on GitHub)
  • Helps maintain clean project structure

Using .env with python-dotenv

You can use python-dotenv to load environment variables from a .env file during development.

Example .env file:

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

Load variables in your project:

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 one settings.py and switches behavior using environment variables.

Approach 2: Multiple settings files

Splits settings into 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 conditional logic inside one 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 for scalable and production-ready projects.

Folder structure:

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

Step-by-step setup:

  1. Create base settings and Put all shared configuration here.
  2. Create development settings and Import base and override development-specific values.
  3. Create production settings and Import base and override production-specific values.
  4. Set DJANGO_SETTINGS_MODULE and Use environment variable to choose which settings to load.

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 which settings Django loads.

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

If you want development to work without setting environment variables manually, you can set a default in manage.py and other files that reference settings like (wsgi.py, asgi.py, celery.py, etc.):

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 deploying we use a production-ready web server like Gunicorn, Waitress (WSGI servers) or Uvicorn (ASGI servers), which can handle multiple requests concurrently and is designed for performance and reliability. These servers work alongside a reverse proxy like Nginx, which can manage static files, handle SSL termination, and provide additional security features.

Gunicorn is a popular WSGI server for Python applications, including Django. It is designed to be simple, fast, and compatible with various web frameworks. Gunicorn not support windows but it is widely used in Linux environments.

To use this server, you need to install it first:

Terminal window
uv add gunicorn

Then you can run your Django application with Gunicorn:

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

Waitress is a production-quality pure-Python WSGI server with very acceptable performance. It is designed to be simple to use and compatible with a wide range of web frameworks, including Django. Waitress is cross-platform and works well on Windows, making it a good choice for developers who need a WSGI server that can run on multiple operating systems.

To use Waitress, you can install it first:

Terminal window
uv add waitress

Then you can run your Django application 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 lightning-fast ASGI server implementation, using uvloop and httptools. It is designed to be lightweight and efficient, making it an excellent choice for running asynchronous web applications built with frameworks like Django Channels or FastAPI. Uvicorn is cross-platform and works well on Windows, Linux, and macOS.

To use Uvicorn, you can install it first:

Terminal window
uv add uvicorn

Then you can run your Django application with Uvicorn:

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

DRF Spectacular automatically generates production-grade OpenAPI 3.0 schemas for Django REST Framework APIs. Unlike manual documentation, it introspects your code to produce accurate, up-to-date API specifications without extra effort. This documentation can be consumed by API clients, frontend developers, and automated tools for code generation and testing.

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

To get started with DRF Spectacular, you need to install it first:

Terminal window
uv add drf-spectacular

Once you have DRF Spectacular installed, you can configure it 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 enhanced schema generator instead of the default. This allows Spectacular to deeply inspect your viewsets, serializers, and authentication to generate a complete OpenAPI specification:

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

Configure the OpenAPI schema metadata. This information appears in the API documentation header:

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 URL endpoints. These provide different ways for developers to access your documentation:

  • api/schema/ — The raw OpenAPI schema (JSON format, consumed by tools)
  • api/schema/swagger-ui/ — Interactive Swagger UI documentation (browser-friendly)
  • api/schema/redoc/ — ReDoc documentation (cleaner, alternative UI)
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) that makes Django deployment easier because you do not manage servers directly. It works well for learning, portfolio projects, and many small-to-medium production apps.

This section keeps the setup beginner-friendly, but the defaults shown here are production-aware.

The Heroku CLI lets you create apps, configure environment variables, manage add-ons, and run one-off commands.

Install it from the official docs:

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

After installation, verify and log in:

Terminal window
heroku --version
heroku login

From your Django project directory:

Terminal window
heroku create your-app-name

Heroku returns:

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

Update Django settings for production.

Set ALLOWED_HOSTS to include your Heroku domain:

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

Also make sure your project uses:

  • WhiteNoise (or another strategy) for static files
  • A production app server such as Gunicorn
  • DEBUG = False in production settings

Heroku stores app configuration in environment variables (Config Vars).

Set core values:

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

You will usually add more variables (database URL, redis URL, API keys, email credentials, etc.). Keep all secrets out of source control.

Heroku uses a Procfile to know which process types to run.

Create Procfile in your project root:

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

Notes:

  • Keep web for running the main web server.
  • Keep release if you want migrations on each deploy.
  • Keep worker only if you use Celery.

PostgreSQL is the recommended default for Django on Heroku. But you can use MySQL or other databases if you prefer.

Install parser/helper package:

Terminal window
uv add dj-database-url

Then parse the database URL in 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.

Provision 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 sets DATABASE_URL automatically. Verify with:

Terminal window
heroku config

Expected output includes:

=== 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 for MySQL on Heroku. It provides a managed MySQL database with a simple setup. It is a popular choice for Django apps that require MySQL instead of PostgreSQL.

If using JawsDB, you can either read directly from JAWSDB_URL or mirror it to DATABASE_URL for consistency.

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 provides 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 verify variables:

Terminal window
heroku config

Expected output includes either JAWSDB_URL or DATABASE_URL:

=== 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 one Redis instance for both Django caching and Celery.

Provision 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 sets REDIS_URL. Verify:

Terminal window
heroku config

Output should include:

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

Use REDIS_URL for cache + 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 transactional email provider for Django apps.

Provision 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 config vars such as MAILGUN_SMTP_LOGIN, MAILGUN_SMTP_PASSWORD, MAILGUN_SMTP_PORT, and MAILGUN_SMTP_SERVER.

Verify:

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

After configuration, deploy using Git:

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

If remote is not added yet:

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

Push code:

Terminal window
git push heroku main

After deploy, it is good practice to run:

Terminal window
heroku logs --tail

This helps you quickly diagnose startup issues.

Production Terminal Access and Running Commands

Section titled “Production Terminal Access and Running Commands”

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

Terminal window
heroku run bash

This opens a shell on a one-off dyno. You can run Django management commands.

Terminal window
python manage.py createsuperuser

Use this for tasks like creating admin users, shell access, data fixes, or running commands manually.

Terminal window
exit

This exits the dyno shell and returns to your local terminal.

We can use Docker to containerize our Django application, making it easier to deploy across different environments. Docker allows you to package your application with all its dependencies into a single container, ensuring consistency and simplifying deployment.

To learn more about Dockerizing Django applications, refer docker documentation: https://docs.docker.com/reference/samples/django/

If Heroku is not the right fit, these are popular alternatives. Pricing and free tiers change over time, so always verify on the provider pricing page before deciding.

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 for the easiest start.
  • Heroku for a polished add-on ecosystem (usually paid).
  • Cloud Run if you are comfortable with Docker/container workflows.
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:

  • For Django + PostgreSQL, Neon or Supabase are usually the quickest to set up.
  • For MySQL, JawsDB is convenient on Heroku, while TiDB Cloud and other managed providers are good external options.
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 with one Redis instance for both cache and Celery.
  • Split into separate instances later if workload grows.
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 quickest setup, choose Resend, Mailgun, or SendGrid.
  • For large-scale/low-cost sending, SES is often preferred after initial setup.