← Back to all blogs
Django Production Setup – A Complete Step‑by‑Step Tutorial
Sat Feb 28 20267 minIntermediate

Django Production Setup – A Complete Step‑by‑Step Tutorial

A professional, SEO‑optimized tutorial that walks you through every stage of preparing a Django project for production, from environment preparation to scaling with Docker and CI/CD pipelines.

#django#production deployment#gunicorn#nginx#docker#ci/cd#security#web architecture

Introduction

<h2>Why a Proper Production Setup Matters</h2> <p>Running Django in development mode is convenient, but it lacks the performance, security, and reliability required for real users. A production‑grade deployment isolates the application, enforces best‑practice security configurations, and provides a scalable stack that can handle traffic spikes.</p> <p>This tutorial targets developers who have a functional Django project and want to transition it to a robust, maintainable production environment. We will cover the complete workflow, including environment variables, static file handling, process management with <code>Gunicorn</code>, reverse proxy configuration with <code>Nginx</code>, Docker containerization, and an optional CI/CD pipeline.</p>

Preparing the Environment

<h2>1. Structuring the Project for Production</h2> <p>Before writing any configuration files, organize your repository so that production‑specific assets are separated from development utilities.</p> <h3>Directory Layout</h3> <pre><code>myproject/ ├─ myproject/ # Django project package │ ├─ __init__.py │ ├─ settings.py # Base settings (common for all environments) │ ├─ urls.py │ └─ wsgi.py ├─ app/ # Your application code ├─ static/ # Collected static files (generated) ├─ media/ # User‑uploaded media ├─ Dockerfile ├─ nginx/ │ └─ myproject.conf ├─ gunicorn_config.py ├─ .env.example └─ requirements.txt </code></pre> <h3>2. Managing Secrets with Environment Variables</h3> <p>Never hard‑code secret keys, database credentials, or API tokens. Use a <code>.env</code> file (excluded from version control) and a library such as <code>python‑decouple</code> or <code>django-environ</code> to load them.</p> <pre><code># .env.example DEBUG=False SECRET_KEY=replace-with-strong-secret DATABASE_URL=postgres://user:password@db:5432/mydb ALLOWED_HOSTS=yourdomain.com,www.yourdomain.com </code></pre> <p>In <code>settings.py</code>:</p> <pre><code>import environ, os env = environ.Env() BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))

Load .env only in development; production reads from OS env

if os.getenv('ENV') != 'production': environ.Env.read_env(os.path.join(BASE_DIR, '.env'))

SECRET_KEY = env('SECRET_KEY') DEBUG = env.bool('DEBUG', default=False) ALLOWED_HOSTS = env.list('ALLOWED_HOSTS') </code></pre>

<h3>3. Configuring the Database</h3> <p>Leverage <code>dj‑database‑url</code> to parse the database URL defined in <code>.env</code>:</p> <pre><code>import dj_database_url DATABASES = { 'default': dj_database_url.config(conn_max_age=600) } </code></pre>

Configuring Django for Production

<h2>2. Production‑Ready Django Settings</h2> <p>Beyond secrets, several Django settings must be tuned for a live environment.</p> <h3>Static and Media Files</h3> <p>Collect static assets into a single directory and let <code>Nginx</code> serve them directly.</p> <pre><code># settings.py (continued) STATIC_URL = '/static/' STATIC_ROOT = os.path.join(BASE_DIR, 'static') MEDIA_URL = '/media/' MEDIA_ROOT = os.path.join(BASE_DIR, 'media') </code></pre> <p>Run the collectstatic command during the Docker image build:</p> <pre><code>python manage.py collectstatic --noinput </code></pre> <h3>Security Middleware</h3> <p>Activate Django’s security middleware to protect against common web attacks.</p> <pre><code># settings.py MIDDLEWARE = [ 'django.middleware.security.SecurityMiddleware', 'django.contrib.sessions.middleware.SessionMiddleware', 'django.middleware.common.CommonMiddleware', 'django.middleware.csrf.CsrfViewMiddleware', 'django.contrib.auth.middleware.AuthenticationMiddleware', 'django.contrib.messages.middleware.MessageMiddleware', 'django.middleware.clickjacking.XFrameOptionsMiddleware', ]

