Django, Docker, and PostgreSQL Tutorial
Tags: django, python, tutorial, beginners
Published: 2020-02-24 20:38:41

In this tutorial we will create a new Django project using Docker and PostgreSQL. Django ships with built-in SQLite support but even for local development you are better off using a "real" database like PostgreSQL that matches what is in production.

It's _possible_ to run PostgreSQL locally using a tool like [Postgres.app](https://postgresapp.com/), however the preferred choice among many developers today is to use [Docker](https://www.docker.com/), a tool for creating isolated operating systems. The easiest way to think of it is as a large virtual environment that contains everything needed for our Django project: dependencies, database, caching services, and any other tools needed.

A big reason to use Docker is that it completely removes any issues around local development set up. Instead of worrying about which software packages are installed or running a local database alongside a project, you simply run a Docker image of the entire project. Best of all, this can be shared in groups and makes team development much simpler.

## Install Docker

The first step is to install the desktop Docker app for your local machine:

* [Docker for Mac](https://docs.docker.com/docker-for-mac/install/)
* [Docker for Windows](https://docs.docker.com/docker-for-windows/install/)
* [Docker for Linux](https://docs.docker.com/install/)

The initial download of Docker might take some time to download. It is a big file. Feel free to stretch your legs at this point!

Once Docker is done installing we can confirm the correct version is running. In your terminal run the command `docker --version`.

```
$ docker --version
Docker version 19.03.5, build 633a0ea
```

[Docker Compose](https://docs.docker.com/compose/) is an additional tool that is automatically included with Mac and Windows downloads of Docker. However if you are on Linux, you will need to add it manually. You can do this by running the command `sudo pip install docker-compose` after your Docker installation is complete.

## Django project

We will use the [Message Board app](https://djangoforbeginners.com/message-board/) from [Django for Beginners](http://djangoforbeginners.com/). It provides the code for a basic message board app using SQLite that can be updated in the admin.

Create a new directory on your Desktop and clone the repo into it.

```
$ cd ~/Desktop
$ git clone https://github.com/wsvincent/djangoforbeginners.git
$ cd djangoforbeginners
$ cd ch4-message-board-app
```

Then install the software packages specified by `Pipenv` and start a new shell. If you see `(ch4-message-board-app)` then you know the virtual environment is active.

```
$ pipenv install
$ pipenv shell
(ch4-message-board-app) $
```

Make sure to `migrate` our database after these changes.

```
(ch4-message-board-app) $ python manage.py migrate
```

If you now use the `python manage.py runserver` command you can see a working version of our application at [http://localhost:8000](http://localhost:8000).

![Message Board Homepage](https://dev-to-uploads.s3.amazonaws.com/i/8l967mpsvj1mlxnurj0b.png)

## Docker (again)

Hopefully Docker is done installing by this point. To confirm the installation was successful quit the local server with `Control+c` and then type `docker run hello-world` on the command line. You should see a response like this:

```
$ docker run hello-world
Unable to find image 'hello-world:latest' locally
latest: Pulling from library/hello-world
d1725b59e92d: Pull complete
Digest: sha256:0add3ace90ecb4adbf7777e9aacf18357296e799f81cabc9fde470971e499788
Status: Downloaded newer image for hello-world:latest

Hello from Docker!
This message shows that your installation appears to be working correctly.

To generate this message, Docker took the following steps:
 1. The Docker client contacted the Docker daemon.
 2. The Docker daemon pulled the "hello-world" image from the Docker Hub.
    (amd64)
 3. The Docker daemon created a new container from that image whi
ch runs the executable that produces the output you are currently reading.
 4. The Docker daemon streamed that output to the Docker client,
which sent it to your terminal.

To try something more ambitious, you can run an Ubuntu container
with:
 $ docker run -it ubuntu bash

Share images, automate workflows, and more with a free Docker ID:
 https://hub.docker.com/

For more examples and ideas, visit:
 https://docs.docker.com/get-started/
```

## Images and Containers

There are two importance concepts to grasp in Docker: **images** and **containers**.

* **Image**: the list of instructions for all the software packages in your projects
* **Container**: a runtime instance of the image

In other words, an image _describes_ what will happen and a container _is_ what actually runs.

To configure Docker images and containers we use two files: `Dockerfile` and `docker-compose.yml`.

The `Dockerfile` contains the list of instructions for **the image**, aka, What actually goes on in the environment of the container.

Create a new `Dockerfile` file.

```
(ch4-message-board-app) $ touch Dockerfile
```

Then add the following code in your text editor.

```
# Dockerfile

# Pull base image
FROM python:3.7

# Set environment variables
ENV PYTHONDONTWRITEBYTECODE 1
ENV PYTHONUNBUFFERED 1

# Set work directory
WORKDIR /code

# Install dependencies
RUN pip install pipenv
COPY Pipfile Pipfile.lock /code/
RUN pipenv install --system

# Copy project
COPY . /code/
```

On the top line we're using an [official Docker image](https://hub.docker.com/_/python/) for Python 3.7. Next we create two environment variables. `PYTHONUNBUFFERED` ensures our console output looks familiar and is not buffered by Docker, which we don't want. [PYTHONDONTWRITEBYTECODE](https://docs.python.org/3/using/cmdline.html?highlight=pythondontwritebytecode#envvar-PYTHONDONTWRITEBYTECODE) means Python won't try to write `.pyc` files which we also do not desire.

The next line sets the `WORKDIR` to `/code`. This means the working directory is located at `/code` so in the future to run any commands like `manage.py` we can just use `WORKDIR` rather than need to remember where exactly on Docker our code is actually located.

Then we install our dependencies, making sure we have the latest version of `pip`, installing `pipenv`, copying our local `Pipfile` and `Pipfile.lock` into Docker and then running it to install our dependencies. The `RUN` command lets us run commands in Docker just as we would on the command line.

Next we need a new `docker-compose.yml` file. This tells Docker _how_ to run our Docker container.

```
(ch4-message-board-app) $ touch docker-compose.yml
```

Then type in the following code.

```
version: '3.7'

services:
  db:
    image: "postgres:11"
    volumes:
      - postgres_data:/var/lib/postgresql/data/
  web:
    build: .
    command: python /code/manage.py runserver 0.0.0.0:8000
    volumes:
      - .:/code
    ports:
      - 8000:8000
    depends_on:
      - db

volumes:
  postgres_data:
```

On the top line we're using the [most recent version](https://docs.docker.com/compose/compose-file/compose-versioning/) of Compose which is `3.7`.

Under `db` for the database we want the Docker image for Postgres 10.1 and use `volumes` to tell Compose where the container should be located in our Docker container.

For `web` we're specifying how the web service will run. First Compose needs to build an image from the current directory and start up the server at `0.0.0.0:8000`. We use `volumes` to tell Compose to store the code in our Docker container at `/code/`. The `ports` config lets us map our own port 8000 to the port 8000 in the Docker container. This is the default Django port. And finally `depends_on` says that we should start the `db` first before running our web services.

The last section `volumes` is because Compose has a rule that you must list named volumes in a top-level `volumes` key.

We can `exit` the virtual environment now since we're about to switch over to running Docker instead.

```
(ch4-message-board-app) $ exit
$ 
```

Now start the Docker container using the `up` command, adding the `-d` flag so it runs in detached mode, and the `--build` flag to build our initial image. If we did not add this flag, we'd need to open a separate command line tab to execute commands.

```
$ docker-compose up -d --build
```

Docker is all set! You can visit the Message Board homepage at [http://127.0.0.1:8000/](http://127.0.0.1:8000/) and it will show, now running via Docker.

## Update to PostgreSQL

We need to update our _Message Board app_ to use PostgreSQL instead of SQLite. First install `psycopg2-binary` for our database bindings to PostgreSQL.

```
$ pipenv install psycopg2-binary
```

Then update the `settings.py` file to specify we'll be using PostgreSQL not SQLite.

```python
# settings.py
DATABASES = {
    'default': {
        'ENGINE': 'django.db.backends.postgresql',
        'NAME': 'postgres',
        'USER': 'postgres',
        'HOST': 'db', # set in docker-compose.yml
        'PORT': 5432 # default postgres port
    }
}
```

We should `migrate` our database at this point on Docker.

```
$ docker-compose exec web python manage.py migrate
```

## Admin
Since the _Message Board app_ requires using the admin, create a superuser on Docker. Fill out the prompts after running the command below.

```
$ docker-compose exec web python manage.py createsuperuser
```

Now go to [http://127.0.0.1:8000/admin](http://127.0.0.1:8000/admin) and login. You can add new posts via the admin and then seem them on the homepage just as described in [Django for Beginners](https://djangoforbeginners.com/message-board/).

![Message Board Admin](https://dev-to-uploads.s3.amazonaws.com/i/3y0uk0f9sorlsf8aslr1.png)

When you're done, don't forget to close down your Docker container.

```
$ docker-compose down
```

## Quick Review
Here is a short version of the terms and concepts we've covered in this post:

* **Image**: the "definition" of your project
* **Container**: what your project actually runs in (an instance of the image)
* **Dockerfile**: defines what your image looks like
* **docker-compose.yml**: a [YAML](http://yaml.org/) file that takes the Dockerfile and adds additional instructions for how our Docker container should behave in production

We use the *Dockerfile* to tell Docker how to build our *image*. Then we run our actual project within a *container*. The *docker-compose.yml* file provides additional information for how our Docker container should behave in production.

## Next Steps

If you'd like to learn more about using Django and Docker together I've written an entire book on the subject, [Django for Professionals](https://djangoforprofessionals.com).