Post

Docker images for python

Docker is a containerization platform that allows developers to package applications and their dependencies into containers. Containers provide an isolated environment that runs the application the same way across different systems. In this post I’m going to provide some docker images to run on Python.

The code for this post is in my repository blogging-code, subdirectory python-dockerfiles.

Install Docker and Colima on MacOS

First we need docker as the engine

1
2
3
brew install docker
brew link docker
docker --version

And then install Colima which is an open source alternative to Docker Desktop.

1
brew install colima

Start colima as the engine for docker

1
brew start colima

When you no longer need your containers running make sure to do brew stop colima.

Python on Debian-based distributions

From Python DockerHub page we have several options to crate a customized docker image. Let’s say wa want to run an application in python 3.12. Normally you would pick between the images python:3.12, python:3.12-slim or python:3.12-alpine. The first image is the largest ~915MB, and contains more libraries and tools than needed for running basic python (although not specified in the docs). The second image (the slim) is a simplified version of the first one and weights about 45MB, the latter (alpine) is an even more minimal version with size about 24MB. We won’t use the alpine, it is so basic we can’t even add users, also wouldn’t be able to install numpy and other packages since numpy is based on glibc library and uses musl libc.

Python-slim

To create new custom images we write Dockerfiles. Those are plaintext files that tell docker how to build the image. For the first example create a file named Dockerfile-python-3.12-slim with the following content

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
FROM python:3.12-slim

ARG USERNAME=user
ARG UID=1000
ARG GID=1000

RUN if getent group ${GID} >/dev/null; then \
        echo "Group with GID ${GID} already exists, using it."; \
        GROUP_NAME=$(getent group ${GID} | cut -d: -f1); \
    else \
        GROUP_NAME=${USERNAME}; \
        groupadd --gid ${GID} ${GROUP_NAME}; \
    fi && \
    useradd --uid ${UID} --gid ${GID} --create-home --shell /bin/bash ${USERNAME}

WORKDIR /home/${USERNAME}
RUN chown -R ${UID}:${GID} /home/${USERNAME}

USER ${USERNAME}
CMD ["/bin/bash"]

This starts from the image python:3.12-slim that docker will pull from dockerhub. The instructions are to create a new group if it doesn’t exist and add a user. Why we do this?. Docker normally operates as root so if you create a new image and a container from it, you will be root. In this case if you mount a sensitive directory from your system (imagine you mount /usr/lib) and accidentally delete it in your container, you would be in trouble. A workaround I found for this not to happen is to crate a user in the container that is the same you have in the host with the same group id, thus removing the root by default.

The last part of the dockerfile sets the working directory (home for the user) and changes the privileges of it. Finally the command USER sets the user that will be used when you execute a container.

Now we can build and run the image with

1
2
3
4
5
docker build -f Docker/Dockerfile-python-3.12 \
            --build-arg USERNAME=$(whoami) \
            --build-arg UID=$(id -u) \
            --build-arg GID=$(id -g) \
            -t python-3.12-image .

And then run and ssh into it with

1
docker run -it python-3.12-image /bin/bash

That’s it, now you are in the container, type python --version to check that your version is 3.12. In another terminal check the images and containers you have in the system

1
2
3
4
5
# show all containers (running and stopped)
docker ps -a 

# show images
docker images

stop the container and remove the image with

1
2
docker rm container
docker rmi python-3.12-image

Then prune

1
2
docker container prune
docker image prune

Python compiled

In this case we will download and install python from source on docker, open a document with name Dockerfile-python-3.12-build, pick your version from FTP Python page.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
FROM ubuntu:latest

ARG USERNAME=user
ARG UID=1000
ARG GID=1000

ENV PYTHON_VERSION=3.12.9

# install required packages to compile python
RUN set -x \
    && echo "Updating..." \
    && apt-get upgrade \
    && apt-get update \
    && echo "Installing Packages..." \
    && apt-get install -y \
    build-essential \
    zlib1g-dev \
    libncurses5-dev \
    libgdbm-dev \
    libnss3-dev \
    libssl-dev \
    libsqlite3-dev \
    libreadline-dev \
    libffi-dev curl \
    libbz2-dev \
    liblzma-dev \
    wget

# download and compile python
RUN cd usr/src && \
    PYTHON_VERSION_SHORT=${PYTHON_VERSION%.*} && \
    wget https://www.python.org/ftp/python/${PYTHON_VERSION}/Python-${PYTHON_VERSION}.tgz && \
    tar xzf Python-${PYTHON_VERSION}.tgz && \
    cd Python-${PYTHON_VERSION} && \
    ./configure --enable-optimizations && \
    make -j 16 && \
    make altinstall && \
    ln -s /usr/local/bin/python${PYTHON_VERSION_SHORT} /usr/bin/python && \
    cd / && \
    rm -rf /usr/src/Python-${PYTHON_VERSION}.tgz /usr/src/