SECURE_SSL_REDIRECT = True # Force HTTPS SESSION_COOKIE_SECURE = True CSRF_COOKIE_SECURE = True SECURE_HSTS_SECONDS = 31536000 # 1 year SECURE_HSTS_INCLUDE_SUBDOMAINS = True SECURE_HSTS_PRELOAD = True </code></pre>

<h3>Logging Configuration</h3> <p>Capture errors in a file and optionally forward them to external services.</p> <pre><code># settings.py LOGGING = { 'version': 1, 'disable_existing_loggers': False, 'formatters': { 'verbose': { 'format': '[%(asctime)s] %(levelname)s %(name)s %(message)s' }, }, 'handlers': { 'file': { 'level': 'ERROR', 'class': 'logging.FileHandler', 'filename': os.path.join(BASE_DIR, 'logs/error.log'), 'formatter': 'verbose', }, }, 'loggers': { 'django': { 'handlers': ['file'], 'level': 'ERROR', 'propagate': True, }, }, } </code></pre>

Setting Up Gunicorn and Nginx

<h2>3. Process Management and Reverse Proxy</h2> <p>Gunicorn runs the Django application as a WSGI server, while Nginx handles static files, SSL termination, and load balancing.</p> <h3>Gunicorn Configuration File</h3> <pre><code># gunicorn_config.py import multiprocessing

bind = '0.0.0.0:8000' workers = multiprocessing.cpu_count() * 2 + 1 loglevel = 'info' accesslog = '-' errorlog = '-' </code></pre>

<h3>Running Gunicorn in Docker</h3> <pre><code># Dockerfile (excerpt) FROM python:3.11-slim WORKDIR /app COPY requirements.txt ./ RUN pip install --no-cache-dir -r requirements.txt COPY . . RUN python manage.py collectstatic --noinput EXPOSE 8000 CMD ["gunicorn", "myproject.wsgi:application", "-c", "gunicorn_config.py"] </code></pre> <h3>Nginx Reverse‑Proxy Configuration</h3> <pre><code># nginx/myproject.conf upstream django { server 127.0.0.1:8000; }

server { listen 80; server_name yourdomain.com www.yourdomain.com;

# Redirect HTTP → HTTPS (optional if you terminate SSL elsewhere)
return 301 https://$host$request_uri;

}

server { listen 443 ssl; server_name yourdomain.com www.yourdomain.com;

ssl_certificate /etc/ssl/certs/fullchain.pem;
ssl_certificate_key /etc/ssl/private/privkey.pem;
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers HIGH:!aNULL:!MD5;

location /static/ {
    alias /app/static/;      # Path from Docker container
}
location /media/ {
    alias /app/media/;
}
location / {
    proxy_set_header Host $host;
    proxy_set_header X-Real-IP $remote_addr;
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    proxy_set_header X-Forwarded-Proto $scheme;
    proxy_pass http://django;
}

} </code></pre>

<h3>Docker‑Compose Example</h3> <p>Combine Django, Gunicorn, and Nginx in a single <code>docker‑compose.yml</code> file for local testing before deploying to a cloud service.</p> <pre><code># docker-compose.yml version: '3.8' services: web: build: . container_name: django_app env_file: .env volumes: - .:/app expose: - "8000" nginx: image: nginx:stable-alpine container_name: nginx_proxy ports: - "80:80" - "443:443" volumes: - ./nginx:/etc/nginx/conf.d - static_volume:/app/static - media_volume:/app/media depends_on: - web volumes: static_volume: media_volume: </code></pre>

Deploying with Docker and CI/CD

<h2>4. Automated Deployment Pipeline</h2> <p>Manual builds are error‑prone. Integrate Docker image creation and deployment into a CI/CD workflow (GitHub Actions, GitLab CI, or Bitbucket Pipelines). The example below uses GitHub Actions.</p> <h3>GitHub Actions Workflow</h3> <pre><code># .github/workflows/deploy.yml name: Deploy Django to Production on: push: branches: [ main ]

