Setting up a development environment can be tricky and painful depending on the packages you use, Docker helps to eliminate the hassle by eliminating the need for configuring the system by creating an isolated environment every where.

This is a set up for running a Django development server with PostgreSQL in a docker environment using docker-compose, that means this is not production ready. I’ll get instructions for a production environment soon.

Docker is a containerisation tool for running isolated, reproducible application environments and Docker Compose facilitates running multi-container Docker applications, in other words you can run multiple docker containers together and let the containers interact with each other.

Let me brief what we are going to do in here:

  1. Define Django app’s environment in a Dockerfile.
  2. Define the services require for the Django app in docker-compose.yml so that all services can be run together in a command in an isolated environment.

Pre-Requisite

I’m proceeding with intention that you get theses done before we proceed, as I won’t be able to explain the following to keep the tutorial simple.

  • Docker (Installation)
  • Django Application, if you don’t have it refer Writing your first Django app.
  • requirements.txt, list of packages required by your application. If you don’t have this file go to your root director of app and run the following pip freeze > requirements.txt ( if you use virtualenv make sure your virtual env is active before you run this).

App Directory

For demo I’m using a standard Django app created with django-admin startproject app. But you can use the below steps and apply it to an existing project as well.


├── app
│    ├── app
│    │   ├── __init__.py
│    │   ├── apps
│    │   ├── urls.py
│    │   └── wsgi.py
│    └── manage.py
├── requirements.txt

No changes have been done to this project that means if you run this you will get a “welcome to Django page” and the app still uses the sqlite database.

GitHub

Containerize Django

We need to get the Django project into a container, to do that a Dockerfile is used to define the specifications for the container.

This container will have the Django application ( code ) and the dependencies only service like database (MySQL, PostgreSQL, etc), cache (memcahce, reddis) or queue service (rabbitmq ) etc will not be in this container it will be run in a parallel container.

FROM python:2.7

# to skip buffering
ENV PYTHONUNBUFFERED=1

EXPOSE 8000

# Created a directory in the container 
# ( use any name, make sure to change it in below lines as well )
RUN mkdir /code

# set as working directory
WORKDIR /code

# copy the contents of entire directory to the directory in the container 
COPY . /code/

# install package in the container
RUN pip install -r requirements.txt

In here python:2.7 is the base image for the container from Official Python Repository on docker hub. You can use any image of your choice if required.

Navigate to the app directory (do not activate virtualenv) and if you execute docker build . you will will see a container being started, followed by packages being installed and close. That means you are good to proceed.

if you add RUN python manage.py runserver you can see a Django server running, but don’t add it here we will be starting the Django server from docker compose.

Note: You will not be able to access the server from http://127.0.0.1:800 or http://localhost:8000 because your system is not yet lined to the container. Hit ctrl+c it will close the Django server and also the container along with it.

Connect Services

We have the Django application in a container now lets add the required services and get them running together. It’s done using docker-compose.yml.

This demo need 2 containers:

  • Database : PostgreSQL
  • Django

Django settings.py

Before we proceed further we need to make a few changes to the settings.py so it can take parameters like Database username, password, Secret Key and other credentials set in ENVIRONMENT rather than hard coding them into the code

os.environ.get() is what I prefer because it allows you to set a fall back value in case environment variable is absent.

These are the variables you need to change in settings.py file, you can replace this with the existing ones.

 
# secret key
SECRET_KEY = os.environ.get('SECRET_KEY', 'y6xg%o&^m4ggo5s!%mcdnu@v#neu#1v#2s9d&_q2(wixlxd((#')

# Debug 
DEBUG = os.getenv('DEBUG', False)
# its good idea to set default as False and over ride to True in testing or development.

# Database 
if 'DATABASE_HOST' in os.environ:
    DATABASES = {
        'default': {
            'HOST': os.environ.get('DATABASE_HOST')
            # ENGINE read notes below
            'ENGINE': 'django.db.backends.postgresql_psycopg2',
            'NAME': os.environ.get('DATABASE_NAME'),
            'USER': os.environ.get('DATABASE_USER'),
            'PASSWORD': os.environ.get('DATABASE_PASSWORD'),
        }
    }
else :
    DATABASES = {
        'default': {
            'ENGINE': 'django.db.backends.sqlite3',
            'NAME': os.path.join(BASE_DIR, 'db.sqlite3'),
        }
    }

These are the necessary setting for this starter app, you might need to move other variables like the Queue services credentials, Memcache credentials which will be set in the container service so they cannot be hard coded and every authentication key out of the code so they don’t get leaked.

