Fredrb's Blog

Tiny Python Docker Images

I was looking for a way to create a very small docker image for a Python project. The python project I created is a Telegram bot quizzes me in German. The python-telegram-bot pip package requires gcc to fully build- so using alpine image did not do the trick for me. I searched online, found a few example bot none that would actually do what I needed with a reasonable image size.

I was able to achieve what I wanted by using python:3.8 image to build the dependencies and a python:3.8-alpine as base for container image. Some examples I found were doing that, but the trick to bridge the dependencies between one image and the other was to use a virtualenv and copy all dependencies to the target image. Maybe there is a way to do this without using virtualenv and copying the packages directly to the filesystem python uses.

Here’s how the Dockerfile looks like at the end of the process:

FROM python:3.8 as base

COPY requirements.txt /requirements.txt
RUN mkdir /install
RUN pip install --prefix=/install --no-cache-dir -r /requirements.txt

FROM python:3.8-alpine

RUN python3 -m venv /usr/src/app/venv

COPY --from=base /install/lib/python3.8/site-packages/ /usr/src/app/venv/lib/python3.8/site-packages/

WORKDIR /usr/src/app

COPY *.py .
COPY nouns .
COPY start.sh .

CMD ["/bin/sh", "./start.sh"]

The steps to build up the layers are as follows:

  1. Create a build image based on python:3.8
  2. Install python dependencies from requirements.txt into a folder /install
  3. Create a container image based on python-3.8-alpine
  4. Create a virtual environment using venv
  5. Copy all dependencies installed in base image /install folder to the newly created virtual environment
  6. Copy the actual python project to WORKDIR and start the process

The start.sh bash script was just a workaround to activate venv virtual environment before starting the python process. It looks something like this:

source ./venv/bin/activate
python ./main.py

Comparing with Buster

Well, it turns out my idea is good but not as good as I thought it was. Multi-stage builds for docker images seem to be a generally good idea, but in this case, alpine Linux can raise some complications specially due to it’s limited dependencies - not only for building - but running Python applications. I created to examples, one with the alpine linux multi-stage build (see above) and one using a smaller python image python:3.8-slim-buster which is a bit bigger, but for dependency heavy application that doesn’t make much of a difference.

The example I created was an application with the following dependencies:

matplotlib
pandas

These dependencies require gcc to build them. So they seemed like a good example to use.

This is how the buster Dockerfile looks like:

FROM python:3.8-slim-buster

WORKDIR /src/usr/app

COPY requirements.txt .
RUN pip install --no-cache-dir -r ./requirements.txt

COPY *.py .

CMD ["python", "main.py"]
REPOSITORY                         TAG                 IMAGE ID            CREATED             SIZE
fredrb/pyimg-test-alpine           latest              bb3d079ca5c5        5 hours ago         204MB
fredrb/pyimg-test-buster           latest              ed3b808ec1ca        5 hours ago         265MB

My Multi-stage Alpine images are still smaller, but the Dockerfile is more complex, I think it depends if these 61MB would make a big difference. In general, I think I would prefer to have the buster image with ~5 LoC Dockerfile than a more complex Dockerfile only for a 20% reduction of size.

#python #docker

⇦ Back Home | ⇧ Top |

If you hated this post, and can't keep it to yourself, consider sending me an e-mail at fred.rbittencourt@gmail.com or complain with me at Twitter X @derfrb. I'm also occasionally responsive to positive comments.