jobs: build-and-deploy: runs-on: ubuntu-latest steps: - name: Checkout repository uses: actions/checkout@v3

  - name: Set up Docker Buildx
    uses: docker/setup-buildx-action@v2

  - name: Log in to Docker Hub
    uses: docker/login-action@v2
    with:
      username: ${{ secrets.DOCKER_USERNAME }}
      password: ${{ secrets.DOCKER_PASSWORD }}

  - name: Build and push image
    uses: docker/build-push-action@v4
    with:
      context: .
      push: true
      tags: ${{ secrets.DOCKER_USERNAME }}/myproject:latest

  - name: Deploy to server via SSH
    uses: appleboy/ssh-action@v0.1.7
    with:
      host: ${{ secrets.SERVER_IP }}
      username: ${{ secrets.SERVER_USER }}
      key: ${{ secrets.SSH_PRIVATE_KEY }}
      script: |
        docker pull ${{ secrets.DOCKER_USERNAME }}/myproject:latest
        docker compose -f /opt/myproject/docker-compose.yml up -d --remove-orphans

</code></pre>

<h3>Benefits of Containerization</h3> <ul> <li><strong>Isolation:</strong> Each component runs in its own container, preventing dependency clashes.</li> <li><strong>Scalability:</strong> Horizontal scaling is as simple as increasing the replica count in the compose file or orchestrator.</li> <li><strong>Portability:</strong> The same image runs locally, on a VPS, or in managed Kubernetes.</li> </ul> <p>When you push a new tag to the repository, the pipeline rebuilds the Docker image, pushes it to a registry, and then pulls the latest version on the production host, ensuring zero‑downtime deployments when combined with a rolling‑update strategy.</p>

FAQs

<h2>Frequently Asked Questions</h2> <h3>1️⃣ Do I need to use Nginx if I deploy Django on a PaaS like Heroku or Render?</h3> <p>Most PaaS providers already include a reverse proxy layer that handles SSL termination and static assets. In those environments you can skip Nginx, but you still need to configure Django’s <code>SECURE_*</code> settings and <code>ALLOWED_HOSTS</code> correctly.</p> <h3>2️⃣ How can I enable automatic HTTPS certificates?</h3> <p>When you own the domain, use <a href="https://letsencrypt.org/" target="_blank">Let’s Encrypt</a> together with <code>certbot</code> inside the Nginx container or on the host. The classic command is <code>certbot --nginx -d yourdomain.com -d www.yourdomain.com</code>. Remember to schedule renewal via a cron job.</p> <h3>3️⃣ What is the recommended number of Gunicorn workers?</h3> <p>The general formula <code>workers = (CPU cores * 2) + 1</code> works well for most I/O‑bound Django apps. Adjust upward if you profile high CPU usage, or lower the count if memory becomes a bottleneck.</p> <h3>4️⃣ Should I store static files in a CDN?</h3> <p>For global latency reduction, serving <code>/static/</code> and <code>/media/</code> through a CDN (e.g., CloudFront, Cloudflare) is recommended. The Django <code>collectstatic</code> command can push files directly to S3 using <code>django‑storages</code>.</p> <h3>5️⃣ Is Docker mandatory for production?</h3> <p>No, you can run Gunicorn and Nginx directly on a VM. Docker simplifies dependency management and replicability, but traditional systemd services are also viable for smaller deployments.</p>

Conclusion

<h2>Wrapping Up the Django Production Setup</h2> <p>Transitioning a Django project from local development to a production‑grade deployment involves a series of deliberate steps: secure configuration, proper static/media handling, a robust WSGI server (Gunicorn), a high‑performance reverse proxy (Nginx), and optional containerization for portability. By following this tutorial, you now have a repeatable workflow that can be automated through CI/CD pipelines, providing faster releases and reduced human error.</p> <p>Remember to monitor your application with tools such as Prometheus, Grafana, or hosted services like Sentry. Continuous monitoring, combined with the security hardening outlined above, ensures that your Django service remains performant and safe as traffic grows.</p> <p>Happy coding, and enjoy the confidence that comes with a well‑engineered production environment!</p>