Approach 1: Single settings file with conditions
Uses one settings.py and switches behavior using environment variables.
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 APIViewfrom rest_framework.response import Responseimport 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
Using .env with python-dotenv
You can use python-dotenv to load environment variables from a .env file during development.
Example .env file:
DEBUG=TrueSECRET_KEY=your-secret-keyDJANGO_SETTINGS_MODULE=myproject.settings.developmentLoad variables in your project:
from dotenv import load_dotenvimport 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.
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.pyStep-by-step setup:
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 = TrueALLOWED_HOSTS = []
# Development-specific settings (e.g., SQLite, local email backend)Production settings (production.py):
from .base import *
DEBUG = FalseSECRET_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.developmentDJANGO_SETTINGS_MODULE=myproject.settings.productionIf 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 osimport 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()| Approach | Simplicity | Scalability | Recommended For |
|---|---|---|---|
| Single file | Very easy | Low | Beginners, small apps |
| Multiple files | Moderate | High | Professional 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:
uv add gunicornThen you can run your Django application with Gunicorn:
gunicorn myproject.wsgi:application# orgunicorn myproject.wsgi:application --bind 0.0.0.0:8000Waitress 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:
uv add waitressThen you can run your Django application with Waitress:
waitress-serve --port=8000 myproject.wsgi:application# orwaitress-serve --listen=0.0.0.0:8000 myproject.wsgi:applicationUvicorn 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:
uv add uvicornThen you can run your Django application with Uvicorn:
uvicorn myproject.asgi:application --host=0.0.0.0 --port=8000DRF 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:
uv add drf-spectacularOnce 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:
heroku --versionheroku loginFrom your Django project directory:
heroku create your-app-nameHeroku returns:
https://your-app-name.herokuapp.com/)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:
DEBUG = False in production settingsHeroku stores app configuration in environment variables (Config Vars).
Set core values:
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 migrateweb: gunicorn myproject.wsgi:applicationworker: celery -A myproject workerNotes:
web for running the main web server.release if you want migrations on each deploy.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:
uv add dj-database-urlThen 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:
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-plansHeroku sets DATABASE_URL automatically. Verify with:
heroku configExpected output includes:
=== your-app-name Config VarsDATABASE_URL: postgres://username:password@hostname:port/database_nameSECRET_KEY: your-secret-keyDJANGO_SETTINGS_MODULE: myproject.settings.productionWe 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.
heroku addons:create jawsdb:<plan># example plan: kitefin (check current plans in your region/account)# docs: https://elements.heroku.com/addons/jawsdbJawsDB 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, )}If you prefer one consistent variable name (DATABASE_URL) across environments, mirror JAWSDB_URL to DATABASE_URL.
Then keep settings simple:
import dj_database_url
DATABASES = { "default": dj_database_url.config( conn_max_age=600, ssl_require=True, )}Set DATABASE_URL from JAWSDB_URL:
heroku config:set DATABASE_URL=$(heroku config:get JAWSDB_URL)To verify variables:
heroku configExpected output includes either JAWSDB_URL or DATABASE_URL:
=== your-app-name Config VarsJAWSDB_URL: mysql://username:password@hostname:port/database_nameDATABASE_URL: mysql://username:password@hostname:port/database_nameSECRET_KEY: your-secret-keyDJANGO_SETTINGS_MODULE: myproject.settings.productionYou can use one Redis instance for both Django caching and Celery.
Provision Heroku Redis:
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/rediscloudHeroku sets REDIS_URL. Verify:
heroku configOutput should include:
=== your-app-name Config VarsREDIS_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 cachingCACHES = { "default": { "BACKEND": "django_redis.cache.RedisCache", "LOCATION": REDIS_URL, "OPTIONS": { "CLIENT_CLASS": "django_redis.client.DefaultClient", }, }}
# Configure Celery broker/result backendCELERY_BROKER_URL = REDIS_URLCELERY_RESULT_BACKEND = REDIS_URLMailgun is a common transactional email provider for Django apps.
Provision Mailgun add-on:
heroku addons:create mailgun:<plan># example plan: starter (check current plans in your region/account)# docs: https://elements.heroku.com/addons/mailgunThis sets config vars such as MAILGUN_SMTP_LOGIN, MAILGUN_SMTP_PASSWORD, MAILGUN_SMTP_PORT, and MAILGUN_SMTP_SERVER.
Verify:
heroku configExample output:
=== your-app-name Config VarsMAILGUN_PUBLIC_KEY: your-mailgun-public-keyMAILGUN_SMTP_LOGIN: your-mailgun-smtp-loginMAILGUN_SMTP_PASSWORD: your-mailgun-smtp-passwordMAILGUN_SMTP_PORT: your-mailgun-smtp-portMAILGUN_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 = TrueAfter configuration, deploy using Git:
git add .git commit -m "Prepare for Heroku deployment"If remote is not added yet:
git remote add heroku https://git.heroku.com/your-app-name.gitPush code:
git push heroku mainAfter deploy, it is good practice to run:
heroku logs --tailThis helps you quickly diagnose startup issues.
You can run one-off commands on Heroku dynos for admin tasks.
heroku run bashThis opens a shell on a one-off dyno. You can run Django management commands.
python manage.py createsuperuserUse this for tasks like creating admin users, shell access, data fixes, or running commands manually.
exitThis 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.
| Provider | What it provides | Homepage | Free plan | Paid only |
|---|---|---|---|---|
| Render | Managed web services, background workers, PostgreSQL, Redis, cron jobs | render.com | Yes (limited) | No |
| Railway | Easy app deploys, databases, private networking, templates | railway.com | Yes (trial/credits) | No |
| Fly.io | Global VM/container deploys close to users, Postgres option, private networking | fly.io | Yes (limited) | No |
| Koyeb | Serverless containers, autoscaling, global regions | koyeb.com | Yes (limited) | No |
| DigitalOcean App Platform | PaaS deploy from Git, managed DB integration, workers | digitalocean.com | No | Yes |
| PythonAnywhere | Beginner-friendly Python hosting, easy Django setup, scheduled tasks | pythonanywhere.com | Yes (limited) | No |
| Heroku | Mature Django workflow, add-ons marketplace, one-off dyno commands | heroku.com | Yes (limited) | Yes |
| AWS Elastic Beanstalk | Managed deployment orchestration on AWS infrastructure | aws.amazon.com | Yes (AWS Free Tier limits) | No |
| Azure App Service | Managed web app hosting with scaling and CI/CD integration | azure.microsoft.com | Yes (limited/trial based) | No |
| Google Cloud Run | Container-based serverless runtime with automatic scaling | cloud.google.com | Yes (always free usage tier) | No |
| Google App Engine | Managed platform for app deployment with autoscaling | cloud.google.com | Yes (limited) | No |
Recommended for beginners:
| Provider | What it provides | Homepage | Free plan | Paid only |
|---|---|---|---|---|
| Neon (PostgreSQL) | Serverless Postgres with branching and autoscaling storage/compute | neon.tech | Yes | No |
| Supabase (PostgreSQL) | Managed Postgres + auth + storage + realtime APIs | supabase.com | Yes | No |
| AWS RDS | Managed PostgreSQL/MySQL/MariaDB and more on AWS | aws.amazon.com | Yes (Free Tier constraints) | No |
| Google Cloud SQL | Managed PostgreSQL/MySQL/SQL Server on GCP | cloud.google.com | No (typically paid, trial credits possible) | Yes |
| Azure Database for PostgreSQL/MySQL | Managed relational databases on Azure | azure.microsoft.com | No (typically paid, trial credits possible) | Yes |
| Heroku Postgres | Managed Postgres tightly integrated with Heroku | elements.heroku.com | No (generally paid) | Yes |
| JawsDB (MySQL) | Managed MySQL add-on commonly used with Heroku | elements.heroku.com | Usually no (plan-dependent) | Usually yes |
| TiDB Cloud (MySQL-compatible) | Distributed SQL database with MySQL compatibility | tidbcloud.com | Yes | No |
| PlanetScale (MySQL-compatible) | Serverless MySQL-compatible platform focused on scaling/workflows | planetscale.com | No (currently paid plans) | Yes |
Beginner tip:
| Provider | What it provides | Homepage | Free plan | Paid only |
|---|---|---|---|---|
| Upstash Redis | Serverless Redis with REST and pay-per-usage model | upstash.com | Yes | No |
| Redis Cloud | Managed Redis by Redis, Inc. with multiple plans | redis.io | Yes (limited) | No |
| Heroku Key-Value Store (Heroku Redis) | Redis for Heroku apps with tight platform integration | elements.heroku.com | No (generally paid) | Yes |
| AWS ElastiCache | Managed Redis/Valkey on AWS for production workloads | aws.amazon.com | No (typically paid) | Yes |
| Azure Cache for Redis | Managed Redis on Azure with enterprise features | azure.microsoft.com | No (typically paid) | Yes |
| Google Cloud Memorystore | Managed Redis on GCP for low-latency caching | cloud.google.com | No (typically paid) | Yes |
Beginner tip:
| Provider | What it provides | Homepage | Free plan | Paid only |
|---|---|---|---|---|
| Resend | Modern transactional email API focused on developer UX | resend.com | Yes | No |
| Mailgun | Transactional email APIs + SMTP relay + analytics | mailgun.com | Yes (trial/limited) | No |
| SendGrid | Transactional and marketing email services | sendgrid.com | Yes (limited) | No |
| Postmark | Transactional email with strong deliverability focus | postmarkapp.com | No free tier (trial available) | Yes |
| Amazon SES | Cost-effective bulk and transactional email service | aws.amazon.com | Yes (conditions apply) | No |
Beginner tip: