How We Deployed a React + Django Application: A Step-by-Step Guide

Series -

In the previous article, we set up a developer environment for building applications using React and Django. In this article, we will share our experience of deploying a Django application using Docker containers, outlining key steps and best practices to achieve a production environment.

In recent years, the use of asynchronous Django applications (ASGI) has become popular due to their high performance and ability to handle a large number of connections. We will be deploying a Django ASGI application using Nginx and an SSL certificate from Let’s Encrypt.

We will deploy the application from the previous article. Let’s configure Nginx for your application. Create a file named your_domain.conf in the frontend/nginx directory:

nginx

server {
    listen      80;
    listen [::]:80;
    gzip on;
    gzip_types text/plain text/css text/js text/xml text/javascript application/javascript application/json
        application/xml application/rss+xml image/svg+xml;

    server_name your_domain.com;

    # Specify the location of your React project in this container. It is set in the Dockerfile
    location / {
        root   /usr/share/nginx/html;
        index  index.html index.htm;
    }

    # Proxy all API requests to the Django server in another container
    # The backend container name is specified in the docker-compose.yml file
    location /api/ {
        proxy_pass http://backend:8000;
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "upgrade";
        proxy_set_header Host $host;
        proxy_cache_bypass $http_upgrade;
    }

    # This directory is needed to obtain the certificate from Let's Encrypt
    location ~ /.well-known/acme-challenge {
        allow all;
        root /var/cert;
    }
}

To deploy the React application, you can build your project using the command npm run build. This will result in a build directory that can be connected to the Nginx container. Alternatively, you can create a new Nginx image and include the React application in it. To do this, create the following Dockerfile in the frontend application directory:

dockerfile

# Build stage
FROM node:current AS build

# Set the working directory
WORKDIR /app

# Copy all application files
COPY . .

# Install dependencies
RUN npm install

# Create the application build
RUN npm run build


# Deployment stage
FROM nginx

# Copy Nginx configuration
COPY ./nginx/* /etc/nginx/conf.d

# Copy build files from the previous stage
# If using the Vite package builder, it creates the application in the dist directory
#COPY --from=build /app/dist /usr/share/nginx/html
COPY --from=build /app/build /usr/share/nginx/html

We will use Daphne as the application server. It is not included in requirements.txt, so we will install it separately. In the root of the backend project, create a Dockerfile with the following content:

dockerfile

# Use the latest official Python image as the base
FROM python:3

# Set the working directory
WORKDIR /app

# Copy all project files into the container
COPY . .

# Install dependencies
RUN pip install --no-cache-dir -r requirements.txt daphne

# Run migrations and collect static files
RUN python manage.py migrate
RUN python manage.py collectstatic --noinput

# Specify the command to run the application
CMD ["daphne", "-b", "0.0.0.0", "-p", "8000", "backend.asgi:application"]

In the root of our project, create a docker-compose.yml file with the following content:

yaml

services:
  web:
    build: ./frontend
    ports:
      - "80:80"
    depends_on:
      - backend

  backend:
    build: ./backend

At this stage, we can already start our containers and check their operation.

shell

docker compose up -d

If you now navigate to your domain name, your application should open.

To do this, we will run a separate container that will periodically check for certificates and request new ones. It needs to share a storage volume with the Nginx container. Specifically, there should be two such volumes: one for storing certificates and another for passing the verification for issuing the certificate. Let’s modify our docker-compose.yml file as follows:

yaml

services:
  web:
    build: ./frontend
    volumes:
       - certbot-etc:/etc/letsencrypt
       - certbot-var:/var/cert
    ports:
      - "80:80"
      - "443:443"
    command: "/bin/sh -c 'while :; do sleep 12h & wait $${!}; nginx -s reload; done & nginx -g \"daemon off;\"'"

  backend:
    build: ./backend
    volumes:
      - .:/app

  certbot:
     image: certbot/certbot
     volumes:
       - certbot-etc:/etc/letsencrypt
       - certbot-var:/var/cert
     depends_on:
       - web
     entrypoint: "/bin/sh -c 'trap exit TERM; while :; do certbot renew; sleep 12h & wait $${!}; done;'"

volumes:
  certbot-etc:
  certbot-var:

Now, restart the containers and try to obtain a new certificate. Don’t forget to substitute your domain name and email address. Also, for testing, run with the --staging option, as Let’s Encrypt has very low limits on attempts, and in case of failures, you will have to wait an hour. Once everything starts working without errors, simply remove this option or change it to --force to renew a certificate that has not yet expired.

shell

docker compose up -d
docker compose run --entrypoint "certbot certonly --webroot --webroot-path=/var/cert \
    --email you@email --agree-tos --no-eff-email --staging -d your_domain.com \
    -d www.your_domain.com" certbot

Now we need to reconfigure Nginx to work with the obtained certificate.
Let’s modify the your_domain.conf file in the frontend/nginx directory as follows:

nginx

# Redirect to https
server {
    listen      80;
    listen [::]:80;
    access_log off;

    server_name your_domain.com www.your_domain.com;

    return 301 https://your_domain.com$request_uri;
}

# Redirect from the www subdomain to the non-www version, or vice versa, depending on your preference.
server {
    listen      443 ssl;
    listen [::]:443 ssl;
    access_log off;

    server_name www.your_domain.com;
    ssl_certificate /etc/letsencrypt/live/your_domain.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/your_domain.com/privkey.pem;
    include conf.d/common_params;

    return 301 https://your_domain.com$request_uri;
}

# Main block for handling requests
server {
    listen      443 ssl;
    listen [::]:443 ssl;
    http2 on;

    server_name your_domain.com;

    ssl_certificate /etc/letsencrypt/live/your_domain.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/your_domain.com/privkey.pem;
    include conf.d/common_params;

    # Specify the location of your React project in this container. It is set in the Dockerfile
    location / {
        root   /usr/share/nginx/html;
        index  index.html index.htm;
    }

    # Proxy all API requests to the Django server in another container
    # The backend container name is specified in the docker-compose.yml file
    location /api/ {
        proxy_pass http://backend:8000;
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "upgrade";
        proxy_set_header Host $host;
        proxy_cache_bypass $http_upgrade;
    }

    # This directory is needed to obtain the certificate from Let's Encrypt
    location ~ /.well-known/acme-challenge {
        allow all;
        root /var/cert;
    }
}

We will also add a common_params file in this directory with common parameters for most websites:

nginx

gzip on;
gzip_types text/plain text/css text/js text/xml text/javascript application/javascript application/json
    application/xml application/rss+xml image/svg+xml;
server_tokens off;

ssl_buffer_size 4k;
ssl_session_cache shared:TLS:2m;

# You can generate a random dhparam file using the following command:
# openssl dhparam 4096 -out /etc/nginx/conf.d/dhparam.pem
ssl_dhparam /etc/nginx/conf.d/dhparam.pem;

ssl_protocols TLSv1.3 TLSv1.2;
ssl_prefer_server_ciphers on;

ssl_ecdh_curve secp521r1:secp384r1;
ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384;
ssl_stapling on;
ssl_stapling_verify on;
resolver 1.1.1.1 1.0.0.1 [2606:4700:4700::1111] [2606:4700:4700::1001]; # Cloudflare

# Set HSTS to 365 days
add_header Strict-Transport-Security 'max-age=31536000; includeSubDomains; preload' always;

Now, let’s rebuild our container with the command docker compose build web and start it with docker compose up -d. If everything is done correctly, we should see a working server through an HTTPS connection with automatic certificate renewal.

Related Content