Docker Compose for Django with Postgres

enter image description here

In this article I will show a working example of running Django and PostgreSQL with Docker Compose. We’ll have a look at running the Django migrations and creating the superuser as well. Also, we want to keep our database changes so that it remains available if we restart the application.

Let’s dive in. To accomplish this, we need two containers - one for Postgres and one for Django.

Postgres

postgres:
  container_name: postgres-container
  image: postgres:latest
  environment:
    POSTGRES_DB: dog_grooming_website
    POSTGRES_USER: dog_grooming_user
    POSTGRES_PASSWORD: yoursecretpassword
  ports:  
    - "5432:5432"
  volumes:
    - ./docker/postgres_data:/var/lib/postgresql/data/
  healthcheck:
    test: ["CMD-SHELL", "pg_isready -d $${POSTGRES_DB} -U $${POSTGRES_USER}"]
    interval: 10s
    timeout: 5s
    retries: 5
  • postgres: The name of the service.
  • container_name: The name of the Docker container of this service.
  • image: The base image which is the latest postgres image. Use the required specific version like postgres:16.
  • environment: Environment variables that define the default Postgres superuser, its password and also the default database.
  • ports: You can map the ports of the container to the host machine. In case you have your own Postgres running on the host machine, you can map it to another port such as 5433:5432. In this case, the Django container will still reach Postgres on port 5432, but in the host machine it will be available on port 5433.
  • volumes: You can volume share data between the container and the host machine. In our example we volume share the database data, so even when we start a new container, the data remains available. You could volume share a database init script too as per the following example: ./docker/init_db.sql:/docker-entrypoint-initdb.d/init_db.sql.
  • healthcheck: You can define a check to see whether the service is healthy or not. The check can be rerun several times based on your parameters. We want that the Django application container is not started until the Postgres container is healthy, because if the database is not up and running, the Django application will fail to connect to the database and execute the migrations.

Django

For the Django application we need an image that has all the requirements installed and has the source code. You can add your source code to the image or you can volume share it. Let’s create our Docker image and based on that the Docker Compose service for our Django application.

Dockerfile:

FROM python:latest

ENV PYTHONUNBUFFERED 1

RUN mkdir /django_app
WORKDIR /django_app

ADD requirements.txt requirements.txt
RUN pip install -r requirements.txt

ADD . /django_app/

As a base we use the official latest python image. You can use a specific Python version too such as python:3.9.6. Then, we just create a folder for our source code, set it as our working directory, install all the requirements and add our source code to the image.

Including it in Docker Compose:

django-app:
  container_name: django-app-container
  build:
    context: .
    dockerfile: docker/Dockerfile
  environment:
    DB_NAME: dog_grooming_website
    DB_TEST_NAME: dog_grooming_website_test
    DB_USER: dog_grooming_user
    DB_PASSWORD: yoursecretpassword
    DB_HOST: postgres
    DB_PORT: 5432
    DJANGO_SUPERUSER_USERNAME: admin
    DJANGO_SUPERUSER_PASSWORD: yoursecretpassword
    DJANGO_SUPERUSER_EMAIL: youremail@djangomail.com
  ports:
    - "8000:8000"
  command:
    - /bin/sh
    - -c
    - |
      python manage.py migrate
      python manage.py createsuperuser --noinput
      python manage.py runserver 0.0.0.0:8000
  depends_on:
    postgres:
      condition: service_healthy
  healthcheck:
    test: curl --fail http://localhost:8000 || exit 1
    interval: 10s
    timeout: 10s
    start_period: 10s
    retries: 10
  • build: We set the context to the current directory and provide the relative path to the Dockerfile. In my case, I have it inside a docker folder.
  • environment: I provide all the database config details as environment variables so that I can set them dynamically in Django’s settings.py when running the application. We’ll see it below. I also provide the variables for the Django superuser that we create in the command section. The database host is postgres because we named our Postgres service like that. We could put all our environment variables into a separate file and use the env_file option instead.
  • ports: Django runs on port 8000 by default, we use the same and map it to the port 8000 of the host machine.
  • command: The commands that are executed upon running the container. As you can see, we run the migrations, create the superuser and run the server on port 8000. Creating the superuser is done by considering the environment variables set above.
  • depends_on: Our Django application depends on the postgres service that we defined above. We define a condition that the service has to be healthy, meaning the database up and running.
  • healthcheck: We add a health check to test whether our Django application is running on port 8000.