Note: I have hard coded the database engine, this will work fine because you won’t be changing it often as other parameters but you can pass it via environment variable too.

docker compose

Lets set the docker-compose settings and get the system running.

version: '3'

services:
  db:
    hostname: db
    image: postgres:9.4
    restart: always
    env_file: .env
    ports:
      - "5432:5432"

   # to make the database data persist, if you don't want to persist data remove this
    volumes:
      - ./postgres-data:/var/lib/postgresql/data

  app:
    build: .
    restart: always
    # script to be run once container starts up
    # see below to know whats inside run_app.sh
    entrypoint: ./run_app.sh
    volumes:
      - ./app:/code/app
    depends_on:
      - db
    # connect system port 80 to 8000 to access Django without port
    ports:
      - "80:8000"
    env_file: .env

Let me explain whats in the xml file.

  • db : This is the container for the database, the container will be starting using the image: postgres:9.4 as base image from Official PostgreSQL Repository on docker hub.

    I have mount the data storage of PostgreSQL to a directory in system so that the data will not be lost when this image is rebuild. The data will be stored in ‘postgres-data’ in directory where docker-compose.yml is located.

  • app: The container for the Django app, this will be build using the configurations in Dockerfile.

    Mount and over ride the ‘app’ directory in the container with the one in system to get changes you make to code reflect immediately, else you will have to rebuild the entire container.

    entrypoint: when we defined the Dockerfile we didn’t add the configuration to start the Django server because the credentials for the services were unavailable and also the migrations were not present so the code to run server, do migrations and other tasks if need will be defined in a shell script and executed by docker-compose

    # run_app.sh
    #!/bin/bash
    
    python app/manage.py makemigrations && python app/manage.py migrate && python app/manage.py runserver 0.0.0.0:8000
    

    Important : Make run_app.sh executable using chmod +x run_app.sh before you build containers. If not a permission error will throw up.

ENVIRONMENT .env

To increase security and easy of porting the credentials and system dependent values are passed as Environment variable so the code can use them as needed. Docker compose will use the values in env_file and will be made available to the container as environment variable. Typically .env is the file name used you can use any of your choice. Make sure to add the name to .gitignore to prevent the credentials from being added to git.

The database user, password and the database name for PostgresSQL container is to be passed via environment variable, if you are using a different image you should refer the documentation to set credentials.

Next we need to set the Environment variable required by Django settings.

#.env
# DB
POSTGRES_USER=postgres
POSTGRES_PASSWORD=postgres
POSTGRES_DB=postgres

# app
SECRET_KEY=y6xg%o&^m4ggo5s!%mcdnu@v#neu#1v#2s9d&_q2(wixlxd((#
DATABASE_HOST=db
DATABASE_NAME=postgres
DATABASE_USER=postgres
DATABASE_PASSWORD=postgres

# for debugging and error reporting.
DEBUG=True

Now its time to test it out, run docker-compose up and you can see docker pulling the image and build the app once done the app will kick up.

App Directory

├── .env
├── Dockerfile
├── app
│    ├── app
│    │   ├── __init__.py
│    │   ├── apps
│    │   ├── urls.py
│    │   └── wsgi.py
│    └── manage.py
├── docker-compose.yml
├── requirements.txt
└── run_app.sh

Other commands for docker-compose

  • docker-compose up to start the containers.
  • docker-compose up -d to start the containers as daemon.
  • docker-compose build rebuild the containers.
  • docker-compose build rebuild container.
  • docker-compose ps list running containers.
  • docker-compose kill kill all running containers.
  • docker-compose kill kill container.

Additional

You might see the app container throwing error stating the database server not available and close itself don’t worry it will start again as restart: always is defined, this is normal and expected because docker-compose does not provide a build in method to control the order in which containers start up refer Control startup order in Compose for more on it.

What happens here is the Django app container is started before the database servers come up. You can fix the issue by adding a script in run_app.sh which checks if the db servers is up and then executes the app or wait-for-it.sh is a good tool for the same.

Create Django Super User

Since the application is running inside the container you cannot run manage.py createsuperuser directly.

Navigate to the project root and start docker compose docker-compose up and open a new terminal window and navigate the the root and enter the bash using docker-compose exec app bash ‘app’ is the name of container you need to enter.

django docker createsuperuser

Now you are inside the container, navigate to where your manage.py is and run manage.py createsuperuser things should work as normal.

GitHub

So that’s it on Django Development server on docker compose. DO NOT USE THIS FOR PRODUCTION, I will get a blog on production soon.