DEV Community

Nickolena
Nickolena

Posted on • Edited on

My favorite way to write a Dockerfile for a Python app

There's more than one way to skin a cat, but this pattern makes my heart flutter.

Containerize a Python app

The asgi.py in our app directory

from fastapi import FastAPI

app = FastAPI()

@app.get("/")
def hello():
  return "World"
Enter fullscreen mode Exit fullscreen mode

Dockerfile

FROM python:3.7-buster as base

ENV SHELL=/bin/bash \
    USER=python \
    UID=10001

# Create a user for the app to be owned by
RUN set -eux; adduser \
    --disabled-password \
    --gecos "" \
    --home "/var/lib/python3" \
    --shell "/sbin/nologin" \
    --no-create-home \
    --uid "${UID}" \
    "${USER}"

# Manually create the users home directory
RUN mkdir -p "/var/lib/python3" && chown -R "${USER}" /var/lib/python3

FROM base as build

# Install the app requirements in its own layer.  
# Isolating the "build" stage from the application stage removes
# some of the unwanted cruft that comes with install 
# python dependencies
USER ${USER}
RUN python -m pip install \
      --user \
      'uvicorn[standard]' \
      'fastapi' \
      'wheel'

FROM base

COPY --from=build --chown=${USER} /var/lib/python3/.local /var/lib/python3/.local
ENV PATH=$PATH:/var/lib/python3/.local/bin

USER root 
ENTRYPOINT ["docker-entrypoint.sh"]
COPY docker-entrypoint.sh /usr/bin/docker-entrypoint.sh 

# Create a designated location for the app as the python user
USER ${USER}
WORKDIR /usr/lib/python

COPY ./app ./app

CMD ["app.asgi:app"]
Enter fullscreen mode Exit fullscreen mode

Entrypoint

#!/bin/bash -e

# If the CMD has not changed process it as a pure Python implementation
if [ "${1#-}" != "${1}" ] || [ -z "$(command -v "${1}")" ]; then
  set -- uvicorn "$@" --host 0.0.0.0 --http h11 --loop asyncio
fi

echo 
echo "Running $@"
echo

exec "$@"
Enter fullscreen mode Exit fullscreen mode

Breaking down the stages of the Dockerfile

This Dockerfile is separated in a few stages to organized by the function of its build step. The multi-stage build strips our application of unnecessary files.

base stage

The first stage is aliased as the "base". This stages bootstraps our required instructions that the remaining layers share. The user lets us install requirements outside of the root user.

build stage

The second stage brings in external requirements and installs them as the python user. This is useful because only the user's .local directory needs to be copied in the final stage. Copying the .local directory brings with only the requirements that application requires to run.

The nameless application stage

The base image of the app starts with a clean layer from the first stage. Removing the need to redeclare any instructions already provided.

The requirements can be copied to the user's .local directory like they were installed by pip. The app layer is not based on the build layer to reduce any left over's brought in by pip install.

Finally, the app itself is copied into a directory outside of user's home.


Top comments (3)

Collapse
 
nickmaris profile image
nickmaris

Why do you need "USER root"?

I didn't know that nodejs has best practices for docker. I wish ruby had too.

Collapse
 
ndfishe profile image
Nickolena

You really don't need it....it's definitely redundant, but I use as indicator for, "hey, these commands are running as root"

Some comments may only be visible to logged-in visitors. Sign in to view all comments.