My favorite way to write a Dockerfile for a Python app
Tags: python, docker, fastapi
Published: 2021-02-24 16:54:48

---
title: My favorite way to write a Dockerfile for a Python app
published: true
description: 
tags: python,docker,fastapi
//cover_image: https://direct_url_to_image.jpg
---

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

```python
from fastapi import FastAPI

app = FastAPI()

@app.get("/")
def hello():
  return "World"
```

### Dockerfile

```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"]
```

### Entrypoint

```bash
#!/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 "$@"
```

## 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.  

---

- https://www.docker.com/blog/containerized-python-development-part-1/
- https://stackoverflow.com/a/55757473/12429735
- https://github.com/nodejs/docker-node/blob/master/docker-entrypoint.sh