Raja's Exocortex

Run Python in Docker

Script to transparently run python inside ephemeral docker containers. All python modules are loaded from the requirements.txt file in the current folder. The host machine need not have python or any modules installed.

The script mounts /home and data from the host into the container. To avoid any file permission issues when accessing the docker host, the python interpreter is run with the same UID/GID as the current user who is running this script from the docker host.

For python scripts that are run repeatedly, this script speeds up container startup by creating a local docker image with all the python modules preinstalled. Default behavior is to build custom docker images, to disable set export DOCKER_BUILD=n.

The custom image is tagged using the md5 has of the sanitized version of the requirements.txt file.

rsubr@hanoi:~/tmp/foo$ docker images
REPOSITORY  TAG                                      IMAGE ID       CREATED          SIZE
python      latest197b4deb87ffa3decd9f045926a86cd0   be265f5c35a3   14 minutes ago   931MB

Docker images are not cleaned up, periodically run docker rmi or docker system prune.

python-docker

#!/bin/bash

# Filename: /usr/local/bin/python-docker
# Script to run python inside an ephemeral docker container
# pip install python modules if requirements.txt is preset
# for faster container startup, build a local docker image with required python modules
# local docker images will be tagged with a santised version of requirements.txt md5

#  export PYTHONVERSION=intel/oneapi-runtime  # alternate python docker image
#  export GPU_DISABLE=y    # to disable GPU/DRI acceleration
#  export DOCKER_BUILD=y   # build docker image locally for faster container startup

# Python version to run from docker hub, python:latest = 900MB, python:slim = 100MB
X_PYTHON_VERSION="${PYTHONVERSION:-python:slim}"

# Disable GPU by default, need it only for ML workloads
X_GPU_DISABLE="${GPU_DISABLE:-y}"

# By default build local docker containers for faster container startup
X_DOCKER_BUILD="${DOCKER_BUILD:-y}"


# do not continue script on errors
set -euo pipefail

# Get current user details so we can run python with this user id later
X_UID=$(id -u)
X_USERNAME=$(id -un)

# Set docker container name to curr dir or python script name and UUID
FNAME="${PWD}" ; [ "$#" -ne "0" ] && FNAME="$1"
NAME=$(realpath ${FNAME} | sed 's/^.//' | sed 's/[^a-zA-Z0-9_-]/-/g' ).$(</proc/sys/kernel/random/uuid)

# Use GPU if available, set GPU_DISABLE not required
X_DRI=""
[ "${X_GPU_DISABLE}" != "y" ] && [ -d /dev/dri ] && X_DRI="--device=/dev/dri"

# Build and tag docker image locally, run pip install for faster container startup
docker_build() {
  docker_tag=$1

  X_DOCKER_CONTEXT="$(mktemp -d)"
  # Remove temp files on exit
  trap "{ rm -rf ${X_DOCKER_CONTEXT}; }" EXIT

  cp requirements.txt ${X_DOCKER_CONTEXT}

  cat > "${X_DOCKER_CONTEXT}/Dockerfile" << EOF

FROM ${X_PYTHON_VERSION}

COPY requirements.txt /tmp

RUN PIP_DISABLE_PIP_VERSION_CHECK=1 PIP_NO_CACHE_DIR=1 PIP_ROOT_USER_ACTION=ignore pip install -qq -r /tmp/requirements.txt
EOF

  docker build -t "${docker_tag}" "${X_DOCKER_CONTEXT}" > /dev/null

  rm -rf "${X_DOCKER_CONTEXT}"
}

# Build a local docker container with python modules if required
REQUIREMENTS_MD5=""
if [ "${X_DOCKER_BUILD}" == "y" ] && [ -r requirements.txt ]; then

  # sanitise requirements.txt - strip comments, blank lines and sort lines before computing shortened md5
  REQUIREMENTS_MD5=`sed 's/\s*#.*$//;/^\s*$/d' < requirements.txt | sort | md5sum | awk '{print $1}' | cut -c1-8`


  # build local docker image if not already available
  docker image inspect "${X_PYTHON_VERSION}-${REQUIREMENTS_MD5}" > /dev/null 2>&1 || \
    docker_build "${X_PYTHON_VERSION}-${REQUIREMENTS_MD5}"

fi

# Create temporary docker ENTRYPOINT shell script
X_TMPDIR="$(mktemp -d)"
ENTRYPOINT="${X_TMPDIR}/entrypoint.sh"
# Remove temp files on exit
trap "{ rm -rf ${X_TMPDIR}; }" EXIT

cat > "${ENTRYPOINT}" <<EOF
#!/bin/bash

# Alternate entrypoint for python docker image
# Run pip as root, then run python as X_USERNAME

# do not continue script on errors
set -euo pipefail

# Optimize pip for docker usage
export PIP_DISABLE_PIP_VERSION_CHECK=1
export PIP_NO_CACHE_DIR=1
export PIP_ROOT_USER_ACTION=ignore

# pip install modules only if DOCKER_BUILD is not used
[ ! -f /tmp/requirements.txt ] && [ -f requirements.txt ] && pip install -qq -r requirements.txt

# create the current user in the container, and add to video group to access /dev/dri/card0
useradd -u ${X_UID} -U -s /bin/bash -M ${X_USERNAME}
usermod -a -G video ${X_USERNAME}

# run python
exec su -c "python \"\$@\"" ${X_USERNAME}
EOF

chmod 755 ${ENTRYPOINT}

# Run python in docker with all volume mounts and options

docker run -it --rm \
        -v /etc/timezone:/etc/timezone:ro \
        -v /etc/localtime:/etc/localtime:ro \
        -v /data:/data -v /home:/home \
        -v ${X_TMPDIR}:${X_TMPDIR} \
        -w $(pwd) \
        --entrypoint "${ENTRYPOINT}" \
        --name ${NAME} \
        --net=host ${X_DRI} \
        ${X_PYTHON_VERSION}-${REQUIREMENTS_MD5} "$@"

TODO

#TODO: test GPU_DISABLE #TODO: Use local PIP mirror if configured, see proxypi-pip-cache

# Use local pip cache
X_INDEX_URL='http://192.168.1.15:5000/index/'
X_TRUSTED_HOST='192.168.1.15'

pip install -qq --index-url=${X_INDEX_URL} --trusted-host ${X_TRUSTED_HOST} -r requirements.txt