How We Deployed a React + Django Application: A Step-by-Step Guide
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.
Setting Up Nginx
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:
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;
}
}
Creating a Container for the React Application and Nginx
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:
# 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
Creating a Container for Our Django Application
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:
# 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"]
Setting Up Multiple Containers with Docker Compose
In the root of our project, create a docker-compose.yml
file with the following content:
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.
docker compose up -d
If you now navigate to your domain name, your application should open.
Setting Up Let’s Encrypt
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:
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.
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:
# 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:
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
If you are interested in this topic, please contact us.