Complete Solution

Consider this sample folder structure:

repo
  |-- Django source
  |-- manage.py
  |-- requirements.txt
  |-- docker
      |-- Dockerfile
      |-- .dockerignore
  |-- docker-compose.yml
  |-- other folder and files  

The files and folders you define in the .dockerignore file will be ignored when adding files into the Docker image. For example:

**/docker/
**/.idea/
**/env/
**/.git/
**/.DS_Store

Dockerfile:

FROM python:latest

ENV PYTHONUNBUFFERED 1

RUN mkdir /django_app
WORKDIR /django_app

ADD requirements.txt requirements.txt
RUN pip install -r requirements.txt

ADD . /django_app/

docker-compose.yml:

version: '1'
services:
  postgres:
    container_name: postgres-container
    image: postgres:latest
    environment:
      POSTGRES_DB: dog_grooming_website
      POSTGRES_USER: dog_grooming_user
      POSTGRES_PASSWORD: yoursecretpassword
    ports:  
      - "5432:5432"
    volumes:
      - ./docker/postgres_data:/var/lib/postgresql/data/
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -d $${POSTGRES_DB} -U $${POSTGRES_USER}"]
      interval: 10s
      timeout: 5s
      retries: 5

  django-app:
    container_name: django-app-container
    build:
      context: .
      dockerfile: docker/Dockerfile
    environment:
      DB_NAME: dog_grooming_website
      DB_TEST_NAME: dog_grooming_website_test
      DB_USER: dog_grooming_user
      DB_PASSWORD: yoursecretpassword
      DB_HOST: postgres
      DB_PORT: 5432
      DJANGO_SUPERUSER_USERNAME: admin
      DJANGO_SUPERUSER_PASSWORD: yoursecretpassword
      DJANGO_SUPERUSER_EMAIL: youremail@djangomail.com
    ports:
      - "8000:8000"
    command:
      - /bin/sh
      - -c
      - |
        python manage.py migrate
        python manage.py createsuperuser --noinput
        python manage.py runserver 0.0.0.0:8000
    depends_on:
      postgres:
        condition: service_healthy
    healthcheck:
      test: curl --fail http://localhost:8000 || exit 1
      interval: 10s
      timeout: 10s
      start_period: 10s
      retries: 10

In Django’s settings.py we get the database settings from the environment variables if they are available, if not, we load them from a config file (not relevant now). As we provided the environment variables through Docker Compose, they will be used here.

DATABASES = {
    'default': {
        'ENGINE': 'django.db.backends.postgresql',
        'NAME': os.environ.get('DB_NAME', db_config.get('name', '')),
        'USER': os.environ.get('DB_USER', db_config.get('user', '')),
        'PASSWORD': os.environ.get('DB_PASSWORD', db_config.get('password', '')),
        'HOST': os.environ.get('DB_HOST', db_config.get('host', '')),
        'PORT': os.environ.get('DB_PORT', db_config.get('port', '')),
        'TEST': {
            'NAME': os.environ.get('DB_TEST_NAME', db_config.get('test_db_name', ''))
        }
    }
}

Example

As an example you can check my latest project on GitHub.

Running The Docker Compose

Just change directory to where you have the docker-compose.yml and run:

docker-compose up -d

-d will run it in detached mode.
As we mapped the port 8000 of the Django application container to the same of the host machine, go to 127.0.0.1:8000 in your browser to make sure your application is running.

To stop and remove the containers:

docker-compose down

Conclusion

Docker Compose is a great and easy way to share your projects and make them easy to run. Containerizing applications is a widely used technique in development and production environments as well.

Comments