Approach 1: Single settings file with conditions
Uses just one settings.py file, and switches its behavior based on environment variables.
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 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 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
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=TrueSECRET_KEY=your-secret-keyDJANGO_SETTINGS_MODULE=myproject.settings.developmentLoad the variables into your project like this:
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 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.
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.pyStep-by-step setup:
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 = 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 exactly which settings file Django loads.
DJANGO_SETTINGS_MODULE=myproject.settings.developmentDJANGO_SETTINGS_MODULE=myproject.settings.productionIf 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 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 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:
uv add gunicornThen run your Django app with Gunicorn:
gunicorn myproject.wsgi:application# orgunicorn myproject.wsgi:application --bind 0.0.0.0:8000Waitress 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:
uv add waitressThen run your Django app with Waitress:
waitress-serve --port=8000 myproject.wsgi:application# orwaitress-serve --listen=0.0.0.0:8000 myproject.wsgi:applicationUvicorn 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:
uv add uvicornThen run your Django app with Uvicorn:
uvicorn myproject.asgi:application --host=0.0.0.0 --port=8000DRF 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:
uv add drf-spectacularOnce 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:
heroku --versionheroku loginFrom inside your Django project folder, run:
heroku create your-app-nameHeroku will give you back:
https://your-app-name.herokuapp.com/)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:
DEBUG = False in your production settingsHeroku keeps your app’s configuration stored as environment variables, which it calls “Config Vars.”
Set the core ones like this:
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 migrateweb: gunicorn myproject.wsgi:applicationworker: celery -A myproject workerNotes:
web line, since it runs your main web server.release line if you want migrations to run automatically on every deploy.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:
uv add dj-database-urlThen 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:
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 will automatically set DATABASE_URL for you. You can check it like this:
heroku configYou should see output like this:
=== 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 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.
heroku addons:create jawsdb:<plan># example plan: kitefin (check current plans in your region/account)# docs: https://elements.heroku.com/addons/jawsdbJawsDB 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, )}If you’d rather use one consistent variable name (DATABASE_URL) across all your environments, you can copy JAWSDB_URL over to DATABASE_URL.
Then you can keep your settings simple:
import dj_database_url
DATABASES = { "default": dj_database_url.config( conn_max_age=600, ssl_require=True, )}Set DATABASE_URL using the value from JAWSDB_URL:
heroku config:set DATABASE_URL=$(heroku config:get JAWSDB_URL)To check your variables:
heroku configYou should see either JAWSDB_URL or DATABASE_URL in the output:
=== 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 a single Redis instance for both Django caching and Celery, all at once.
Set up 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 will set REDIS_URL for you automatically. Check it like this:
heroku configYou should see something like this in the output:
=== your-app-name Config VarsREDIS_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 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 choice for sending transactional emails (like password resets or order confirmations) from Django apps.
Set up the 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 up config variables like MAILGUN_SMTP_LOGIN, MAILGUN_SMTP_PASSWORD, MAILGUN_SMTP_PORT, and MAILGUN_SMTP_SERVER.
Check them like this:
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 = TrueOnce everything is configured, you can deploy using Git:
git add .git commit -m "Prepare for Heroku deployment"If you haven’t added the Heroku remote yet:
git remote add heroku https://git.heroku.com/your-app-name.gitPush your code:
git push heroku mainAfter deploying, it’s a good habit to run:
heroku logs --tailThis helps you quickly spot and fix any startup problems.
You can run one-off commands directly on Heroku for admin tasks.
heroku run bashThis 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.
python manage.py createsuperuserYou can use this terminal for things like creating an admin user, getting shell access, fixing data, or running other commands by hand.
exitThis 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.
| 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: