Setting Up Nginx as a Reverse Proxy for a Django App in Docker on Ubuntu

Configuring Nginx on Ubuntu to reverse proxy a Django application running inside a Docker container might sound complex, but it’s a straightforward process once you break it down. In this guide, I’ll walk you through each step, assuming you’re familiar with Ubuntu, Nginx, Docker, and Django basics. Our goal? Set up Nginx to seamlessly forward HTTP requests to a Django app powered by a WSGI server (like Gunicorn) inside a Docker container. Let’s dive in!

Table of Contents


Prerequisites

Before we get started, make sure you have the following in place:

  1. Ubuntu Server: A system running Ubuntu (22.04, or later) with admin access.
  2. Docker: Installed and ready on your Ubuntu machine.
  3. Django App in a Docker Container: A Django app running inside Docker, served by Gunicorn or a similar WSGI server.
  4. Nginx: Installed on the host Ubuntu system (we’ll cover this step).
  5. Domain or IP: A domain name (optional) or your server’s public IP to access the app.

Got everything? Great—let’s set it up!


Set Up Your Django App in a Docker Container

First, we need a Django app running inside a Docker container. Assume you have a Django application in a folder.

  • Define Dependencies in requirements.txt:
django>=5.1.7,<5.2
gunicorn==23.0.0
mysqlclient>=2.2.7,<2.3
  • Create a Dockerfile:
# Use the official Python runtime image
FROM python:3.13

# Create the app directory
RUN mkdir /app

# Set the working directory inside the container
WORKDIR /app

# Set environment variables 
# Prevents Python from writing pyc files to disk
ENV PYTHONDONTWRITEBYTECODE=1
#Prevents Python from buffering stdout and stderr
ENV PYTHONUNBUFFERED=1

