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:
After navigating to our project, we can easily set up our project for containerization with docker init
. This will create:
.dockerignore
Dockerfile
compose.yaml
README.Docker.md
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:
- Lack off a dependency management tool (for e.g,
pipenv
,poetry
both are good, production-ready); - This is single-step builder. Should we split it to
dependencies
andruntime
steps, which will limit the objects in theruntime
image to only those needed to run the application?
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:
- first
--mount=type=cache,target=/root/.cache/pip
option:
- This creates a cache mount for pip’s cache directory;
- It speeds up subsequent builds by reusing cached pip packages;
- The cache persists between builds, saving time and bandwidth.
- second
--mount=type=bind,source=requirements.txt,target=requirements.txt
option:
- This creates a bind mount for the
requirements.txt
file; - It allows access to the
requirements.txt
file without copying it into the image layer; - This is useful for keeping the image size smaller and allowing changes to
requirements.txt
without rebuilding all layers.
In conclusion, benefits are:
- Faster builds: By using a cache mount for pip, subsequent builds can reuse cached packages, significantly speeding up the process.
- 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. - Better caching: Changes to
requirements.txt
don’t invalidate the entire layer cache, only the parts that have changed. - 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!