Final code for this tutorial if you want to skip the text, or get lost with some of the references, can be found on GitHub.
Update: ohduran has created a cookiecutter template based on this tutorial if you want a quick-and-easy way to get the code.
Inspired by sports data sites like Squiggle and Matter of Stats, in building the app that houses Tipresias (my footy-tipping machine-learning model), I wanted to include a proper front-end with metrics, charts, and round-by-round tips. I already knew that I would have to dockerize the thing, because I was working with multiple packages across Python and R, and such complex dependencies are incredibly difficult to manage in a remote-server context (and impossible to run on an out-of-the-box service like Heroku) without using Docker. I could have avoided exacerbating my complexity issue by using basic Django views (i.e. static HTML templates) to build my pages, but having worked with a mishmash of ancient Rails views that had React components grafted on to add a little interactivity (then a lot of interactivity), I preferred starting out with clear separation between my frontend and backend. What's more, I wanted to focus on the machine learning, data engineering, and server-side logic (not to mention the fact that I couldn't design my way out of a wet paper bag), so my intelligent, lovely wife agreed to help me out with the frontend, and there was no way she was going to settle for coding in the context of a 10-year-old paradigm. It was going to be a modern web-app architecture, or I was going to have to pad my own divs.
The problem with combining Docker, Django, and React was that I had never set up anything like this before, and, though I ultimately figured it out, I had to piece together my solution from multiple guides/tutorials that did some aspect of what I wanted without covering the whole. In particular, the tutorials that I found tended to build static Javascript assets that Django could use in its views. This is fine for production, but working without hot-reloading (i.e. having file changes automatically restart the server, so that they are reflected in the relevant pages that are loaded in the browser) is the hair shirt of development: at first you think you can endure the mild discomfort, but the constant itching wears you down, becoming the all-consuming focus of your every waking thought, driving you to distraction, to questioning all of your choices in life. Imagine having to run a build command that takes maybe a minute every time you change so much as a single line of code. Side projects don't exactly require optimal productivity, but, unlike jobs, if they become a pain to work on, it's pretty easy to just quit.
What we're gonna do
- Create a Django app that runs inside a Docker container.
- Create a React app with the all-too-literally-named Create React App that runs inside a Docker container.
- Implement these dockerized apps as services in Docker Compose.
- Connect the frontend service to a basic backend API from which it can fetch data.
Note: This tutorial assumes working knowledge of Docker, Django, and React in order to focus on the specifics of getting these three things working together in a dev environment.
1. Create a dockerized Django app
Let's start by creating a project directory named whatever you want, then a backend
subdirectory with a requirements.txt
that just adds the django
package for now. This will allow us to install and run Django in a Docker image built with the following Dockerfile
:
# Use an official Python runtime as a parent image
FROM python:3.6
# Adding backend directory to make absolute filepaths consistent across services
WORKDIR /app/backend
# Install Python dependencies
COPY requirements.txt /app/backend
RUN pip3 install --upgrade pip -r requirements.txt
# Add the rest of the code
COPY . /app/backend
# Make port 8000 available for the app
EXPOSE 8000
# Be sure to use 0.0.0.0 for the host within the Docker container,
# otherwise the browser won't be able to find it
CMD python3 manage.py runserver 0.0.0.0:8000
In the terminal, run the following commands to build the image, create a Django project named hello_world, and run the app:
docker build -t backend:latest backend
docker run -v $PWD/backend:/app/backend backend:latest django-admin startproject hello_world .
docker run -v $PWD/backend:/app/backend -p 8000:8000 backend:latest
Note that we create a volume for the backend
directory, so the code created by startproject
will appear on our machine. The .
at the end of the create command will place all of the Django folders and files inside our backend directories instead of creating a new project directory, which can complicate managing the working directory within the Docker container.
Open your browser to localhost:8000
to verify that the app is up and running.
2. Create a dockerized Create React App (CRA) app
Although I got my start coding frontend Javascript, I found my calling working on back-end systems. So, through a combination of my own dereliction and the rapid pace of change of frontend tools and technologies, I am ill-equipped to set up a modern frontend application from scratch. I am, however, fully capable of installing a package and running a command.
Unlike with the Django app, we can't create a Docker image with a CRA app all at once, because we'll first need a Dockerfile
with node, so we can initialise the CRA app, then we'll be able to add the usual Dockerfile
commands to install dependencies. So, create a frontend
directory with a Dockerfile
that looks like the following:
# Use an official node runtime as a parent image
FROM node:8
WORKDIR /app/
# Install dependencies
# COPY package.json yarn.lock /app/
# RUN npm install
# Add rest of the client code
COPY . /app/
EXPOSE 3000
# CMD npm start
Some of the commands are currently commented out, because we don't have a few of the files referenced, but we will need these commands later. Run the following commands in the terminal to build the image, create the app, and run it:
docker build -t frontend:latest frontend
docker run -v $PWD/frontend:/app frontend:latest npx create-react-app hello-world
mv frontend/hello-world/* frontend/hello-world/.gitignore frontend/ && rmdir frontend/hello-world
docker run -v $PWD/frontend:/app -p 3000:3000 frontend:latest npm start
Note that we move the newly-created app directory's contents up to the frontend directory and remove it. Django gives us the option to do this by default, but I couldn't find anything to suggest that CRA will do anything other than create its own directory. Working around this nested structure is kind of a pain, so I find it easier to just move everything up the docker-service level and work from there. Navigate your browser to localhost:3000
to make sure the app is running. Also, you can uncomment the rest of the commands in the Dockerfile
, so that any new dependencies will be installed the next time you rebuild the image.
3. Docker-composify into services
Now that we have our two Docker images and are able to run the apps in their respective Docker containers, let's simplify the process of running them with Docker Compose. In docker-compose.yml
, we can define our two services, frontend
and backend
, and how to run them, which will allow us to consolidate the multiple docker
commands, and their multiple arguments, into much fewer docker-compose
commands. The config file looks like this:
version: "3.2"
services:
backend:
build: ./backend
volumes:
- ./backend:/app/backend
ports:
- "8000:8000"
stdin_open: true
tty: true
command: python3 manage.py runserver 0.0.0.0:8000
frontend:
build: ./frontend
volumes:
- ./frontend:/app
# One-way volume to use node_modules from inside image
- /app/node_modules
ports:
- "3000:3000"
environment:
- NODE_ENV=development
depends_on:
- backend
command: npm start
We've converted the various arguments for the docker commands into key-value pairs in the config file, and now we can run both our frontend and backend apps by just executing docker-compose up
. With that, you should be able to see them both running in parallel at localhost:8000
and localhost:3000
.
4. Connecting both ends into a single app
Of course, the purpose of this post is not to learn how to overcomplicate running independent React and Django apps just for the fun of it. We are here to build a single, integrated app with a dynamic, modern frontend that's fed with data from a robust backend API. Toward that goal, while still keeping the app as simple as possible, let's have the frontend send text to the backend, which will return a count of the number of characters in the text, which the frontend will then display.
Setting up the Django API
Let's start by creating an API route for the frontend to call. You can create a new Django app (which is kind of a sub-app/module within the Django project architecture) by running the following in the terminal:
docker-compose run --rm backend python3 manage.py startapp char_count
This gives you a new directory inside backend
called char_count
, where we can define routes and their associated logic.
We can create the API response in backend/char_count/views.py
with the following, which, as promised, will return the character count of the submitted text:
from django.http import JsonResponse
def char_count(request):
text = request.GET.get("text", "")
return JsonResponse({"count": len(text)})
Now, to make the Django project aware of our new app, we need to update INSTALLED_APPS
in backend/hello_world/settings.py
by adding "char_count.apps.CharCountConfig"
to the list. To add our count response to the available URLs, we update backend/hello_world/urls.py
with our char_count view as follows:
from django.contrib import admin
from django.urls import path
from char_count.views import char_count
urlpatterns = [
path('admin/', admin.site.urls),
path('char_count', char_count, name='char_count'),
]
Since we're changing project settings, we'll need to stop our Docker Compose processes (either ctl+c or docker-compose stop
in a separate tab) and start it again with docker-compose up
. We can now go to localhost:8000/char_count?text=hello world
and see that it has 11 characters.
Connecting React to the API
First, let's add a little more of that sweet config to make sure we don't get silent errors related to networking stuff that we'd really rather not deal with. Our Django app currently won't run on any host other than localhost
, but our React app can only access it via the Docker service name backend
(which does some magic host mapping stuff). So, we need to add "backend"
to ALLOWED_HOSTS
in backend/hello_world/settings.py
, and we add "proxy": "http://backend:8000"
to package.json
. This will allow both services to talk to each other. Also, we'll need to use the npm package axios
to make the API call, so add it to package.json
and rebuild the images with the following:
docker-compose run --rm frontend npm add axios
docker-compose down
docker-compose up --build
My frontend dev skills are, admittedly, subpar, but please keep in mind that the little component below is not a reflection of my knowledge of React (or even HTML for that matter). In the interest of simplicity, I just removed the CRA boilerplate and replaced it with an input, a button, a click handler, and a headline.
import React from 'react';
import axios from 'axios';
import './App.css';
function handleSubmit(event) {
const text = document.querySelector('#char-input').value
axios
.get(`/char_count?text=${text}`).then(({data}) => {
document.querySelector('#char-count').textContent = `${data.count} characters!`
})
.catch(err => console.log(err))
}
function App() {
return (
<div className="App">
<div>
<label htmlFor='char-input'>How many characters does</label>
<input id='char-input' type='text' />
<button onClick={handleSubmit}>have?</button>
</div>
<div>
<h3 id='char-count'></h3>
</div>
</div>
);
}
export default App;
Now, when we enter text into the input and click the button, the character count of the text is displayed below. And best of all: we got hot reloading all up and down the field! You can add new components to the frontend, new classes to the backend, and all your changes (short of config or dependencies) will be reflected in the functioning of the app as you work, without having to manually restart the servers.
Summary
In the end, setting all this up isn't too complicated, but there are lots of little gotchas, many of which don't give you a nice error message to look up on Stack Overflow. Also, at least in my case, I really struggled at first to conceptualise how the pieces were going to work together. Would the React app go inside the Django app, like it does with webpacker
in Rails? If the two apps are separate Docker Compose services, how do you connect them? In the end we learned how to:
- Set up Django in a Docker container.
- Set up Create React App in a Docker container
- Configure those containers with Docker Compose
- Use Docker Compose's service names (e.g.
backend
) andpackage.json
's"proxy"
attribute to direct React's HTTP call to Django's API and display the response.
Top comments (16)
Thanks for your useful tutorial
I have a problem when in frontend dockerization, I face to this error when I run the code: content not from webpack is served from /app/public. And the container will Exited, do you know how can I solve the problem?
Thanks
I cloned a fresh copy of the repo and was able to build and run the app without error. Since
frontend
is mostly Create React App boilerplate, which handles all the Webpack configuration, the source of the problem is hidden under all of CRA's magic.Assuming you didn't eject CRA or move files into or out of
public
, it's possible that old packages are causing problems, as I haven't updated any of them since writing the post. Try updating CRA to the latest version.issues/8688
This issue started with the v3.4.1 upgrade, I listed 5-6 ways from this thread on how to fix it, from pinning to the 3.4.0 version, changing
docker-compose up
todocker-compose run
.. for now I've settled adding a terminal to my compose file, but someone else pointed out the reason they closed the terminal is it could be a security risk. Ultimately the issue is when docker has the server running it thinks it's completed the job and exits.Hey Craig, fantastic tutorial here! I've used it as an inspiration for a cookiecutter that other people might find useful, let me know what you think: cookiecutter-react-django
That's really cool! When I get a minute, I'll update the post to link to your project as well. I imagine it will be a bit more convenient than cloning specific branches in the repo for the tutorial.
Hi Craig
Thank you for the tutorial. It scratches my back happily~. Appreciate it!
I have an issue while following your instructions. I installed axios in success and then after editing App.js according to the code in the end and tried to build as follows
I got this error
Do you have any ideas about what's wrong and what I'm missing?
Thank you and I look forward to your feedback soon~
Never mind. I forgot to uncomment lines in Dockerfile within frontend.
Afterward, it works.
Heroku fails with this error
Step 15/17 : RUN DJANGO_SETTINGS_MODULE=hello_world.settings.production SECRET_KEY=somethingsupersecret python3 backend/manage.py collectstatic --noinput
---> Running in b36173638829
Traceback (most recent call last):
File "backend/manage.py", line 22, in
main()
File "backend/manage.py", line 18, in main
execute_from_command_line(sys.argv)
File "/usr/local/lib/python3.8/site-packages/django/core/management/init.py", line 401, in execute_from_command_line
utility.execute()
File "/usr/local/lib/python3.8/site-packages/django/core/management/init.py", line 395, in execute
self.fetch_command(subcommand).run_from_argv(self.argv)
File "/usr/local/lib/python3.8/site-packages/django/core/management/base.py", line 330, in run_from_argv
self.execute(*args, **cmd_options)
File "/usr/local/lib/python3.8/site-packages/django/core/management/base.py", line 371, in execute
output = self.handle(*args, **options)
File "/usr/local/lib/python3.8/site-packages/django/contrib/staticfiles/management/commands/collectstatic.py", line 194, in handle
collected = self.collect()
File "/usr/local/lib/python3.8/site-packages/django/contrib/staticfiles/management/commands/collectstatic.py", line 109, in collect
for path, storage in finder.list(self.ignore_patterns):
File "/usr/local/lib/python3.8/site-packages/django/contrib/staticfiles/finders.py", line 130, in list
for path in utils.get_files(storage, ignore_patterns):
File "/usr/local/lib/python3.8/site-packages/django/contrib/staticfiles/utils.py", line 23, in get_files
directories, files = storage.listdir(location)
File "/usr/local/lib/python3.8/site-packages/django/core/files/storage.py", line 316, in listdir
for entry in os.scandir(path):
FileNotFoundError: [Errno 2] No such file or directory: '/app/backend/frontend/build/static'
The command '/bin/sh -c DJANGO_SETTINGS_MODULE=hello_world.settings.production SECRET_KEY=somethingsupersecret python3 backend/manage.py collectstatic --noinput' returned a non-zero code: 1
That error is due to the frontend files not being in the expected folder after running
yarn build
. I wasn't able to recreate that specific error, but I was unable to deploy to Heroku due to some mysterious error while trying to install Yarn in theDockerfile
. I've updated dependencies inmaster
and have since been able to build and deploy the app without problem. Trying pullingmaster
and redeploying.Nice men, it's really helpfull. Good work!
This is great. I've often wondered whether docker would be a good fit for data viz.
Hi, excellent tutorial, everything worked like a charm. Just one question, why one-way volumes? Exactly how do they work and how to make good use of them? (Well, 3 actually)
Thanks!
Thanks. The only time I use on-way volumes is for managing the
node_modules
directory. I don't know the internals well, but with a two-way volume, when you rundocker-compose up
, docker takes the files on your machine and inserts them in the newly-started container, overwriting any equivalent files that were in your image. This is usually what you want, so you don't have to rebuild the image every time you change your code, but the whole point of installing dependencies in an image is that they are independent of the machine running the container. Using a one-way volume prevents thenode_modules
directory on your computer from replacing the one in your docker image.Iam getting an error after running docker-compose up: Post unix/filesharing/share: net/http: request canceled (Client.Timeout exceeded while awaiting headers)
I just did a fresh, no-cache build on the
development
branch of the repo and I was able to load the page without error. The timeout error that you're getting suggests internet connection problems, but without more information, I'm not sure what the cause could be.Yes i managed to solve it by stopping my firewall.