Tejas Mandre | Blog

How to containerise a web app (Django + React.js) using docker and make it production ready ? 🐋

By in software

Read my previous blog on what is docker? To know docker terminologies and how things actually work

This will make your project production-ready as the project will be served through Nginx servers

This post will guide you through containerising a full-stack application built using Django, React.js/Next.js, PostgreSQL and a web server NGINX. I am going to be using Linux throughout but steps should remain the same on Windows or macOS. This post assumes you already know Nginx, Django, React and you've read my previous post explaining what docker and docker terminologies are.

To install docker follow the official documentation. On Linux, you'd need to run an extra step after installation that is "sudo apt install docker-compose".

The architecture of the app

undefined

Initial steps:

Run them one by one

mkdir docker_tutorial
cd docker_tutorial
virtualenv venv
source venv/bin/activate
pip install django gunicorn psycopg2-binary gevent
django-admin startproject backend
npx create-react-app frontend

What do these steps exactly do? 

  1. Create a new folder, navigate into it
  2. Set up a virtual environment, activate it (to install virtualenv library, run pip install virtualenv)
  3. Install Django and gunicorn (a mod python wsgi to be used with nginx reverse proxy) and pscopg2-binary for connecting with PostgreSQL
  4. Create a new Django Project
  5. Create a new React.js / Next.js Project (assuming you have node and npm installed)
  6. For Nginx and PostgreSQL we'll be using pre-built images from the docker hub to make our lives easier

If you want to learn more about Nginx and gunicorn check out digital ocean articles.

After this, your folder should contain three folders backend, frontend and venv. Check by running the "ls" command.

The next step is to write individual docker files inside backend and frontend. Dockerfiles tell the process which commands to run during build.

Navigate to the backend folder that is the Django app and create a requirement.txt file

cd backend
pip freeze > requirements.txt

Now the simple way to spin up the server is by running python manage.py runserver .  If we have any migrations then python manage.py makemigrations and python manage.py migrate . We need to the same inside the container that we are going to spin up using Docker.

Create a new file called Dockerfile . This file has no extensions. Inside here we'll write the steps to copy our app inside the container and run it.

# docker_tutorial/backend/Dockerfile

FROM python:3.8
ENV PYTHONDONTWRITEBYTECODE 1
ENV PYTHONUNBUFFERED 1
COPY . /app
WORKDIR /app
RUN pip3 install -r requirements.txt
RUN python manage.py makemigrations
RUN python manage.py migrate
CMD gunicorn -b 0.0.0.0:8000 --worker-class=gevent --worker-connections=1000 --workers=5 backend.wsgi

This file simply uses the python image from docker hub, copies our app into the container in a folder named app, installs dependencies, runs migrations and spins up the gunicorn server.

Next, we have to do the same with our frontend react.js app. Navigate to the frontend folder and create a Dockerfile in it. Paste the following inside the Dockerfile.

# docker_tutorial/frontend/Dockerfile

FROM node:14-alpine AS builder

WORKDIR /opt/web
COPY . ./
RUN npm install

ENV PATH="./node_modules/.bin:$PATH"

RUN npm run build

FROM nginx:1.20.1-alpine
RUN apk --no-cache add curl
RUN curl -L https://github.com/a8m/envsubst/releases/download/v1.1.0/envsubst-`uname -s`-`uname -m` -o envsubst && \
    chmod +x envsubst && \
    mv envsubst /usr/local/bin
COPY ./nginx.config /etc/nginx/nginx.template
CMD ["/bin/sh", "-c", "envsubst < /etc/nginx/nginx.template > /etc/nginx/conf.d/default.conf && nginx -g 'daemon off;'"]
COPY --from=builder /opt/web/build /usr/share/nginx/html

This will get the node:alpine image from docker hub which has npm preinstalled, copy our app inside it, builds the image. After this, it looks for nginx image and copies the nginx.config file inside the container.

Paste the following contents in ngin.config file

# docker_tutorial/frontend/nginx.config

server {
    listen       ${PORT:-80};
    server_name  _;

    root /usr/share/nginx/html;
    index index.html;

    location / {
        try_files $$uri /index.html;
    }
}

Congratulations on making it here, we now have frontend and backend apps running in their own containers.

The next step is to configure the Nginx server to serve our Django and React app. Navigate to the docker_tutorial directory and create a new folder called nginx_backend_server and create a Dockerfile inside it. Paste the following contents inside of it.

# docker_tutorial/nginx_backend_server/Dockerfile

FROM nginx:1.20.1-alpine
RUN rm /etc/nginx/conf.d/default.conf
COPY nginx.conf /etc/nginx/conf.d

This will get the nginx image from docker hub and copy our custom nginx.conf inside the container. Thus, we'll need to create a nginx.conf now in the same place and paste the following contents inside it.

# docker_tutorial/nginx_backend_server/nginx.conf

server {

    listen 8000;

    location / {
        proxy_pass http://backend:8000;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header Host $host;
        proxy_redirect off;
    }

}

 This configuration is a reverse proxy to the backend container running gunicorn server and we can access the backend server simply using http://backend. This is how Docker containers communicate with each other. We'll also need to keep the same service name in the docker-compose file which we'll add later. The backend server will this way run on port 8000 of host machine.

The final step is configuring the database and putting the credentials in the settings.py of the Django project, which we'll be doing inside the docker-compose.yml file. Navigate to the docker_tutorial directory and create a new docker-compose.yml. Paste the following contents inside of it.

# docker_tutorial/docker-compose.yml

version: "3"
services:

  database:
    image: postgres:12.7-alpine
    volumes:
        - ./backup_data/db:/var/lib/postgresql/data
    environment:
        - POSTGRES_DB=postgres
        - POSTGRES_USER=postgres
        - POSTGRES_PASSWORD=postgres

  backend:
    build: ./backend
    volumes:
      - ./backend:/app
    depends_on:
      - database
  
  frontend:
    build: ./frontend
    volumes:
      - ./frontend:/app
    depends_on:
      - backend
    ports:
      - 80:80
  
  nginx_backend_server:
    build: ./nginx_backend_server
    ports:
        - 8000:8000
    depends_on:
        - backend

To get an in-depth understanding of this file watch my video on youtube by clicking here since it'd be very long to type it out.

Now run "docker-compose up" on the terminal at the docker_tutorial directory level. Images should start building and containers should start running. If this was successful go to the settings.py file in your Django project and change the database configuration the following

DATABASES = {
    'default': {
        'ENGINE': 'django.db.backends.postgresql',
        'NAME': 'postgres',
        'USER': 'postgres',
        'PASSWORD': 'postgres',
        'HOST': 'database',
        'PORT': 5432,
    }
}

The HOST should be the name of the service given in docker-compose for docker to know about the database. Watch my YouTube video on this to get a full in-depth understanding of everything written in this post.

The process should now reload and Tadaaaa !! You have a dockerized production-ready full-stack application. Go to http://localhost:80 to see the react.js app and localhost:8000 to see the Django app. Both are served using Nginx.

See you next time! Kudos!