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"
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
#!/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.
Top comments (3)
Why do you need "USER root"?
I didn't know that nodejs has best practices for docker. I wish ruby had too.
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.