Dockerizing a Full-stack Application

Matthew Rosendin
Jan 22, 2018 · 5 min read

A common problem for developers is managing their application stack. There are a lot of different services to consider — frontend, backend, database, and maybe an in-memory database and a process for distributed task queueing. On top of this, there is also a web server such as Nginx or Apache for your production environment. The typical procedure for starting the application involves running a tedious sequence of commands. Wouldn’t it be nice to automate this stack?

Docker is a company that can help ease the pain. More specifically, Docker Compose is the tool that makes life easier. With Docker Compose, you can start your application stack in just one command. In this post, I will show you how I did this. The tools that I use are:

  • Nginx: reverse proxy server
  • Django: backend
  • Gunicorn: web server
  • PostgreSQL: database
  • Redis: in-memory store
  • Celery: distributed task queue
  • Vue.js: frontend

This is a pretty common application stack, especially for Django developers. Before I begin, I want to make a quick note. This tutorial will help recreate a production environment. I have found that it can be very useful to recreate a development environment with Docker Compose as well.

Additionally, I will use Vue.js as a single-page application frontend. If you aren’t familiar with Vue.js, it is similar to React. In my case, I used Webpack to bundle the frontend into static files that will be served by Nginx.

Ok, let’s get started!


Project Structure

A quick word on the structure of my project. I’ve separated concerns between the client, the server, and Nginx configuration. All other services are neatly abstracted away thanks to Docker images.

project/
├── client/ # Contains frontend files
├── nginx/ # Contains Nginx config
├── server/ # Contains Django API
├── .env # Environment variable config
├── docker-compose.yml
└── README.md

Nginx Configuration

Nginx configuration can be challenging on its own, but adding Docker containers into the mix complicates things further. In my case, I had to point Nginx to the static files from the Vue.js bundle to handle regular requests but for routes beginning with /api or /admin I proxied to the container running the Gunicorn web server. Note that the host (which normally would be 127.0.0.1 without Docker) is called server . This is a name that I define in the docker-compose.yml file and it resolves to the container’s IP automatically.

user nginx;
worker_processes 1;
error_log /var/log/nginx/error.log warn;
pid /var/run/nginx.pid;
events {
worker_connections 1024;
}
http {
include /etc/nginx/mime.types;
default_type application/octet-stream;
log_format main '$remote_addr - $remote_user [$time_local]'
'"$request" $status $body_bytes_sent'
'"$http_referer" "$http_user_agent"'
'"$http_x_forwarded_for"';
access_log /var/log/nginx/access.log main; upstream server {
server server:8000;
}
server {
listen 80 default_server;
listen [::]:80 default_server;
server_name localhost;
charset utf-8;
root /usr/share/nginx/html;
index index.html;
location / {
try_files $uri $uri/ @rewrites;
}
location @rewrites {
rewrite ^(.+)$ /index.html last;
}
location ^~ /static/ {
autoindex on;
alias /usr/share/nginx/html/static/;
}
location ~ ^/api {
proxy_pass http://server;
}
location ~ ^/admin {
proxy_pass http://server;
}
}
}

Dockerfiles

I have a Dockerfile in each of the top-level folders, except the client folder. Since the frontend is just static files, I run npm run build which outputs the assets to ./client/dist/ .

The Nginx Dockerfile is simple. I copy an nginx.conf that is within the Nginx directory and I copy the static assets bundle from the client directory.

FROM nginx# Add the Nginx configuration file
ADD ./nginx/nginx.conf /etc/nginx/nginx.conf
# Copy over static assets from the client application
COPY ./client/dist /usr/share/nginx/html

That just leaves the Django Dockerfile. It is also relatively straightforward, with the exception of the dependency section. Copying the requirements.txt file before installing the dependencies enables Docker to cache the dependencies on subsequent builds. In my case, I had a private module hosted on GitHub which couldn’t be downloaded from GitHub by Docker (since it required credentials) so I have downloaded the zip file and added it.

FROM python:3.6# To enable logging
ENV PYTHONUNBUFFERED 1
# Create server directory
RUN mkdir -p /srv/project
WORKDIR /srv/project
# Install the server dependencies
COPY requirements.txt /srv/project
ADD ./vendor/module-1.0.zip /srv/project/vendor/
RUN pip3 install -r requirements.txt
# Bundle the source
COPY . /srv/project
# Expose port 8000 in the container
EXPOSE 8000

Docker Compose File

This part can take a while to get right, especially if you’re new to Docker like me. I created a file called docker-compose.yml at the root of my project like so:

version: "3"
services:
nginx:
container_name: nginx
build:
context: .
dockerfile: ./nginx/Dockerfile
image: nginx
restart: always
volumes:
- ./server/static/admin:/usr/share/nginx/html/static/admin
ports:
- 80:80
depends_on:
- server
command: nginx -g 'daemon off';
server:
container_name: server
build:
context: ./server
dockerfile: Dockerfile
hostname: server
ports:
- 8000:8000
volumes:
- ./server:/srv/project
depends_on:
- postgres
env_file: .env
command: >
bash -c '
python manage.py makemigrations &&
python manage.py migrate &&
gunicorn project.wsgi -b 0.0.0.0:8000'
postgres:
container_name: postgres
image: postgres:latest
hostname: postgres
ports:
- 5432:5432
redis:
container_name: redis
image: redis:latest
hostname: redis
ports:
- 6379:6379
celery:
container_name: celery
build:
context: ./server
env_file: .env
command: celery -A project worker -l info
volumes:
- ./server:/srv/project
depends_on:
- server
- redis

There’s nothing too special about this file, other than the fact that Nginx contains a volume for the Django admin site. Essentially what I did was I ran python manage.py collectstatic locally and shared the resulting folder on the host with the container so that Nginx could access those files. As you will later see, I already had my Vue.js static files in /usr/share/nginx/html so I had to be careful not to overwrite them with the volume; therefore, I individually copied over the admin app’s static file folder instead of the whole static directory.

Pitfalls

If you are using Webpack dev server (which I am not in this post), you may want to ensure that all outgoing API requests from your frontend are correctly proxied to the Django docker container. In your Webpack config, add the following:

module.exports = {
dev: {
assetsSubDirectory: 'static',
assetsPublicPath: '/',
proxyTable: {
'/api': {
target: 'http://server:8000',
changeOrigin: true,
pathRewrite: {
'^': ''
}
}
},
host: '0.0.0.0',
port: 8080,
...

If you enabled HSTS (HTTPS Strict Transport Security) in your Django configuration file at some point and forgot to remove it like I did, all requests to your backend will be redirected to https if you use Chrome. If you face this painful issue, go to chrome://net-internals/#hsts in Chrome and go to the “Delete domain security policies” section. Enter “localhost” (or whatever you use) and delete the HSTS settings.

Running the Application

Now the time for satisfaction. A single terminal tab running docker-compose up --build will build and start all services.

Conclusion

That’s pretty much it. My next thoughts are to rename docker-compose.yml to production.yml as recommended here and create a new docker-compose.yml containing my development stack with the Webpack dev server hot reload feature. I would also remove Nginx to avoid unnecessary complexity and use the Django runserver command for debugging. This may induce another post soon. Until then, happy coding!

Matthew Rosendin

Written by

Integration Engineering @ Ripple, formerly @ Zendesk, co-founded Polyledger. San Francisco, CA 🌉

Welcome to a place where words matter. On Medium, smart voices and original ideas take center stage - with no ads in sight. Watch
Follow all the topics you care about, and we’ll deliver the best stories for you to your homepage and inbox. Explore
Get unlimited access to the best stories on Medium — and support writers while you’re at it. Just $5/month. Upgrade