# Install system dependencies (gcc, mysql-client for MySQL database connection, curl for Node.js)
RUN apt update && apt install -y \
    gcc \
    default-libmysqlclient-dev \
    && rm -rf /var/lib/apt/lists/*

# Upgrade pip
RUN pip install --upgrade pip 

# Copy the Django project and install dependencies
COPY requirements.txt  /app/

# run this command to install all dependencies 
RUN pip install --no-cache-dir -r requirements.txt

# Copy the Django project to the container
COPY . /app/

# Expose the Django port
EXPOSE 8000

# Start the application using Gunicorn
CMD ["gunicorn", "--bind", "0.0.0.0:8000", "--workers", "3", "my-django-app.wsgi:application"]
  • Create a docker compose file
services:
  django-app:
    build:
      context: .
      dockerfile: Dockerfile
    container_name: django-app
    restart: always
    volumes:
      - ./staticfiles:/app/staticfiles
    ports:
      - "8000:8000"
    environment:
      - DB_HOST=db # Reference environment variable from .env file
      - DB_NAME=${DB_NAME}
      - DB_USER=${DB_USER}
      - DB_PASSWORD=${DB_PASSWORD}
      - SECRET_KEY=${SECRET_KEY}
      - DEBUG=${DEBUG}
      - DJANGO_ALLOWED_HOSTS=${DJANGO_ALLOWED_HOSTS}
      - CSRF_TRUSTED_ORIGINS=${CSRF_TRUSTED_ORIGINS}
    depends_on:
      - db # Make sure the database service starts first
    env_file:
      - .env
    command: >
      sh -c "python manage.py wait_for_db &&
             python manage.py migrate &&
             python manage.py collectstatic --noinput &&
             gunicorn --bind 0.0.0.0:8000 blogproject.wsgi:application"
  db:
    image: mysql:8.0.41
    volumes:
      - mysql_db:/var/lib/mysql # Persist the database data
    environment:
      - MYSQL_ROOT_PASSWORD=${MYSQL_ROOT_PASSWORD} # Use the root password from .env file
      - MYSQL_DATABASE=${DB_NAME} # The database name from .env file
    ports:
      - "3306:3306" # MySQL default port
    env_file:
      - .env

volumes:
  mysql_db: # Named volume for MySQL data persistence
  • Create .env file
DB_NAME=mydb
DB_USER=db_user
DB_PASSWORD=db_password
MYSQL_ROOT_PASSWORD=db_root_password
SECRET_KEY=secret_key
DEBUG=False
DJANGO_ALLOWED_HOSTS=localhost,127.0.0.1
CSRF_TRUSTED_ORIGINS=http://localhost:8000,http://127.0.0.1:8000
  • Update settings.py

To use environment variables from .env file

SECRET_KEY = os.environ.get("SECRET_KEY")
DEBUG = bool(os.environ.get("DEBUG", default=0))
ALLOWED_HOSTS = os.environ.get("DJANGO_ALLOWED_HOSTS","127.0.0.1").split(",")
CSRF_TRUSTED_ORIGINS = os.environ.get("CSRF_TRUSTED_ORIGINS").split(",")

To serve static file correctly

STATIC_URL = '/static/'

STATICFILES_DIRS = [
    BASE_DIR / "static"
]

STATIC_ROOT = BASE_DIR / 'staticfiles'
  • Build and Run:
docker compose up -d --build

This spins up your Django app on port 8000 inside the container, mapped to the same port on the host. Test it by running curl http://localhost:8000.


Install Nginx on the Ubuntu Host

Next, let’s install Nginx on the host system—it’ll act as our reverse proxy, sitting outside the Docker container.

  • Update your package index and install Nginx:
sudo apt update
sudo apt install nginx
  • Check its status:
sudo systemctl status nginx

If it says active (running), you’re good.

  • Open port 80 on nginx
sudo ufw app list
sudo ufw allow 'Nginx HTTP'
sudo ufw status

Open http://<server-ip> in your browser—you should see Nginx’s default welcome page.


Configure Nginx as a Reverse Proxy

Now, let’s configure Nginx to forward incoming requests to your Django app.

  • Create a Config File:

Open a new file in /etc/nginx/sites-available/:

sudo nano /etc/nginx/sites-available/my-django-app
  • Add the Configuration: Paste this, tweaking as needed:
server {
    listen 80;
    server_name your-domain.com www.your-domain.com;

    location / {
        proxy_pass http://127.0.0.1:8000;
        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;
    }

    location /static/ {
        alias /var/www/my-django-app/static;
    }
}

Here’s what’s happening:

  • listen 80: Handles HTTP traffic.
  • server_name: Your domain or IP (use _ if no domain).
  • proxy_pass: Sends requests to the Django app at 127.0.0.1:8000.
  • proxy_set_header: Passes key headers so Django knows the client’s details.
  • location /static/: Serve static files.

  • Enable It: Link it to sites-enabled:

sudo ln -s /etc/nginx/sites-available/my-django-app /etc/nginx/sites-enabled/
  • Test and Restart:
sudo nginx -t
sudo systemctl restart nginx

If the test passes (syntax is ok), Nginx is ready to proxy requests!


Handle Static Files

For performance, Nginx should serve Django’s static and media files directly, not through the app. When running docker compose up, docker runs python manage.py collectstatic --noinput automatically and collect static files in staticfiles folder. We need to copy them to a host directory (e.g., /var/www/my-django-app/static/):

sudo docker cp ~/django-app/staticfiles /var/www/my-django-app/static
  • Set Permissions:
sudo chown -R www-data:www-data /var/www/my-django-app/static
sudo chmod -R 755 /var/www/my-django-app/static

Now Nginx handles these files efficiently.


Enable HTTPS using Certbot

For production, set up HTTPS using Certbot

sudo snap install core; sudo snap refresh core
sudo snap install --classic certbot
sudo ln -s /snap/bin/certbot /usr/bin/certbot
sudo ufw status
sudo ufw allow 'Nginx Full'
sudo ufw delete allow 'Nginx HTTP'
sudo ufw status

sudo certbot --nginx -d example.com -d www.example.com
sudo systemctl status snap.certbot.renew.service

This setup gives you a robust foundation for running Django with Nginx on Ubuntu.