Setting up a Django app in Docker with Poetry

This is a short introduction how to build a Django app with Poetry as a package manager and then make it work in a Docker container. Finally, there's a how-to for Heroku and an example app you can use as a reference.

Setting up Poetry

Installing poetry is very simple:

pip3 install --user poetry

Poetry uses the pyproject.toml, a file based on PEP-518, which is supposed to be a unified way of specifying project meta information, build system, dependencies, etc. However, each tool that uses this file can add its own sections and the mess with Python virtual environments and package managers continues.

Adding dependencies is as easy as:

poetry add Django

I'd recommend always switching into the venv by poetry shell and then running ./manage.py runserver.

Now, Poetry uses it's lock file poetry.lock. This is different from using plain old requirements.txt for pip, but it's on par with - for example - Pipenv.

The advantage of using Poetry over virtualenv or Pipenv is the easy interface and the option to build and publish packages on PyPI.

So now we setup the project with Poetry as the package manager of choice and our Django app works locally without problems (hopefully).

gunicorn

gunicorn is a Python WSGI server that we'll use for the Docker image. You can already install it with Poetry: poetry add --dev gunicorn and then run your app using gunicorn project.wsgi.

Configuring static files

Serving static files is a bit more complicated with gunicorn, but once we set up a few things, it'll help us create the Docker image.

In your settings.py, configure static files like this:

STATIC_ROOT = os.path.join(BASE_DIR, 'staticfiles')
STATIC_URL = '/static/'

STATICFILES_DIRS = (
    os.path.join(BASE_DIR, '<module name>', 'static'),
)

Furthermore, you need to install and enable whitenoise:

poetry add --dev whitenoise

settings.py:

MIDDLEWARE = [
    # ...
    'whitenoise.middleware.WhiteNoiseMiddleware',
    # ...
]

Putting everything into Dockerfile

For reference, I will put an example Dockerfile here and then explain how it works.

FROM python:3
WORKDIR /usr/src/app
EXPOSE 8000

# Install poetry and deps
# Exporting to requirements.txt will avoid hassle with poetry venv
COPY pyproject.toml ./
COPY poetry.lock ./
RUN pip install poetry
RUN poetry export -f requirements.txt --output requirements.txt
RUN pip install -r requirements.txt

# Copy project
COPY manage.py manage.py
COPY <your app> <your app>
COPY <your project> <your project>

# Collect static files, pass dummy DJANGO_SECRET_KEY
# or else ./manage.py will fail
# since DJANGO_SECRET_KEY is not available during the build step
RUN DJANGO_SECRET_KEY=dummy ./manage.py collectstatic

# Run server
CMD [ "gunicorn", "-b", "0.0.0.0:8000", "<your project>.wsgi" ]

The steps are already somewhat explained with the comments. In more detail now.

We start with a base python3 image, setup a working directory for the project source code and expose the port where gunicorn will be running (default is 8000).

FROM python:3
WORKDIR /usr/src/app
EXPOSE 8000

Then we copy files for Poetry, pyproject.toml and poetry.lock:

COPY pyproject.toml ./
COPY poetry.lock ./

After that, we install Poetry itself, use it to export our dependencies from the lock file into requirements.txt and then use pip to install those frozen dependencies:

RUN pip install poetry
RUN poetry export -f requirements.txt --output requirements.txt
RUN pip install -r requirements.txt

Now the environment is setup and we can copy the project source code (actually this step could happen earlier):

COPY manage.py manage.py
COPY <your app> <your app>
COPY <your project> <your project>

<your project> is the root folder name and <your app> is each app inside your project.

Now the part about collecting static files from your apps.

RUN DJANGO_SECRET_KEY=dummy ./manage.py collectstatic

Since this is a build step, ./manage.py cannot reach the environment variables. They are only reachable once the app is running. This SO question sums the problem up. However, we don't need to generate random secret key as in the accepted answer. What we can do is inside this Dockerfile example. Just export a dummy DJANGO_SECRET_KEY before running ./manage.py collectstatic so the script doesn't fail and everything is okay. Since this is a build step that doesn't in any way interact with the users of your app, it's perfectly fine to use any dummy value.

And finally we start up the web server:

CMD [ "gunicorn", "-b", "0.0.0.0:8000", "<your project>.wsgi" ]

Now build the image:

docker build -t project-name .

And run it:

docker run --rm -e DJANGO_SECRET_KEY="<secret key>" -p 8000:8000 project-name

Using Poetry on Heroku

For deploying your project on Heroku, you need a Procfile in your project's root directory, containing the command to run the server:

web: gunicorn project-name.wsgi

But since Heroku's heroku/python buildpack only detects requirements.txt or Pipfile, we need some pre-processing to export Poetry's dependencies into requirements.txt.

This is done by a python-poetry-buildpack. It needs to run before heroku/python. This will export requirements.txt for us, which can be then grabbed by heroku/python and the deploy process can continue as with pip or Pipenv.

Example app

Feel free to use my daylio-stats project as a reference for building your own cool stuff! Some of the steps described here were pretty difficult to find on the internet and make work.