RUN if getent group ${GID} >/dev/null; then \
        echo "Group with GID ${GID} already exists, using it."; \
        GROUP_NAME=$(getent group ${GID} | cut -d: -f1); \
    else \
        GROUP_NAME=${USERNAME}; \
        groupadd --gid ${GID} ${GROUP_NAME}; \
    fi && \
    useradd --uid ${UID} --gid ${GID} --create-home --shell /bin/bash ${USERNAME}

WORKDIR /home/${USERNAME}
RUN chown -R ${UID}:${GID} /home/${USERNAME}

USER ${USERNAME}
CMD ["/bin/bash"]

Mamba & Conda

A third docker image that can be useful is one containing mamba and conda. A container to do data science perhaps. I do not recommend this container to run apps or microservices. Let’s name the dockerfile as Dockerfile-mamba.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
FROM ubuntu:latest

ARG USERNAME=user
ARG UID=1000
ARG GID=1000

RUN set -x \
    && echo "Updating..." \
    && apt-get upgrade \
    && apt-get update \
    && echo "Installing Packages..." \
    && apt-get install -y \
    wget 

# Define Miniforge version and install path
ENV MINIFORGE_PATH=/opt/miniforge

# Download and install Miniforge
RUN wget https://github.com/conda-forge/miniforge/releases/latest/download/Miniforge3-Linux-x86_64.sh -O /tmp/Miniforge.sh \
    && bash /tmp/Miniforge.sh -b -p $MINIFORGE_PATH \
    && rm /tmp/Miniforge.sh

# Set environment variables for Conda and Mamba
ENV PATH="$MINIFORGE_PATH/bin:$PATH"

RUN if getent group ${GID} >/dev/null; then \
        echo "Group with GID ${GID} already exists, using it."; \
        GROUP_NAME=$(getent group ${GID} | cut -d: -f1); \
    else \
        GROUP_NAME=${USERNAME}; \
        groupadd --gid ${GID} ${GROUP_NAME}; \
    fi && \
    useradd --uid ${UID} --gid ${GID} --create-home --shell /bin/bash ${USERNAME}

WORKDIR /home/${USERNAME}
RUN chown -R ${UID}:${GID} /home/${USERNAME}

USER ${USERNAME}
CMD ["/bin/bash"]

Docker image with UV

Following the previous pattern you may not be surprised about this one

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
FROM ubuntu:latest

ARG USERNAME=user
ARG UID=1000
ARG GID=1000

RUN set -x \
    && echo "Updating..." \
    && apt-get upgrade \
    && apt-get update \
    && echo "Installing Packages..." \
    && apt-get install -y \
    wget \
    curl

RUN if getent group ${GID} >/dev/null; then \
        echo "Group with GID ${GID} already exists, using it."; \
        GROUP_NAME=$(getent group ${GID} | cut -d: -f1); \
    else \
        GROUP_NAME=${USERNAME}; \
        groupadd --gid ${GID} ${GROUP_NAME}; \
    fi && \
    useradd --uid ${UID} --gid ${GID} --create-home --shell /bin/bash ${USERNAME}

WORKDIR /home/${USERNAME}
RUN chown -R ${UID}:${GID} /home/${USERNAME}

USER ${USERNAME}

# install UV on user
RUN curl -LsSf https://astral.sh/uv/install.sh | sh -s -- --verbose 

CMD ["/bin/bash"]

Here instead of installing uv on root we install it on the user (goes after the statement USER).

Building and running the images

As usual I have a quick recipe to build these images, a bash file (I will name it as build-run.sh) that can be used for the different builds

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
#!/bin/bash

THIS_DIR=$(dirname "$(realpath "$0")")

# # Dockerfile name
# DOCKERFILE=python-3.12
# DOCKERFILE=python-3.12-slim
# DOCKERFILE=python-3.12-build
# DOCKERFILE=mamba
DOCKERFILE=uv


build_image(){
    docker build -f Docker/Dockerfile-${DOCKERFILE} \
                --build-arg USERNAME=$(whoami) \
                --build-arg UID=$(id -u) \
                --build-arg GID=$(id -g) \
                 -t ${DOCKERFILE}-image .
}

run_image(){
    docker run \
    -v ${THIS_DIR}:/home/$(whoami) \
    -it \
    ${DOCKERFILE}-image \
     /bin/bash
}

croak(){
    echo "[ERROR] $*" > /dev/stderr
    exit 1
}

main(){
    if [[ -z "$TASK" ]]; then
        croak "No TASK specified."
    fi
    echo "[INFO] running $TASK $*"
    $TASK "$@"
}

main "$@"

simple uncomment the dockerfile you want to build (here we uncomment uv) and run the following commands to build an ssh

1
2
3
4
5
export TASK=build_image
./build-run.sh

export TASK=run_image
./build-run.sh

Place the dockerfiles under Docker directory as we show in the repository.

This post is licensed under CC BY 4.0 by the author.