Now I know docker init is a thing

How to properly use Docker to containerize your Python project
docker
python
Author
Published

July 30, 2024

Modified

July 30, 2024

TIL that Docker has an example for containerizing Python apps. It uses a simple FastAPI example for demonstration. We can download the project as below:

git clone https://github.com/estebanx64/python-docker-example

After navigating to our project, we can easily set up our project for containerization with docker init. This will create:

docker init

Welcome to the Docker Init CLI!

This utility will walk you through creating the following files with sensible defaults for your project:
  - .dockerignore
  - Dockerfile
  - compose.yaml
  - README.Docker.md

Let's get started!

? What application platform does your project use? Python
? What version of Python do you want to use? (3.11.4)

? What version of Python do you want to use? 3.11.4
? What port do you want your app to listen on? (8000)

? What port do you want your app to listen on? 8000
? What is the command you use to run your app? (uvicorn 'app:app' --host=0.0.0.0 --port=8000)

? What is the command you use to run your app? uvicorn 'app:app' --host=0.0.0.0 --port=8000

✔ Created → .dockerignore
✔ Created → Dockerfile
✔ Created → compose.yaml
✔ Created → README.Docker.md

→ Your Docker files are ready!
  Review your Docker files and tailor them to your application.
  Consult README.Docker.md for information about using the generated files.

! Warning → Make sure your requirements.txt contains an entry for the uvicorn package, which is required to run your application.

What's next?
  Start your application by running → docker compose up --build
  Your application will be available at http://localhost:8000

I can access the application right after running docker compose up --build. The image buidling process was fast, and image size was only 203.21MB.

Let’s take a look at the Dockerfile. IMHO, this is not yet a optimal Dockerized Python project:

I do see some good practices here that we should run our apps in a non-privileged user rather than root as well as mount cache and bind, using Docker’s BuildKit feature, which allows more advanced mounting capabilities during build time:

  1. first --mount=type=cache,target=/root/.cache/pip option:
  1. second --mount=type=bind,source=requirements.txt,target=requirements.txt option:

In conclusion, benefits are:

  1. Faster builds: By using a cache mount for pip, subsequent builds can reuse cached packages, significantly speeding up the process.
  2. Smaller image size: The bind mount for requirements.txt means the file doesn’t need to be copied into the image, keeping the image size smaller.
  3. Better caching: Changes to requirements.txt don’t invalidate the entire layer cache, only the parts that have changed.
  4. Separation of concerns: Downloading dependencies is done as a separate step, which can be beneficial for Docker’s layer caching mechanism.
# syntax=docker/dockerfile:1

# Comments are provided throughout this file to help you get started.
# If you need more help, visit the Dockerfile reference guide at
# https://docs.docker.com/go/dockerfile-reference/

# Want to help us make this template better? Share your feedback here: https://forms.gle/ybq9Krt8jtBL3iCk7

ARG PYTHON_VERSION=3.11.4
FROM python:${PYTHON_VERSION}-slim as base

# Prevents Python from writing pyc files.
ENV PYTHONDONTWRITEBYTECODE=1

# Keeps Python from buffering stdout and stderr to avoid situations where
# the application crashes without emitting any logs due to buffering.
ENV PYTHONUNBUFFERED=1

WORKDIR /app

# Create a non-privileged user that the app will run under.
# See https://docs.docker.com/go/dockerfile-user-best-practices/
ARG UID=10001
RUN adduser \
    --disabled-password \
    --gecos "" \
    --home "/nonexistent" \
    --shell "/sbin/nologin" \
    --no-create-home \
    --uid "${UID}" \
    appuser

# Download dependencies as a separate step to take advantage of Docker's caching.
# Leverage a cache mount to /root/.cache/pip to speed up subsequent builds.
# Leverage a bind mount to requirements.txt to avoid having to copy them into
# into this layer.
RUN --mount=type=cache,target=/root/.cache/pip \
    --mount=type=bind,source=requirements.txt,target=requirements.txt \
    python -m pip install -r requirements.txt

# Switch to the non-privileged user to run the application.
USER appuser

# Copy the source code into the container.
COPY . .

# Expose the port that the application listens on.
EXPOSE 8000

# Run the application.
CMD uvicorn 'app:app' --host=0.0.0.0 --port=8000

Happy coding!