diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..d131fe2 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,38 @@ +sudo: required +services: + - docker + +matrix: + fast_finish: true + +before_install: + - bash ./.travis/main.sh + +env: + global: + - alpine_image='alpine:3.8' + - repo='jfloff/alpine-python' + - latest='3.8' + matrix: + - version='2.7' type='' + - version='2.7' type='-onbuild' + - version='2.7' type='-slim' + - version='3.6' type='' + - version='3.6' type='-onbuild' + - version='3.6' type='-slim' + - version='3.7' type='' + - version='3.7' type='-onbuild' + - version='3.7' type='-slim' + - version='3.8' type='' + - version='3.8' type='-onbuild' + - version='3.8' type='-slim' + +before_script: + - sudo docker pull $alpine_image + +script: + - sudo docker build -t $repo:$version$type $version$type + - echo 'print("Success")' | sudo docker run -i --rm $repo:$version$type + +after_success: + - bash ./.travis/push.sh diff --git a/.travis/main.sh b/.travis/main.sh new file mode 100755 index 0000000..afb52e0 --- /dev/null +++ b/.travis/main.sh @@ -0,0 +1,61 @@ +#!/bin/bash + +# Set an option to exit immediately if any error appears +set -o errexit + +# Main function that describes the behavior of the +# script. +# By making it a function we can place our methods +# below and have the main execution described in a +# concise way via function invocations. +main() { + setup_dependencies + update_docker_configuration + + echo "SUCCESS: + Done! Finished setting up Travis machine. + " +} + +# Prepare the dependencies that the machine need. +# Here I'm just updating the apt references and then +# installing both python and python-pip. This allows +# us to make use of `pip` to fetch the latest `docker-compose` +# later. +# We also upgrade `docker-ce` so that we can get the +# latest docker version which allows us to perform +# image squashing as well as multi-stage builds. +setup_dependencies() { + echo "INFO: + Setting up dependencies. + " + + sudo apt update -y --allow-unauthenticated + # sudo apt install realpath python python-pip -y + sudo apt install --only-upgrade docker-ce -y + + # sudo pip install docker-compose || true + + docker info + # docker-compose --version +} + +# Tweak the daemon configuration so that we +# can make use of experimental features (like image +# squashing) as well as have a bigger amount of +# concurrent downloads and uploads. +update_docker_configuration() { + echo "INFO: + Updating docker configuration + " + + echo '{ + "experimental": true, + "storage-driver": "overlay2", + "max-concurrent-downloads": 50, + "max-concurrent-uploads": 50 +}' | sudo tee /etc/docker/daemon.json + sudo service docker restart +} + +main diff --git a/.travis/push.sh b/.travis/push.sh new file mode 100755 index 0000000..f07bf44 --- /dev/null +++ b/.travis/push.sh @@ -0,0 +1,33 @@ +#!/bin/bash +set -o errexit + +PUSH=1 + + +if [[ "$TRAVIS_BRANCH" == "master" ]]; then + if [[ -z "$DOCKER_USERNAME" || -z "$DOCKER_PASSWORD" ]]; then + echo "Skipping Docker push, no credentials specified." + PUSH=0 + else + echo "$DOCKER_PASSWORD" | sudo docker login -u "$DOCKER_USERNAME" --password-stdin + fi + + if [[ "$PUSH" -eq 1 ]]; then + echo "Pushing $repo:$version$type" + sudo docker push $repo:$version$type + else + echo "Would have pushed as $repo:$version$type" + fi + + if [[ "$latest" == "$version" ]]; then + if [[ "$PUSH" -eq 1 ]]; then + echo "Pushing this one as latest$type!" + sudo docker tag $repo:$version$type $repo:latest$type + sudo docker push $repo:latest$type + else + echo "Would have pushed as $repo:latest$type" + fi + fi + + fi +fi diff --git a/2.7-onbuild/Dockerfile b/2.7-onbuild/Dockerfile new file mode 100644 index 0000000..c374ebc --- /dev/null +++ b/2.7-onbuild/Dockerfile @@ -0,0 +1,124 @@ +FROM alpine:3.11 + +# VERSIONS +ENV ALPINE_VERSION=3.11 \ + PYTHON_VERSION=2.7.16 + +# PATHS +ENV PYTHON_PATH=/usr/local/bin/ \ + PATH="/usr/local/lib/python$PYTHON_VERSION/bin/:/usr/local/lib/pyenv/versions/$PYTHON_VERSION/bin:${PATH}" \ + # These are always installed. + # * dumb-init: a proper init system for containers, to reap zombie children + # * musl: standard C library + # * lib6-compat: compatibility libraries for glibc + # * linux-headers: commonly needed, and an unusual package name from Alpine. + # * build-base: used so we include the basic development packages (gcc) + # * bash: so we can access /bin/bash + # * git: to ease up clones of repos + # * ca-certificates: for SSL verification during Pip and easy_install + PACKAGES="\ + dumb-init \ + musl \ + libc6-compat \ + linux-headers \ + build-base \ + bash \ + git \ + ca-certificates \ + libressl-dev \ + tzdata \ + " \ + # PACKAGES needed to built python + PYTHON_BUILD_PACKAGES="\ + bzip2-dev \ + coreutils \ + dpkg-dev dpkg \ + expat-dev \ + findutils \ + gcc \ + gdbm-dev \ + libc-dev \ + libffi-dev \ + libnsl-dev \ + libtirpc-dev \ + linux-headers \ + make \ + ncurses-dev \ + libressl-dev \ + pax-utils \ + readline-dev \ + sqlite-dev \ + tcl-dev \ + tk \ + tk-dev \ + util-linux-dev \ + xz-dev \ + zlib-dev \ + git \ + " + +# Copy in the entrypoint script -- this installs prerequisites on container start. +COPY entrypoint.sh /entrypoint.sh + +RUN set -ex ;\ + # find MAJOR and MINOR python versions based on $PYTHON_VERSION + export PYTHON_MAJOR_VERSION=$(echo "${PYTHON_VERSION}" | rev | cut -d"." -f3- | rev) ;\ + export PYTHON_MINOR_VERSION=$(echo "${PYTHON_VERSION}" | rev | cut -d"." -f2- | rev) ;\ + # replacing default repositories with edge ones + echo "http://dl-cdn.alpinelinux.org/alpine/v$ALPINE_VERSION/community" >> /etc/apk/repositories ;\ + echo "http://dl-cdn.alpinelinux.org/alpine/v$ALPINE_VERSION/main" >> /etc/apk/repositories ;\ + # Add the packages, with a CDN-breakage fallback if needed + apk add --no-cache $PACKAGES || \ + (sed -i -e 's/dl-cdn/dl-4/g' /etc/apk/repositories && apk add --no-cache $PACKAGES) ;\ + # Add packages just for the python build process with a CDN-breakage fallback if needed + apk add --no-cache --virtual .build-deps $PYTHON_BUILD_PACKAGES || \ + (sed -i -e 's/dl-cdn/dl-4/g' /etc/apk/repositories && apk add --no-cache --virtual .build-deps $PYTHON_BUILD_PACKAGES) ;\ + # turn back the clock -- so hacky! + echo "http://dl-cdn.alpinelinux.org/alpine/v$ALPINE_VERSION/main/" > /etc/apk/repositories ;\ + # echo "@community http://dl-cdn.alpinelinux.org/alpine/v$ALPINE_VERSION/community" >> /etc/apk/repositories ;\ + # echo "@testing http://dl-cdn.alpinelinux.org/alpine/v$ALPINE_VERSION/testing" >> /etc/apk/repositories ;\ + # echo "@edge-main http://dl-cdn.alpinelinux.org/alpine/edge/main" >> /etc/apk/repositories ;\ + # use pyenv to download and compile specific python version + git clone --depth 1 https://github.com/pyenv/pyenv /usr/local/lib/pyenv ;\ + # install + GNU_ARCH="$(dpkg-architecture --query DEB_BUILD_GNU_TYPE)" ;\ + PYENV_ROOT=/usr/local/lib/pyenv CONFIGURE_OPTS="--build=$GNU_ARCH --enable-loadable-sqlite-extensions --enable-shared --with-system-expat --with-system-ffi --without-ensurepip --with-shared" /usr/local/lib/pyenv/bin/pyenv install $PYTHON_VERSION ;\ + # keep the needed .so files + # ignore libpython - that one comes from the pyenv instalation + find /usr/local -type f -executable -not \( -name '*tkinter*' \) -exec scanelf --needed --nobanner --format '%n#p' '{}' ';' \ + | tr ',' '\n' \ + | sort -u \ + | awk 'system("[ -e /usr/local/lib/" $1 " ]") == 0 { next } { print "so:" $1 }' \ + | grep -ve 'libpython' \ + | xargs -rt apk add --no-cache --virtual .python-rundeps ;\ + # for debug + # | xargs -n1 echo ;\ + # delete everything from pyenv except the installed version + # this throws an error but we ignore it + find /usr/local/lib/pyenv/ -mindepth 1 -name versions -prune -o -exec rm -rf {} \; || true ;\ + # delete files to to reduce container size + # tips taken from main python docker repo + find /usr/local/lib/pyenv/versions/$PYTHON_VERSION/ -depth \( -name '*.pyo' -o -name '*.pyc' -o -name 'test' -o -name 'tests' \) -exec rm -rf '{}' + ;\ + # symlink the binaries + ln -s /usr/local/lib/pyenv/versions/$PYTHON_VERSION/bin/* $PYTHON_PATH ;\ + ln -s /usr/local/lib/pyenv/versions/$PYTHON_VERSION/include/python$PYTHON_MINOR_VERSION /usr/include/ ;\ + # set timezone info + ln -fs /usr/share/zoneinfo/Etc/UTC /etc/localtime ;\ + # remove build dependencies and any leftover apk cache + apk del --no-cache --purge .build-deps ;\ + rm -rf /var/cache/apk/* ;\ + # set proper permission to run entrypoint script + chmod a+x /entrypoint.sh + +# install requirements +# this way when you build you won't need to install again +# and since COPY is cached we don't need to wait +ONBUILD COPY requirements.txt /tmp/requirements.txt + +# Run the dependencies installer and then allow it to be run again if needed. +ONBUILD RUN /entrypoint.sh -r /tmp/requirements.txt +ONBUILD RUN rm -f /requirements.installed + +# since we will be "always" mounting the volume, we can set this up +ENTRYPOINT ["/usr/bin/dumb-init"] +CMD ["python"] diff --git a/2.7-onbuild/entrypoint.sh b/2.7-onbuild/entrypoint.sh new file mode 100755 index 0000000..50c2ab1 --- /dev/null +++ b/2.7-onbuild/entrypoint.sh @@ -0,0 +1,153 @@ +#!/usr/bin/dumb-init /bin/bash +set -e + +APK_REQUIREMENTS=() +BUILD_REQUIREMENTS=() +PIP_REQUIREMENTS=() +APKFILE='/apk-requirements.txt' +BUILDFILE='/build-requirements.txt' +REQFILE='/requirements.txt' +VERBOSITY=1 + +TMP_REQFILE='/tmp/requirements.txt' + +function usage () { + cat <<"EOF" +Usage: $0 [-a -b -p -A -B -P -r -q -x] [--] + -a : APK requirement. Can be specified multiple times. + -b : APK build requirement. These will be removed at the end to save space. + -p : Pip requirement. Can be specified multiple times. + + -A : apk-requirements.txt file location, default: /apk-requirements.txt + -B : build-requirements.txt file location, default: /build-requirements.txt + -P : requirements.txt file location, default: /requirements.txt + -r : same as above, just to match Pip's -r flag. + + -q : quiet, doesn't print anything at all. + -x : Bash debug mode. Extremely verbose! + + -- : Separator for flags and your command + + Whatever you provide after your arguments is run at the end. +EOF + exit 1 +} + +function vlog () { + if [ $VERBOSITY -gt 0 ]; then + echo $1 + fi +} + +# Get and process arguments +while getopts ":a:b:p:A:B:P:r:qx" opt; do + case $opt in + a) APK_REQUIREMENTS+=("$OPTARG") ;; + b) BUILD_REQUIREMENTS+=("$OPTARG") ;; + p) PIP_REQUIREMENTS+=("$OPTARG") ;; + A) APKFILE="$OPTARG" ;; + B) BUILDFILE="$OPTARG" ;; + P) REQFILE="$OPTARG" ;; + r) REQFILE="$OPTARG" ;; + q) VERBOSITY=0 ;; + x) set -x ;; + \?) + echo "Invalid option: -$OPTARG" >&2 + usage + ;; + :) + echo "Option -$OPTARG requires an argument." >&2 + usage + ;; + esac +done + +# Bad arguments +if [ $? -ne 0 ]; +then + usage +fi + +# Strip out all the arguments that have been processed +shift $((OPTIND-1)) + +# If there's a double dash at the end, get that off +[[ $1 = "--" ]] && shift + +# Make some common flags objects +PIP_FLAGS='' +if [ $VERBOSITY -eq 0 ]; then + PIP_FLAGS="$PIP_FLAGS -q" +fi + +APK_FLAGS='--no-cache --no-progress' +if [ $VERBOSITY -eq 0 ]; then + APK_FLAGS="$APK_FLAGS -q" +fi + +# Don't do anything if we've already done this. +if [[ ! -f /requirements.installed ]]; then + vlog "First run, checking for any requirements..." + + # Install any APK requirements + if [[ -f "$APKFILE" ]]; then + vlog "APK requirements file detected!" + APK_REQUIREMENTS+=($( cat "$APKFILE" )) + fi + + if [[ -f "$BUILDFILE" ]]; then + vlog "Build requirements file detected!" + BUILD_REQUIREMENTS+=($( cat "$BUILDFILE" )) + fi + + # Unfortunately the Alpine repositories are in a slightly inconsistent state for now-- python2 only exists in 'edge', not main. + # if [[ "$PYTHON_VERSION" == '2' ]]; then BUILD_PACKAGES="$(echo $BUILD_PACKAGES | sed -e 's/python2/python/g')"; fi \ + vlog "Installing all APK requirements..." + apk add $APK_FLAGS $BUILD_PACKAGES "${APK_REQUIREMENTS[@]}" "${BUILD_REQUIREMENTS[@]}" + + # Install any Pip requirements + if [[ -f "$REQFILE" && "$(cat $REQFILE | wc -l)" -gt 0 ]]; then + # Do this check a little early-- since we merge cli in with file, + # we'd get a false positive for logging otherwise. + vlog "Pip requirements file detected!" + fi + + # If we use CLI parameters, we'll have to reassign this. + TARGET_REQFILE="$REQFILE" + if [[ ${#PIP_REQUIREMENTS[@]} -gt 0 ]]; then + # Put all Pip requirements into the same file. + printf "%s\n" "${PIP_REQUIREMENTS[@]}" >> "$TMP_REQFILE" + + if [[ -f "$REQFILE" && "$(cat $REQFILE | wc -l)" -gt 0 ]]; then + cat "$REQFILE" >> "$TMP_REQFILE" + fi + + TARGET_REQFILE="$TMP_REQFILE" + fi + + if [[ -f $TARGET_REQFILE && "$(cat $TARGET_REQFILE | wc -l)" -gt 0 ]]; then + vlog "Upgrading Pip..." + pip install $PIP_FLAGS --upgrade pip + vlog "Installing all Pip requirements..." + pip install $PIP_FLAGS -r "$TARGET_REQFILE" + fi + + # Remove packages that were only required for build. + apk del $APK_FLAGS $BUILD_PACKAGES "${BUILD_REQUIREMENTS[@]}" + + vlog "Installing APK requirements..." + apk add $APK_FLAGS "${APK_REQUIREMENTS[@]}" + + touch /requirements.installed +else + vlog "/requirements.installed file exists-- skipping requirements installs." +fi + + +if [[ ! -z "$@" ]]; then + # If the user has given us a command, run it. + $@ +else + # Otherwise, default to running 'python'. + python +fi diff --git a/2.7-slim/Dockerfile b/2.7-slim/Dockerfile new file mode 100644 index 0000000..d31f322 --- /dev/null +++ b/2.7-slim/Dockerfile @@ -0,0 +1,114 @@ +FROM alpine:3.11 + +# VERSIONS +ENV ALPINE_VERSION=3.11 \ + PYTHON_VERSION=2.7.16 + +ENV PYTHON_PATH=/usr/local/bin/ \ + PATH="/usr/local/lib/python$PYTHON_VERSION/bin/:/usr/local/lib/pyenv/versions/$PYTHON_VERSION/bin:${PATH}" \ + # These are always installed. Notes: + # * dumb-init: a proper init system for containers, to reap zombie children + # * bash: For entrypoint, and debugging + # * ca-certificates: for SSL verification during Pip and easy_install + PACKAGES="\ + dumb-init \ + bash \ + ca-certificates \ + " \ + # PACKAGES needed to built python + PYTHON_BUILD_PACKAGES="\ + bzip2-dev \ + coreutils \ + dpkg-dev dpkg \ + expat-dev \ + findutils \ + gcc \ + gdbm-dev \ + libc-dev \ + libffi-dev \ + libnsl-dev \ + libtirpc-dev \ + linux-headers \ + make \ + ncurses-dev \ + libressl-dev \ + pax-utils \ + readline-dev \ + sqlite-dev \ + tcl-dev \ + tk \ + tk-dev \ + util-linux-dev \ + xz-dev \ + zlib-dev \ + git \ + " \ + # These packages are not installed immediately, but are added at runtime or ONBUILD to shrink the image as much as possible. Notes: + # * build-base: used so we include the basic development packages (gcc) + # * linux-headers: commonly needed, and an unusual package name from Alpine. + # * lib6-compat: compatibility libraries for glibc + # * git: to ease up clones of repos + BUILD_PACKAGES="\ + build-base \ + linux-headers \ + libc6-compat \ + git \ + " + +# Copy in the entrypoint script -- this installs prerequisites on container start. +COPY entrypoint.sh /entrypoint.sh + +RUN set -ex ;\ + # find MAJOR and MINOR python versions based on $PYTHON_VERSION + export PYTHON_MAJOR_VERSION=$(echo "${PYTHON_VERSION}" | rev | cut -d"." -f3- | rev) ;\ + export PYTHON_MINOR_VERSION=$(echo "${PYTHON_VERSION}" | rev | cut -d"." -f2- | rev) ;\ + # replacing default repositories with edge ones + echo "http://dl-cdn.alpinelinux.org/alpine/v$ALPINE_VERSION/community" >> /etc/apk/repositories ;\ + echo "http://dl-cdn.alpinelinux.org/alpine/v$ALPINE_VERSION/main" >> /etc/apk/repositories ;\ + # Add the packages, with a CDN-breakage fallback if needed + apk add --no-cache $PACKAGES || \ + (sed -i -e 's/dl-cdn/dl-4/g' /etc/apk/repositories && apk add --no-cache $PACKAGES) ;\ + # Add packages just for the python build process with a CDN-breakage fallback if needed + apk add --no-cache --virtual .build-deps $PYTHON_BUILD_PACKAGES || \ + (sed -i -e 's/dl-cdn/dl-4/g' /etc/apk/repositories && apk add --no-cache --virtual .build-deps $PYTHON_BUILD_PACKAGES) ;\ + # turn back the clock -- so hacky! + echo "http://dl-cdn.alpinelinux.org/alpine/v$ALPINE_VERSION/main/" > /etc/apk/repositories ;\ + # echo "@community http://dl-cdn.alpinelinux.org/alpine/v$ALPINE_VERSION/community" >> /etc/apk/repositories ;\ + # echo "@testing http://dl-cdn.alpinelinux.org/alpine/v$ALPINE_VERSION/testing" >> /etc/apk/repositories ;\ + # echo "@edge-main http://dl-cdn.alpinelinux.org/alpine/edge/main" >> /etc/apk/repositories ;\ + # use pyenv to download and compile specific python version + git clone --depth 1 https://github.com/pyenv/pyenv /usr/local/lib/pyenv ;\ + # install + GNU_ARCH="$(dpkg-architecture --query DEB_BUILD_GNU_TYPE)" ;\ + PYENV_ROOT=/usr/local/lib/pyenv CONFIGURE_OPTS="--build=$GNU_ARCH --enable-loadable-sqlite-extensions --enable-shared --with-system-expat --with-system-ffi --without-ensurepip --with-shared" /usr/local/lib/pyenv/bin/pyenv install $PYTHON_VERSION ;\ + # keep the needed .so files + # ignore libpython - that one comes from the pyenv instalation + find /usr/local -type f -executable -not \( -name '*tkinter*' \) -exec scanelf --needed --nobanner --format '%n#p' '{}' ';' \ + | tr ',' '\n' \ + | sort -u \ + | awk 'system("[ -e /usr/local/lib/" $1 " ]") == 0 { next } { print "so:" $1 }' \ + | grep -ve 'libpython' \ + | xargs -rt apk add --no-cache --virtual .python-rundeps ;\ + # for debug + # | xargs -n1 echo ;\ + # delete everything from pyenv except the installed version + # this throws an error but we ignore it + find /usr/local/lib/pyenv/ -mindepth 1 -name versions -prune -o -exec rm -rf {} \; || true ;\ + # delete files to to reduce container size + # tips taken from main python docker repo + find /usr/local/lib/pyenv/versions/$PYTHON_VERSION/ -depth \( -name '*.pyo' -o -name '*.pyc' -o -name 'test' -o -name 'tests' \) -exec rm -rf '{}' + ;\ + # symlink the binaries + ln -s /usr/local/lib/pyenv/versions/$PYTHON_VERSION/bin/* $PYTHON_PATH ;\ + ln -s /usr/local/lib/pyenv/versions/$PYTHON_VERSION/include/python$PYTHON_MINOR_VERSION /usr/include/ ;\ + # remove build dependencies and any leftover apk cache + apk del --no-cache --purge .build-deps ;\ + rm -rf /var/cache/apk/* ;\ + # set proper permission to run entrypoint script + chmod a+x /entrypoint.sh + +# This script installs APK and Pip prerequisites on container start, or ONBUILD. Notes: +# * Reads the -a flags and /apk-requirements.txt for install requests +# * Reads the -b flags and /build-requirements.txt for build packages -- removed when build is complete +# * Reads the -p flags and /requirements.txt for Pip packages +# * Reads the -r flag to specify a different file path for /requirements.txt +ENTRYPOINT ["/usr/bin/dumb-init", "bash", "/entrypoint.sh"] diff --git a/2.7-slim/entrypoint.sh b/2.7-slim/entrypoint.sh new file mode 100755 index 0000000..50c2ab1 --- /dev/null +++ b/2.7-slim/entrypoint.sh @@ -0,0 +1,153 @@ +#!/usr/bin/dumb-init /bin/bash +set -e + +APK_REQUIREMENTS=() +BUILD_REQUIREMENTS=() +PIP_REQUIREMENTS=() +APKFILE='/apk-requirements.txt' +BUILDFILE='/build-requirements.txt' +REQFILE='/requirements.txt' +VERBOSITY=1 + +TMP_REQFILE='/tmp/requirements.txt' + +function usage () { + cat <<"EOF" +Usage: $0 [-a -b -p -A -B -P -r -q -x] [--] + -a : APK requirement. Can be specified multiple times. + -b : APK build requirement. These will be removed at the end to save space. + -p : Pip requirement. Can be specified multiple times. + + -A : apk-requirements.txt file location, default: /apk-requirements.txt + -B : build-requirements.txt file location, default: /build-requirements.txt + -P : requirements.txt file location, default: /requirements.txt + -r : same as above, just to match Pip's -r flag. + + -q : quiet, doesn't print anything at all. + -x : Bash debug mode. Extremely verbose! + + -- : Separator for flags and your command + + Whatever you provide after your arguments is run at the end. +EOF + exit 1 +} + +function vlog () { + if [ $VERBOSITY -gt 0 ]; then + echo $1 + fi +} + +# Get and process arguments +while getopts ":a:b:p:A:B:P:r:qx" opt; do + case $opt in + a) APK_REQUIREMENTS+=("$OPTARG") ;; + b) BUILD_REQUIREMENTS+=("$OPTARG") ;; + p) PIP_REQUIREMENTS+=("$OPTARG") ;; + A) APKFILE="$OPTARG" ;; + B) BUILDFILE="$OPTARG" ;; + P) REQFILE="$OPTARG" ;; + r) REQFILE="$OPTARG" ;; + q) VERBOSITY=0 ;; + x) set -x ;; + \?) + echo "Invalid option: -$OPTARG" >&2 + usage + ;; + :) + echo "Option -$OPTARG requires an argument." >&2 + usage + ;; + esac +done + +# Bad arguments +if [ $? -ne 0 ]; +then + usage +fi + +# Strip out all the arguments that have been processed +shift $((OPTIND-1)) + +# If there's a double dash at the end, get that off +[[ $1 = "--" ]] && shift + +# Make some common flags objects +PIP_FLAGS='' +if [ $VERBOSITY -eq 0 ]; then + PIP_FLAGS="$PIP_FLAGS -q" +fi + +APK_FLAGS='--no-cache --no-progress' +if [ $VERBOSITY -eq 0 ]; then + APK_FLAGS="$APK_FLAGS -q" +fi + +# Don't do anything if we've already done this. +if [[ ! -f /requirements.installed ]]; then + vlog "First run, checking for any requirements..." + + # Install any APK requirements + if [[ -f "$APKFILE" ]]; then + vlog "APK requirements file detected!" + APK_REQUIREMENTS+=($( cat "$APKFILE" )) + fi + + if [[ -f "$BUILDFILE" ]]; then + vlog "Build requirements file detected!" + BUILD_REQUIREMENTS+=($( cat "$BUILDFILE" )) + fi + + # Unfortunately the Alpine repositories are in a slightly inconsistent state for now-- python2 only exists in 'edge', not main. + # if [[ "$PYTHON_VERSION" == '2' ]]; then BUILD_PACKAGES="$(echo $BUILD_PACKAGES | sed -e 's/python2/python/g')"; fi \ + vlog "Installing all APK requirements..." + apk add $APK_FLAGS $BUILD_PACKAGES "${APK_REQUIREMENTS[@]}" "${BUILD_REQUIREMENTS[@]}" + + # Install any Pip requirements + if [[ -f "$REQFILE" && "$(cat $REQFILE | wc -l)" -gt 0 ]]; then + # Do this check a little early-- since we merge cli in with file, + # we'd get a false positive for logging otherwise. + vlog "Pip requirements file detected!" + fi + + # If we use CLI parameters, we'll have to reassign this. + TARGET_REQFILE="$REQFILE" + if [[ ${#PIP_REQUIREMENTS[@]} -gt 0 ]]; then + # Put all Pip requirements into the same file. + printf "%s\n" "${PIP_REQUIREMENTS[@]}" >> "$TMP_REQFILE" + + if [[ -f "$REQFILE" && "$(cat $REQFILE | wc -l)" -gt 0 ]]; then + cat "$REQFILE" >> "$TMP_REQFILE" + fi + + TARGET_REQFILE="$TMP_REQFILE" + fi + + if [[ -f $TARGET_REQFILE && "$(cat $TARGET_REQFILE | wc -l)" -gt 0 ]]; then + vlog "Upgrading Pip..." + pip install $PIP_FLAGS --upgrade pip + vlog "Installing all Pip requirements..." + pip install $PIP_FLAGS -r "$TARGET_REQFILE" + fi + + # Remove packages that were only required for build. + apk del $APK_FLAGS $BUILD_PACKAGES "${BUILD_REQUIREMENTS[@]}" + + vlog "Installing APK requirements..." + apk add $APK_FLAGS "${APK_REQUIREMENTS[@]}" + + touch /requirements.installed +else + vlog "/requirements.installed file exists-- skipping requirements installs." +fi + + +if [[ ! -z "$@" ]]; then + # If the user has given us a command, run it. + $@ +else + # Otherwise, default to running 'python'. + python +fi diff --git a/2.7/Dockerfile b/2.7/Dockerfile index 94f8a34..01384e4 100644 --- a/2.7/Dockerfile +++ b/2.7/Dockerfile @@ -1,28 +1,110 @@ -FROM alpine:latest +FROM alpine:3.11 -# Adds testing package to repositories -# Install needed packages. Notes: -# * build-base: used so we include the basic development packages (gcc) -# * python-dev: are used for gevent e.g. -# * bash: so we can access /bin/bash -RUN echo "@testing http://dl-4.alpinelinux.org/alpine/edge/testing" >> /etc/apk/repositories \ - && apk add --update \ - musl \ - build-base \ - bash \ - git \ - python \ - python-dev \ - py-pip \ - && pip install --upgrade pip \ - && rm /var/cache/apk/* +# VERSIONS +ENV ALPINE_VERSION=3.11 \ + PYTHON_VERSION=2.7.16 -# make some useful symlinks that are expected to exist -RUN cd /usr/bin \ - && ln -sf easy_install-2.7 easy_install \ - && ln -sf python2.7 python \ - && ln -sf python2.7-config python-config \ - && ln -sf pip2.7 pip +# PATHS +ENV PYTHON_PATH=/usr/local/bin/ \ + PATH="/usr/local/lib/python$PYTHON_VERSION/bin/:/usr/local/lib/pyenv/versions/$PYTHON_VERSION/bin:${PATH}" \ + # These are always installed. + # * dumb-init: a proper init system for containers, to reap zombie children + # * musl: standard C library + # * lib6-compat: compatibility libraries for glibc + # * linux-headers: commonly needed, and an unusual package name from Alpine. + # * build-base: used so we include the basic development packages (gcc) + # * bash: so we can access /bin/bash + # * git: to ease up clones of repos + # * ca-certificates: for SSL verification during Pip and easy_install + PACKAGES="\ + dumb-init \ + musl \ + libc6-compat \ + linux-headers \ + build-base \ + bash \ + git \ + ca-certificates \ + libressl-dev \ + tzdata \ + " \ + # PACKAGES needed to built python + PYTHON_BUILD_PACKAGES="\ + bzip2-dev \ + coreutils \ + dpkg-dev dpkg \ + expat-dev \ + findutils \ + gcc \ + gdbm-dev \ + libc-dev \ + libffi-dev \ + libnsl-dev \ + libtirpc-dev \ + linux-headers \ + make \ + ncurses-dev \ + libressl-dev \ + pax-utils \ + readline-dev \ + sqlite-dev \ + tcl-dev \ + tk \ + tk-dev \ + util-linux-dev \ + xz-dev \ + zlib-dev \ + git \ + " + +RUN set -ex ;\ + # find MAJOR and MINOR python versions based on $PYTHON_VERSION + export PYTHON_MAJOR_VERSION=$(echo "${PYTHON_VERSION}" | rev | cut -d"." -f3- | rev) ;\ + export PYTHON_MINOR_VERSION=$(echo "${PYTHON_VERSION}" | rev | cut -d"." -f2- | rev) ;\ + # replacing default repositories with edge ones + echo "http://dl-cdn.alpinelinux.org/alpine/v$ALPINE_VERSION/community" >> /etc/apk/repositories ;\ + echo "http://dl-cdn.alpinelinux.org/alpine/v$ALPINE_VERSION/main" >> /etc/apk/repositories ;\ + # Add the packages, with a CDN-breakage fallback if needed + apk add --no-cache $PACKAGES || \ + (sed -i -e 's/dl-cdn/dl-4/g' /etc/apk/repositories && apk add --no-cache $PACKAGES) ;\ + # Add packages just for the python build process with a CDN-breakage fallback if needed + apk add --no-cache --virtual .build-deps $PYTHON_BUILD_PACKAGES || \ + (sed -i -e 's/dl-cdn/dl-4/g' /etc/apk/repositories && apk add --no-cache --virtual .build-deps $PYTHON_BUILD_PACKAGES) ;\ + # turn back the clock -- so hacky! + echo "http://dl-cdn.alpinelinux.org/alpine/v$ALPINE_VERSION/main/" > /etc/apk/repositories ;\ + # echo "@community http://dl-cdn.alpinelinux.org/alpine/v$ALPINE_VERSION/community" >> /etc/apk/repositories ;\ + # echo "@testing http://dl-cdn.alpinelinux.org/alpine/v$ALPINE_VERSION/testing" >> /etc/apk/repositories ;\ + # echo "@edge-main http://dl-cdn.alpinelinux.org/alpine/edge/main" >> /etc/apk/repositories ;\ + # use pyenv to download and compile specific python version + git clone --depth 1 https://github.com/pyenv/pyenv /usr/local/lib/pyenv ;\ + # install + GNU_ARCH="$(dpkg-architecture --query DEB_BUILD_GNU_TYPE)" ;\ + PYENV_ROOT=/usr/local/lib/pyenv CONFIGURE_OPTS="--build=$GNU_ARCH --enable-loadable-sqlite-extensions --enable-shared --with-system-expat --with-system-ffi --without-ensurepip --with-shared" /usr/local/lib/pyenv/bin/pyenv install $PYTHON_VERSION ;\ + # keep the needed .so files + # ignore libpython - that one comes from the pyenv instalation + find /usr/local -type f -executable -not \( -name '*tkinter*' \) -exec scanelf --needed --nobanner --format '%n#p' '{}' ';' \ + | tr ',' '\n' \ + | sort -u \ + | awk 'system("[ -e /usr/local/lib/" $1 " ]") == 0 { next } { print "so:" $1 }' \ + | grep -ve 'libpython' \ + | xargs -rt apk add --no-cache --virtual .python-rundeps ;\ + # for debug + # | xargs -n1 echo ;\ + # delete everything from pyenv except the installed version + # this throws an error but we ignore it + find /usr/local/lib/pyenv/ -mindepth 1 -name versions -prune -o -exec rm -rf {} \; || true ;\ + # delete files to to reduce container size + # tips taken from main python docker repo + find /usr/local/lib/pyenv/versions/$PYTHON_VERSION/ -depth \( -name '*.pyo' -o -name '*.pyc' -o -name 'test' -o -name 'tests' \) -exec rm -rf '{}' + ;\ + # symlink the binaries + ln -s /usr/local/lib/pyenv/versions/$PYTHON_VERSION/bin/* $PYTHON_PATH ;\ + ln -s /usr/local/lib/pyenv/versions/$PYTHON_VERSION/include/python$PYTHON_MINOR_VERSION /usr/include/ ;\ + # set timezone info + ln -fs /usr/share/zoneinfo/Etc/UTC /etc/localtime ;\ + # remove build dependencies and any leftover apk cache + apk del --no-cache --purge .build-deps ;\ + rm -rf /var/cache/apk/* # since we will be "always" mounting the volume, we can set this up -CMD python +ENTRYPOINT ["/usr/bin/dumb-init"] +CMD ["python"] diff --git a/2.7/onbuild/Dockerfile b/2.7/onbuild/Dockerfile deleted file mode 100644 index 54c0222..0000000 --- a/2.7/onbuild/Dockerfile +++ /dev/null @@ -1,7 +0,0 @@ -FROM jfloff/alpine-python:2.7 - -# install requirements -# this way when you build you won't need to install again -# ans since COPY is cached we don't need to wait -ONBUILD COPY ./requirements.txt /tmp/requirements.txt -ONBUILD RUN pip install -r /tmp/requirements.txt diff --git a/3.4/Dockerfile b/3.4/Dockerfile deleted file mode 100644 index d24b7cc..0000000 --- a/3.4/Dockerfile +++ /dev/null @@ -1,29 +0,0 @@ -FROM alpine:latest - -# Adds testing package to repositories -# Install needed packages. Notes: -# * build-base: used so we include the basic development packages (gcc) -# * python-dev: are used for gevent e.g. -# * bash: so we can access /bin/bash -RUN echo "@testing http://dl-4.alpinelinux.org/alpine/edge/testing" >> /etc/apk/repositories \ - && apk add --update \ - musl \ - build-base \ - python3 \ - python3-dev \ - bash \ - git \ - && pip3.4 install --upgrade pip \ - && rm /var/cache/apk/* - -# make some useful symlinks that are expected to exist -RUN cd /usr/bin \ - && ln -sf easy_install-3.4 easy_install \ - && ln -sf idle3.4 idle \ - && ln -sf pydoc3.4 pydoc \ - && ln -sf python3.4 python \ - && ln -sf python-config3.4 python-config \ - && ln -sf pip3.4 pip - -# since we will be "always" mounting the volume, we can set this up -CMD python diff --git a/3.4/onbuild/Dockerfile b/3.4/onbuild/Dockerfile deleted file mode 100644 index bc02e56..0000000 --- a/3.4/onbuild/Dockerfile +++ /dev/null @@ -1,7 +0,0 @@ -FROM jfloff/alpine-python:3.4 - -# install requirements -# this way when you build you won't need to install again -# ans since COPY is cached we don't need to wait -ONBUILD COPY ./requirements.txt /tmp/requirements.txt -ONBUILD RUN pip install -r /tmp/requirements.txt diff --git a/3.6-onbuild/Dockerfile b/3.6-onbuild/Dockerfile new file mode 100644 index 0000000..08f8787 --- /dev/null +++ b/3.6-onbuild/Dockerfile @@ -0,0 +1,124 @@ +FROM alpine:3.11 + +# VERSIONS +ENV ALPINE_VERSION=3.11 \ + PYTHON_VERSION=3.6.9 + +# PATHS +ENV PYTHON_PATH=/usr/local/bin/ \ + PATH="/usr/local/lib/python$PYTHON_VERSION/bin/:/usr/local/lib/pyenv/versions/$PYTHON_VERSION/bin:${PATH}" \ + # These are always installed. + # * dumb-init: a proper init system for containers, to reap zombie children + # * musl: standard C library + # * lib6-compat: compatibility libraries for glibc + # * linux-headers: commonly needed, and an unusual package name from Alpine. + # * build-base: used so we include the basic development packages (gcc) + # * bash: so we can access /bin/bash + # * git: to ease up clones of repos + # * ca-certificates: for SSL verification during Pip and easy_install + PACKAGES="\ + dumb-init \ + musl \ + libc6-compat \ + linux-headers \ + build-base \ + bash \ + git \ + ca-certificates \ + libressl-dev \ + tzdata \ + " \ + # PACKAGES needed to built python + PYTHON_BUILD_PACKAGES="\ + bzip2-dev \ + coreutils \ + dpkg-dev dpkg \ + expat-dev \ + findutils \ + gcc \ + gdbm-dev \ + libc-dev \ + libffi-dev \ + libnsl-dev \ + libtirpc-dev \ + linux-headers \ + make \ + ncurses-dev \ + libressl-dev \ + pax-utils \ + readline-dev \ + sqlite-dev \ + tcl-dev \ + tk \ + tk-dev \ + util-linux-dev \ + xz-dev \ + zlib-dev \ + git \ + " + +# Copy in the entrypoint script -- this installs prerequisites on container start. +COPY entrypoint.sh /entrypoint.sh + +RUN set -ex ;\ + # find MAJOR and MINOR python versions based on $PYTHON_VERSION + export PYTHON_MAJOR_VERSION=$(echo "${PYTHON_VERSION}" | rev | cut -d"." -f3- | rev) ;\ + export PYTHON_MINOR_VERSION=$(echo "${PYTHON_VERSION}" | rev | cut -d"." -f2- | rev) ;\ + # replacing default repositories with edge ones + echo "http://dl-cdn.alpinelinux.org/alpine/v$ALPINE_VERSION/community" >> /etc/apk/repositories ;\ + echo "http://dl-cdn.alpinelinux.org/alpine/v$ALPINE_VERSION/main" >> /etc/apk/repositories ;\ + # Add the packages, with a CDN-breakage fallback if needed + apk add --no-cache $PACKAGES || \ + (sed -i -e 's/dl-cdn/dl-4/g' /etc/apk/repositories && apk add --no-cache $PACKAGES) ;\ + # Add packages just for the python build process with a CDN-breakage fallback if needed + apk add --no-cache --virtual .build-deps $PYTHON_BUILD_PACKAGES || \ + (sed -i -e 's/dl-cdn/dl-4/g' /etc/apk/repositories && apk add --no-cache --virtual .build-deps $PYTHON_BUILD_PACKAGES) ;\ + # turn back the clock -- so hacky! + echo "http://dl-cdn.alpinelinux.org/alpine/v$ALPINE_VERSION/main/" > /etc/apk/repositories ;\ + # echo "@community http://dl-cdn.alpinelinux.org/alpine/v$ALPINE_VERSION/community" >> /etc/apk/repositories ;\ + # echo "@testing http://dl-cdn.alpinelinux.org/alpine/v$ALPINE_VERSION/testing" >> /etc/apk/repositories ;\ + # echo "@edge-main http://dl-cdn.alpinelinux.org/alpine/edge/main" >> /etc/apk/repositories ;\ + # use pyenv to download and compile specific python version + git clone --depth 1 https://github.com/pyenv/pyenv /usr/local/lib/pyenv ;\ + # install + GNU_ARCH="$(dpkg-architecture --query DEB_BUILD_GNU_TYPE)" ;\ + PYENV_ROOT=/usr/local/lib/pyenv CONFIGURE_OPTS="--build=$GNU_ARCH --enable-loadable-sqlite-extensions --enable-shared --with-system-expat --with-system-ffi --without-ensurepip --with-shared" /usr/local/lib/pyenv/bin/pyenv install $PYTHON_VERSION ;\ + # keep the needed .so files + # ignore libpython - that one comes from the pyenv instalation + find /usr/local -type f -executable -not \( -name '*tkinter*' \) -exec scanelf --needed --nobanner --format '%n#p' '{}' ';' \ + | tr ',' '\n' \ + | sort -u \ + | awk 'system("[ -e /usr/local/lib/" $1 " ]") == 0 { next } { print "so:" $1 }' \ + | grep -ve 'libpython' \ + | xargs -rt apk add --no-cache --virtual .python-rundeps ;\ + # for debug + # | xargs -n1 echo ;\ + # delete everything from pyenv except the installed version + # this throws an error but we ignore it + find /usr/local/lib/pyenv/ -mindepth 1 -name versions -prune -o -exec rm -rf {} \; || true ;\ + # delete files to to reduce container size + # tips taken from main python docker repo + find /usr/local/lib/pyenv/versions/$PYTHON_VERSION/ -depth \( -name '*.pyo' -o -name '*.pyc' -o -name 'test' -o -name 'tests' \) -exec rm -rf '{}' + ;\ + # symlink the binaries + ln -s /usr/local/lib/pyenv/versions/$PYTHON_VERSION/bin/* $PYTHON_PATH ;\ + ln -s /usr/local/lib/pyenv/versions/$PYTHON_VERSION/include/python$PYTHON_MINOR_VERSION /usr/include/ ;\ + # set timezone info + ln -fs /usr/share/zoneinfo/Etc/UTC /etc/localtime ;\ + # remove build dependencies and any leftover apk cache + apk del --no-cache --purge .build-deps ;\ + rm -rf /var/cache/apk/* ;\ + # set proper permission to run entrypoint script + chmod a+x /entrypoint.sh + +# install requirements +# this way when you build you won't need to install again +# and since COPY is cached we don't need to wait +ONBUILD COPY requirements.txt /tmp/requirements.txt + +# Run the dependencies installer and then allow it to be run again if needed. +ONBUILD RUN /entrypoint.sh -r /tmp/requirements.txt +ONBUILD RUN rm -f /requirements.installed + +# since we will be "always" mounting the volume, we can set this up +ENTRYPOINT ["/usr/bin/dumb-init"] +CMD ["python"] diff --git a/3.6-onbuild/entrypoint.sh b/3.6-onbuild/entrypoint.sh new file mode 100755 index 0000000..50c2ab1 --- /dev/null +++ b/3.6-onbuild/entrypoint.sh @@ -0,0 +1,153 @@ +#!/usr/bin/dumb-init /bin/bash +set -e + +APK_REQUIREMENTS=() +BUILD_REQUIREMENTS=() +PIP_REQUIREMENTS=() +APKFILE='/apk-requirements.txt' +BUILDFILE='/build-requirements.txt' +REQFILE='/requirements.txt' +VERBOSITY=1 + +TMP_REQFILE='/tmp/requirements.txt' + +function usage () { + cat <<"EOF" +Usage: $0 [-a -b -p -A -B -P -r -q -x] [--] + -a : APK requirement. Can be specified multiple times. + -b : APK build requirement. These will be removed at the end to save space. + -p : Pip requirement. Can be specified multiple times. + + -A : apk-requirements.txt file location, default: /apk-requirements.txt + -B : build-requirements.txt file location, default: /build-requirements.txt + -P : requirements.txt file location, default: /requirements.txt + -r : same as above, just to match Pip's -r flag. + + -q : quiet, doesn't print anything at all. + -x : Bash debug mode. Extremely verbose! + + -- : Separator for flags and your command + + Whatever you provide after your arguments is run at the end. +EOF + exit 1 +} + +function vlog () { + if [ $VERBOSITY -gt 0 ]; then + echo $1 + fi +} + +# Get and process arguments +while getopts ":a:b:p:A:B:P:r:qx" opt; do + case $opt in + a) APK_REQUIREMENTS+=("$OPTARG") ;; + b) BUILD_REQUIREMENTS+=("$OPTARG") ;; + p) PIP_REQUIREMENTS+=("$OPTARG") ;; + A) APKFILE="$OPTARG" ;; + B) BUILDFILE="$OPTARG" ;; + P) REQFILE="$OPTARG" ;; + r) REQFILE="$OPTARG" ;; + q) VERBOSITY=0 ;; + x) set -x ;; + \?) + echo "Invalid option: -$OPTARG" >&2 + usage + ;; + :) + echo "Option -$OPTARG requires an argument." >&2 + usage + ;; + esac +done + +# Bad arguments +if [ $? -ne 0 ]; +then + usage +fi + +# Strip out all the arguments that have been processed +shift $((OPTIND-1)) + +# If there's a double dash at the end, get that off +[[ $1 = "--" ]] && shift + +# Make some common flags objects +PIP_FLAGS='' +if [ $VERBOSITY -eq 0 ]; then + PIP_FLAGS="$PIP_FLAGS -q" +fi + +APK_FLAGS='--no-cache --no-progress' +if [ $VERBOSITY -eq 0 ]; then + APK_FLAGS="$APK_FLAGS -q" +fi + +# Don't do anything if we've already done this. +if [[ ! -f /requirements.installed ]]; then + vlog "First run, checking for any requirements..." + + # Install any APK requirements + if [[ -f "$APKFILE" ]]; then + vlog "APK requirements file detected!" + APK_REQUIREMENTS+=($( cat "$APKFILE" )) + fi + + if [[ -f "$BUILDFILE" ]]; then + vlog "Build requirements file detected!" + BUILD_REQUIREMENTS+=($( cat "$BUILDFILE" )) + fi + + # Unfortunately the Alpine repositories are in a slightly inconsistent state for now-- python2 only exists in 'edge', not main. + # if [[ "$PYTHON_VERSION" == '2' ]]; then BUILD_PACKAGES="$(echo $BUILD_PACKAGES | sed -e 's/python2/python/g')"; fi \ + vlog "Installing all APK requirements..." + apk add $APK_FLAGS $BUILD_PACKAGES "${APK_REQUIREMENTS[@]}" "${BUILD_REQUIREMENTS[@]}" + + # Install any Pip requirements + if [[ -f "$REQFILE" && "$(cat $REQFILE | wc -l)" -gt 0 ]]; then + # Do this check a little early-- since we merge cli in with file, + # we'd get a false positive for logging otherwise. + vlog "Pip requirements file detected!" + fi + + # If we use CLI parameters, we'll have to reassign this. + TARGET_REQFILE="$REQFILE" + if [[ ${#PIP_REQUIREMENTS[@]} -gt 0 ]]; then + # Put all Pip requirements into the same file. + printf "%s\n" "${PIP_REQUIREMENTS[@]}" >> "$TMP_REQFILE" + + if [[ -f "$REQFILE" && "$(cat $REQFILE | wc -l)" -gt 0 ]]; then + cat "$REQFILE" >> "$TMP_REQFILE" + fi + + TARGET_REQFILE="$TMP_REQFILE" + fi + + if [[ -f $TARGET_REQFILE && "$(cat $TARGET_REQFILE | wc -l)" -gt 0 ]]; then + vlog "Upgrading Pip..." + pip install $PIP_FLAGS --upgrade pip + vlog "Installing all Pip requirements..." + pip install $PIP_FLAGS -r "$TARGET_REQFILE" + fi + + # Remove packages that were only required for build. + apk del $APK_FLAGS $BUILD_PACKAGES "${BUILD_REQUIREMENTS[@]}" + + vlog "Installing APK requirements..." + apk add $APK_FLAGS "${APK_REQUIREMENTS[@]}" + + touch /requirements.installed +else + vlog "/requirements.installed file exists-- skipping requirements installs." +fi + + +if [[ ! -z "$@" ]]; then + # If the user has given us a command, run it. + $@ +else + # Otherwise, default to running 'python'. + python +fi diff --git a/3.6-slim/Dockerfile b/3.6-slim/Dockerfile new file mode 100644 index 0000000..976838f --- /dev/null +++ b/3.6-slim/Dockerfile @@ -0,0 +1,115 @@ +FROM alpine:3.11 + +# VERSIONS +ENV ALPINE_VERSION=3.11 \ + PYTHON_VERSION=3.6.9 + +# PATHS +ENV PYTHON_PATH=/usr/local/bin/ \ + PATH="/usr/local/lib/python$PYTHON_VERSION/bin/:/usr/local/lib/pyenv/versions/$PYTHON_VERSION/bin:${PATH}" \ + # These are always installed. Notes: + # * dumb-init: a proper init system for containers, to reap zombie children + # * bash: For entrypoint, and debugging + # * ca-certificates: for SSL verification during Pip and easy_install + PACKAGES="\ + dumb-init \ + bash \ + ca-certificates \ + " \ + # PACKAGES needed to built python + PYTHON_BUILD_PACKAGES="\ + bzip2-dev \ + coreutils \ + dpkg-dev dpkg \ + expat-dev \ + findutils \ + gcc \ + gdbm-dev \ + libc-dev \ + libffi-dev \ + libnsl-dev \ + libtirpc-dev \ + linux-headers \ + make \ + ncurses-dev \ + libressl-dev \ + pax-utils \ + readline-dev \ + sqlite-dev \ + tcl-dev \ + tk \ + tk-dev \ + util-linux-dev \ + xz-dev \ + zlib-dev \ + git \ + " \ + # These packages are not installed immediately, but are added at runtime or ONBUILD to shrink the image as much as possible. Notes: + # * build-base: used so we include the basic development packages (gcc) + # * linux-headers: commonly needed, and an unusual package name from Alpine. + # * lib6-compat: compatibility libraries for glibc + # * git: to ease up clones of repos + BUILD_PACKAGES="\ + build-base \ + linux-headers \ + libc6-compat \ + git \ + " + +# Copy in the entrypoint script -- this installs prerequisites on container start. +COPY entrypoint.sh /entrypoint.sh + +RUN set -ex ;\ + # find MAJOR and MINOR python versions based on $PYTHON_VERSION + export PYTHON_MAJOR_VERSION=$(echo "${PYTHON_VERSION}" | rev | cut -d"." -f3- | rev) ;\ + export PYTHON_MINOR_VERSION=$(echo "${PYTHON_VERSION}" | rev | cut -d"." -f2- | rev) ;\ + # replacing default repositories with edge ones + echo "http://dl-cdn.alpinelinux.org/alpine/v$ALPINE_VERSION/community" >> /etc/apk/repositories ;\ + echo "http://dl-cdn.alpinelinux.org/alpine/v$ALPINE_VERSION/main" >> /etc/apk/repositories ;\ + # Add the packages, with a CDN-breakage fallback if needed + apk add --no-cache $PACKAGES || \ + (sed -i -e 's/dl-cdn/dl-4/g' /etc/apk/repositories && apk add --no-cache $PACKAGES) ;\ + # Add packages just for the python build process with a CDN-breakage fallback if needed + apk add --no-cache --virtual .build-deps $PYTHON_BUILD_PACKAGES || \ + (sed -i -e 's/dl-cdn/dl-4/g' /etc/apk/repositories && apk add --no-cache --virtual .build-deps $PYTHON_BUILD_PACKAGES) ;\ + # turn back the clock -- so hacky! + echo "http://dl-cdn.alpinelinux.org/alpine/v$ALPINE_VERSION/main/" > /etc/apk/repositories ;\ + # echo "@community http://dl-cdn.alpinelinux.org/alpine/v$ALPINE_VERSION/community" >> /etc/apk/repositories ;\ + # echo "@testing http://dl-cdn.alpinelinux.org/alpine/v$ALPINE_VERSION/testing" >> /etc/apk/repositories ;\ + # echo "@edge-main http://dl-cdn.alpinelinux.org/alpine/edge/main" >> /etc/apk/repositories ;\ + # use pyenv to download and compile specific python version + git clone --depth 1 https://github.com/pyenv/pyenv /usr/local/lib/pyenv ;\ + # install + GNU_ARCH="$(dpkg-architecture --query DEB_BUILD_GNU_TYPE)" ;\ + PYENV_ROOT=/usr/local/lib/pyenv CONFIGURE_OPTS="--build=$GNU_ARCH --enable-loadable-sqlite-extensions --enable-shared --with-system-expat --with-system-ffi --without-ensurepip --with-shared" /usr/local/lib/pyenv/bin/pyenv install $PYTHON_VERSION ;\ + # keep the needed .so files + # ignore libpython - that one comes from the pyenv instalation + find /usr/local -type f -executable -not \( -name '*tkinter*' \) -exec scanelf --needed --nobanner --format '%n#p' '{}' ';' \ + | tr ',' '\n' \ + | sort -u \ + | awk 'system("[ -e /usr/local/lib/" $1 " ]") == 0 { next } { print "so:" $1 }' \ + | grep -ve 'libpython' \ + | xargs -rt apk add --no-cache --virtual .python-rundeps ;\ + # for debug + # | xargs -n1 echo ;\ + # delete everything from pyenv except the installed version + # this throws an error but we ignore it + find /usr/local/lib/pyenv/ -mindepth 1 -name versions -prune -o -exec rm -rf {} \; || true ;\ + # delete files to to reduce container size + # tips taken from main python docker repo + find /usr/local/lib/pyenv/versions/$PYTHON_VERSION/ -depth \( -name '*.pyo' -o -name '*.pyc' -o -name 'test' -o -name 'tests' \) -exec rm -rf '{}' + ;\ + # symlink the binaries + ln -s /usr/local/lib/pyenv/versions/$PYTHON_VERSION/bin/* $PYTHON_PATH ;\ + ln -s /usr/local/lib/pyenv/versions/$PYTHON_VERSION/include/python$PYTHON_MINOR_VERSION /usr/include/ ;\ + # remove build dependencies and any leftover apk cache + apk del --no-cache --purge .build-deps ;\ + rm -rf /var/cache/apk/* ;\ + # set proper permission to run entrypoint script + chmod a+x /entrypoint.sh + +# This script installs APK and Pip prerequisites on container start, or ONBUILD. Notes: +# * Reads the -a flags and /apk-requirements.txt for install requests +# * Reads the -b flags and /build-requirements.txt for build packages -- removed when build is complete +# * Reads the -p flags and /requirements.txt for Pip packages +# * Reads the -r flag to specify a different file path for /requirements.txt +ENTRYPOINT ["/usr/bin/dumb-init", "bash", "/entrypoint.sh"] diff --git a/3.6-slim/entrypoint.sh b/3.6-slim/entrypoint.sh new file mode 100755 index 0000000..50c2ab1 --- /dev/null +++ b/3.6-slim/entrypoint.sh @@ -0,0 +1,153 @@ +#!/usr/bin/dumb-init /bin/bash +set -e + +APK_REQUIREMENTS=() +BUILD_REQUIREMENTS=() +PIP_REQUIREMENTS=() +APKFILE='/apk-requirements.txt' +BUILDFILE='/build-requirements.txt' +REQFILE='/requirements.txt' +VERBOSITY=1 + +TMP_REQFILE='/tmp/requirements.txt' + +function usage () { + cat <<"EOF" +Usage: $0 [-a -b -p -A -B -P -r -q -x] [--] + -a : APK requirement. Can be specified multiple times. + -b : APK build requirement. These will be removed at the end to save space. + -p : Pip requirement. Can be specified multiple times. + + -A : apk-requirements.txt file location, default: /apk-requirements.txt + -B : build-requirements.txt file location, default: /build-requirements.txt + -P : requirements.txt file location, default: /requirements.txt + -r : same as above, just to match Pip's -r flag. + + -q : quiet, doesn't print anything at all. + -x : Bash debug mode. Extremely verbose! + + -- : Separator for flags and your command + + Whatever you provide after your arguments is run at the end. +EOF + exit 1 +} + +function vlog () { + if [ $VERBOSITY -gt 0 ]; then + echo $1 + fi +} + +# Get and process arguments +while getopts ":a:b:p:A:B:P:r:qx" opt; do + case $opt in + a) APK_REQUIREMENTS+=("$OPTARG") ;; + b) BUILD_REQUIREMENTS+=("$OPTARG") ;; + p) PIP_REQUIREMENTS+=("$OPTARG") ;; + A) APKFILE="$OPTARG" ;; + B) BUILDFILE="$OPTARG" ;; + P) REQFILE="$OPTARG" ;; + r) REQFILE="$OPTARG" ;; + q) VERBOSITY=0 ;; + x) set -x ;; + \?) + echo "Invalid option: -$OPTARG" >&2 + usage + ;; + :) + echo "Option -$OPTARG requires an argument." >&2 + usage + ;; + esac +done + +# Bad arguments +if [ $? -ne 0 ]; +then + usage +fi + +# Strip out all the arguments that have been processed +shift $((OPTIND-1)) + +# If there's a double dash at the end, get that off +[[ $1 = "--" ]] && shift + +# Make some common flags objects +PIP_FLAGS='' +if [ $VERBOSITY -eq 0 ]; then + PIP_FLAGS="$PIP_FLAGS -q" +fi + +APK_FLAGS='--no-cache --no-progress' +if [ $VERBOSITY -eq 0 ]; then + APK_FLAGS="$APK_FLAGS -q" +fi + +# Don't do anything if we've already done this. +if [[ ! -f /requirements.installed ]]; then + vlog "First run, checking for any requirements..." + + # Install any APK requirements + if [[ -f "$APKFILE" ]]; then + vlog "APK requirements file detected!" + APK_REQUIREMENTS+=($( cat "$APKFILE" )) + fi + + if [[ -f "$BUILDFILE" ]]; then + vlog "Build requirements file detected!" + BUILD_REQUIREMENTS+=($( cat "$BUILDFILE" )) + fi + + # Unfortunately the Alpine repositories are in a slightly inconsistent state for now-- python2 only exists in 'edge', not main. + # if [[ "$PYTHON_VERSION" == '2' ]]; then BUILD_PACKAGES="$(echo $BUILD_PACKAGES | sed -e 's/python2/python/g')"; fi \ + vlog "Installing all APK requirements..." + apk add $APK_FLAGS $BUILD_PACKAGES "${APK_REQUIREMENTS[@]}" "${BUILD_REQUIREMENTS[@]}" + + # Install any Pip requirements + if [[ -f "$REQFILE" && "$(cat $REQFILE | wc -l)" -gt 0 ]]; then + # Do this check a little early-- since we merge cli in with file, + # we'd get a false positive for logging otherwise. + vlog "Pip requirements file detected!" + fi + + # If we use CLI parameters, we'll have to reassign this. + TARGET_REQFILE="$REQFILE" + if [[ ${#PIP_REQUIREMENTS[@]} -gt 0 ]]; then + # Put all Pip requirements into the same file. + printf "%s\n" "${PIP_REQUIREMENTS[@]}" >> "$TMP_REQFILE" + + if [[ -f "$REQFILE" && "$(cat $REQFILE | wc -l)" -gt 0 ]]; then + cat "$REQFILE" >> "$TMP_REQFILE" + fi + + TARGET_REQFILE="$TMP_REQFILE" + fi + + if [[ -f $TARGET_REQFILE && "$(cat $TARGET_REQFILE | wc -l)" -gt 0 ]]; then + vlog "Upgrading Pip..." + pip install $PIP_FLAGS --upgrade pip + vlog "Installing all Pip requirements..." + pip install $PIP_FLAGS -r "$TARGET_REQFILE" + fi + + # Remove packages that were only required for build. + apk del $APK_FLAGS $BUILD_PACKAGES "${BUILD_REQUIREMENTS[@]}" + + vlog "Installing APK requirements..." + apk add $APK_FLAGS "${APK_REQUIREMENTS[@]}" + + touch /requirements.installed +else + vlog "/requirements.installed file exists-- skipping requirements installs." +fi + + +if [[ ! -z "$@" ]]; then + # If the user has given us a command, run it. + $@ +else + # Otherwise, default to running 'python'. + python +fi diff --git a/3.6/Dockerfile b/3.6/Dockerfile new file mode 100644 index 0000000..ecfbfd8 --- /dev/null +++ b/3.6/Dockerfile @@ -0,0 +1,110 @@ +FROM alpine:3.11 + +# VERSIONS +ENV ALPINE_VERSION=3.11 \ + PYTHON_VERSION=3.6.9 + +# PATHS +ENV PYTHON_PATH=/usr/local/bin/ \ + PATH="/usr/local/lib/python$PYTHON_VERSION/bin/:/usr/local/lib/pyenv/versions/$PYTHON_VERSION/bin:${PATH}" \ + # These are always installed. + # * dumb-init: a proper init system for containers, to reap zombie children + # * musl: standard C library + # * lib6-compat: compatibility libraries for glibc + # * linux-headers: commonly needed, and an unusual package name from Alpine. + # * build-base: used so we include the basic development packages (gcc) + # * bash: so we can access /bin/bash + # * git: to ease up clones of repos + # * ca-certificates: for SSL verification during Pip and easy_install + PACKAGES="\ + dumb-init \ + musl \ + libc6-compat \ + linux-headers \ + build-base \ + bash \ + git \ + ca-certificates \ + libressl-dev \ + tzdata \ + " \ + # PACKAGES needed to built python + PYTHON_BUILD_PACKAGES="\ + bzip2-dev \ + coreutils \ + dpkg-dev dpkg \ + expat-dev \ + findutils \ + gcc \ + gdbm-dev \ + libc-dev \ + libffi-dev \ + libnsl-dev \ + libtirpc-dev \ + linux-headers \ + make \ + ncurses-dev \ + libressl-dev \ + pax-utils \ + readline-dev \ + sqlite-dev \ + tcl-dev \ + tk \ + tk-dev \ + util-linux-dev \ + xz-dev \ + zlib-dev \ + git \ + " + +RUN set -ex ;\ + # find MAJOR and MINOR python versions based on $PYTHON_VERSION + export PYTHON_MAJOR_VERSION=$(echo "${PYTHON_VERSION}" | rev | cut -d"." -f3- | rev) ;\ + export PYTHON_MINOR_VERSION=$(echo "${PYTHON_VERSION}" | rev | cut -d"." -f2- | rev) ;\ + # replacing default repositories with edge ones + echo "http://dl-cdn.alpinelinux.org/alpine/v$ALPINE_VERSION/community" >> /etc/apk/repositories ;\ + echo "http://dl-cdn.alpinelinux.org/alpine/v$ALPINE_VERSION/main" >> /etc/apk/repositories ;\ + # Add the packages, with a CDN-breakage fallback if needed + apk add --no-cache $PACKAGES || \ + (sed -i -e 's/dl-cdn/dl-4/g' /etc/apk/repositories && apk add --no-cache $PACKAGES) ;\ + # Add packages just for the python build process with a CDN-breakage fallback if needed + apk add --no-cache --virtual .build-deps $PYTHON_BUILD_PACKAGES || \ + (sed -i -e 's/dl-cdn/dl-4/g' /etc/apk/repositories && apk add --no-cache --virtual .build-deps $PYTHON_BUILD_PACKAGES) ;\ + # turn back the clock -- so hacky! + echo "http://dl-cdn.alpinelinux.org/alpine/v$ALPINE_VERSION/main/" > /etc/apk/repositories ;\ + # echo "@community http://dl-cdn.alpinelinux.org/alpine/v$ALPINE_VERSION/community" >> /etc/apk/repositories ;\ + # echo "@testing http://dl-cdn.alpinelinux.org/alpine/v$ALPINE_VERSION/testing" >> /etc/apk/repositories ;\ + # echo "@edge-main http://dl-cdn.alpinelinux.org/alpine/edge/main" >> /etc/apk/repositories ;\ + # use pyenv to download and compile specific python version + git clone --depth 1 https://github.com/pyenv/pyenv /usr/local/lib/pyenv ;\ + # install + GNU_ARCH="$(dpkg-architecture --query DEB_BUILD_GNU_TYPE)" ;\ + PYENV_ROOT=/usr/local/lib/pyenv CONFIGURE_OPTS="--build=$GNU_ARCH --enable-loadable-sqlite-extensions --enable-shared --with-system-expat --with-system-ffi --without-ensurepip --with-shared" /usr/local/lib/pyenv/bin/pyenv install $PYTHON_VERSION ;\ + # keep the needed .so files + # ignore libpython - that one comes from the pyenv instalation + find /usr/local -type f -executable -not \( -name '*tkinter*' \) -exec scanelf --needed --nobanner --format '%n#p' '{}' ';' \ + | tr ',' '\n' \ + | sort -u \ + | awk 'system("[ -e /usr/local/lib/" $1 " ]") == 0 { next } { print "so:" $1 }' \ + | grep -ve 'libpython' \ + | xargs -rt apk add --no-cache --virtual .python-rundeps ;\ + # for debug + # | xargs -n1 echo ;\ + # delete everything from pyenv except the installed version + # this throws an error but we ignore it + find /usr/local/lib/pyenv/ -mindepth 1 -name versions -prune -o -exec rm -rf {} \; || true ;\ + # delete files to to reduce container size + # tips taken from main python docker repo + find /usr/local/lib/pyenv/versions/$PYTHON_VERSION/ -depth \( -name '*.pyo' -o -name '*.pyc' -o -name 'test' -o -name 'tests' \) -exec rm -rf '{}' + ;\ + # symlink the binaries + ln -s /usr/local/lib/pyenv/versions/$PYTHON_VERSION/bin/* $PYTHON_PATH ;\ + ln -s /usr/local/lib/pyenv/versions/$PYTHON_VERSION/include/python$PYTHON_MINOR_VERSION /usr/include/ ;\ + # set timezone info + ln -fs /usr/share/zoneinfo/Etc/UTC /etc/localtime ;\ + # remove build dependencies and any leftover apk cache + apk del --no-cache --purge .build-deps ;\ + rm -rf /var/cache/apk/* + +# since we will be "always" mounting the volume, we can set this up +ENTRYPOINT ["/usr/bin/dumb-init"] +CMD ["python"] diff --git a/3.7-onbuild/Dockerfile b/3.7-onbuild/Dockerfile new file mode 100644 index 0000000..1303fd1 --- /dev/null +++ b/3.7-onbuild/Dockerfile @@ -0,0 +1,124 @@ +FROM alpine:3.11 + +# VERSIONS +ENV ALPINE_VERSION=3.11 \ + PYTHON_VERSION=3.7.3 + +# PATHS +ENV PYTHON_PATH=/usr/local/bin/ \ + PATH="/usr/local/lib/python$PYTHON_VERSION/bin/:/usr/local/lib/pyenv/versions/$PYTHON_VERSION/bin:${PATH}" \ + # These are always installed. + # * dumb-init: a proper init system for containers, to reap zombie children + # * musl: standard C library + # * lib6-compat: compatibility libraries for glibc + # * linux-headers: commonly needed, and an unusual package name from Alpine. + # * build-base: used so we include the basic development packages (gcc) + # * bash: so we can access /bin/bash + # * git: to ease up clones of repos + # * ca-certificates: for SSL verification during Pip and easy_install + PACKAGES="\ + dumb-init \ + musl \ + libc6-compat \ + linux-headers \ + build-base \ + bash \ + git \ + ca-certificates \ + libressl-dev \ + tzdata \ + " \ + # PACKAGES needed to built python + PYTHON_BUILD_PACKAGES="\ + bzip2-dev \ + coreutils \ + dpkg-dev dpkg \ + expat-dev \ + findutils \ + gcc \ + gdbm-dev \ + libc-dev \ + libffi-dev \ + libnsl-dev \ + libtirpc-dev \ + linux-headers \ + make \ + ncurses-dev \ + libressl-dev \ + pax-utils \ + readline-dev \ + sqlite-dev \ + tcl-dev \ + tk \ + tk-dev \ + util-linux-dev \ + xz-dev \ + zlib-dev \ + git \ + " + +# Copy in the entrypoint script -- this installs prerequisites on container start. +COPY entrypoint.sh /entrypoint.sh + +RUN set -ex ;\ + # find MAJOR and MINOR python versions based on $PYTHON_VERSION + export PYTHON_MAJOR_VERSION=$(echo "${PYTHON_VERSION}" | rev | cut -d"." -f3- | rev) ;\ + export PYTHON_MINOR_VERSION=$(echo "${PYTHON_VERSION}" | rev | cut -d"." -f2- | rev) ;\ + # replacing default repositories with edge ones + echo "http://dl-cdn.alpinelinux.org/alpine/v$ALPINE_VERSION/community" >> /etc/apk/repositories ;\ + echo "http://dl-cdn.alpinelinux.org/alpine/v$ALPINE_VERSION/main" >> /etc/apk/repositories ;\ + # Add the packages, with a CDN-breakage fallback if needed + apk add --no-cache $PACKAGES || \ + (sed -i -e 's/dl-cdn/dl-4/g' /etc/apk/repositories && apk add --no-cache $PACKAGES) ;\ + # Add packages just for the python build process with a CDN-breakage fallback if needed + apk add --no-cache --virtual .build-deps $PYTHON_BUILD_PACKAGES || \ + (sed -i -e 's/dl-cdn/dl-4/g' /etc/apk/repositories && apk add --no-cache --virtual .build-deps $PYTHON_BUILD_PACKAGES) ;\ + # turn back the clock -- so hacky! + echo "http://dl-cdn.alpinelinux.org/alpine/v$ALPINE_VERSION/main/" > /etc/apk/repositories ;\ + # echo "@community http://dl-cdn.alpinelinux.org/alpine/v$ALPINE_VERSION/community" >> /etc/apk/repositories ;\ + # echo "@testing http://dl-cdn.alpinelinux.org/alpine/v$ALPINE_VERSION/testing" >> /etc/apk/repositories ;\ + # echo "@edge-main http://dl-cdn.alpinelinux.org/alpine/edge/main" >> /etc/apk/repositories ;\ + # use pyenv to download and compile specific python version + git clone --depth 1 https://github.com/pyenv/pyenv /usr/local/lib/pyenv ;\ + # install + GNU_ARCH="$(dpkg-architecture --query DEB_BUILD_GNU_TYPE)" ;\ + PYENV_ROOT=/usr/local/lib/pyenv CONFIGURE_OPTS="--build=$GNU_ARCH --enable-loadable-sqlite-extensions --enable-shared --with-system-expat --with-system-ffi --without-ensurepip --with-shared" /usr/local/lib/pyenv/bin/pyenv install $PYTHON_VERSION ;\ + # keep the needed .so files + # ignore libpython - that one comes from the pyenv instalation + find /usr/local -type f -executable -not \( -name '*tkinter*' \) -exec scanelf --needed --nobanner --format '%n#p' '{}' ';' \ + | tr ',' '\n' \ + | sort -u \ + | awk 'system("[ -e /usr/local/lib/" $1 " ]") == 0 { next } { print "so:" $1 }' \ + | grep -ve 'libpython' \ + | xargs -rt apk add --no-cache --virtual .python-rundeps ;\ + # for debug + # | xargs -n1 echo ;\ + # delete everything from pyenv except the installed version + # this throws an error but we ignore it + find /usr/local/lib/pyenv/ -mindepth 1 -name versions -prune -o -exec rm -rf {} \; || true ;\ + # delete files to to reduce container size + # tips taken from main python docker repo + find /usr/local/lib/pyenv/versions/$PYTHON_VERSION/ -depth \( -name '*.pyo' -o -name '*.pyc' -o -name 'test' -o -name 'tests' \) -exec rm -rf '{}' + ;\ + # symlink the binaries + ln -s /usr/local/lib/pyenv/versions/$PYTHON_VERSION/bin/* $PYTHON_PATH ;\ + ln -s /usr/local/lib/pyenv/versions/$PYTHON_VERSION/include/python$PYTHON_MINOR_VERSION /usr/include/ ;\ + # set timezone info + ln -fs /usr/share/zoneinfo/Etc/UTC /etc/localtime ;\ + # remove build dependencies and any leftover apk cache + apk del --no-cache --purge .build-deps ;\ + rm -rf /var/cache/apk/* ;\ + # set proper permission to run entrypoint script + chmod a+x /entrypoint.sh + +# install requirements +# this way when you build you won't need to install again +# and since COPY is cached we don't need to wait +ONBUILD COPY requirements.txt /tmp/requirements.txt + +# Run the dependencies installer and then allow it to be run again if needed. +ONBUILD RUN /entrypoint.sh -r /tmp/requirements.txt +ONBUILD RUN rm -f /requirements.installed + +# since we will be "always" mounting the volume, we can set this up +ENTRYPOINT ["/usr/bin/dumb-init"] +CMD ["python"] diff --git a/3.7-onbuild/entrypoint.sh b/3.7-onbuild/entrypoint.sh new file mode 100755 index 0000000..50c2ab1 --- /dev/null +++ b/3.7-onbuild/entrypoint.sh @@ -0,0 +1,153 @@ +#!/usr/bin/dumb-init /bin/bash +set -e + +APK_REQUIREMENTS=() +BUILD_REQUIREMENTS=() +PIP_REQUIREMENTS=() +APKFILE='/apk-requirements.txt' +BUILDFILE='/build-requirements.txt' +REQFILE='/requirements.txt' +VERBOSITY=1 + +TMP_REQFILE='/tmp/requirements.txt' + +function usage () { + cat <<"EOF" +Usage: $0 [-a -b -p -A -B -P -r -q -x] [--] + -a : APK requirement. Can be specified multiple times. + -b : APK build requirement. These will be removed at the end to save space. + -p : Pip requirement. Can be specified multiple times. + + -A : apk-requirements.txt file location, default: /apk-requirements.txt + -B : build-requirements.txt file location, default: /build-requirements.txt + -P : requirements.txt file location, default: /requirements.txt + -r : same as above, just to match Pip's -r flag. + + -q : quiet, doesn't print anything at all. + -x : Bash debug mode. Extremely verbose! + + -- : Separator for flags and your command + + Whatever you provide after your arguments is run at the end. +EOF + exit 1 +} + +function vlog () { + if [ $VERBOSITY -gt 0 ]; then + echo $1 + fi +} + +# Get and process arguments +while getopts ":a:b:p:A:B:P:r:qx" opt; do + case $opt in + a) APK_REQUIREMENTS+=("$OPTARG") ;; + b) BUILD_REQUIREMENTS+=("$OPTARG") ;; + p) PIP_REQUIREMENTS+=("$OPTARG") ;; + A) APKFILE="$OPTARG" ;; + B) BUILDFILE="$OPTARG" ;; + P) REQFILE="$OPTARG" ;; + r) REQFILE="$OPTARG" ;; + q) VERBOSITY=0 ;; + x) set -x ;; + \?) + echo "Invalid option: -$OPTARG" >&2 + usage + ;; + :) + echo "Option -$OPTARG requires an argument." >&2 + usage + ;; + esac +done + +# Bad arguments +if [ $? -ne 0 ]; +then + usage +fi + +# Strip out all the arguments that have been processed +shift $((OPTIND-1)) + +# If there's a double dash at the end, get that off +[[ $1 = "--" ]] && shift + +# Make some common flags objects +PIP_FLAGS='' +if [ $VERBOSITY -eq 0 ]; then + PIP_FLAGS="$PIP_FLAGS -q" +fi + +APK_FLAGS='--no-cache --no-progress' +if [ $VERBOSITY -eq 0 ]; then + APK_FLAGS="$APK_FLAGS -q" +fi + +# Don't do anything if we've already done this. +if [[ ! -f /requirements.installed ]]; then + vlog "First run, checking for any requirements..." + + # Install any APK requirements + if [[ -f "$APKFILE" ]]; then + vlog "APK requirements file detected!" + APK_REQUIREMENTS+=($( cat "$APKFILE" )) + fi + + if [[ -f "$BUILDFILE" ]]; then + vlog "Build requirements file detected!" + BUILD_REQUIREMENTS+=($( cat "$BUILDFILE" )) + fi + + # Unfortunately the Alpine repositories are in a slightly inconsistent state for now-- python2 only exists in 'edge', not main. + # if [[ "$PYTHON_VERSION" == '2' ]]; then BUILD_PACKAGES="$(echo $BUILD_PACKAGES | sed -e 's/python2/python/g')"; fi \ + vlog "Installing all APK requirements..." + apk add $APK_FLAGS $BUILD_PACKAGES "${APK_REQUIREMENTS[@]}" "${BUILD_REQUIREMENTS[@]}" + + # Install any Pip requirements + if [[ -f "$REQFILE" && "$(cat $REQFILE | wc -l)" -gt 0 ]]; then + # Do this check a little early-- since we merge cli in with file, + # we'd get a false positive for logging otherwise. + vlog "Pip requirements file detected!" + fi + + # If we use CLI parameters, we'll have to reassign this. + TARGET_REQFILE="$REQFILE" + if [[ ${#PIP_REQUIREMENTS[@]} -gt 0 ]]; then + # Put all Pip requirements into the same file. + printf "%s\n" "${PIP_REQUIREMENTS[@]}" >> "$TMP_REQFILE" + + if [[ -f "$REQFILE" && "$(cat $REQFILE | wc -l)" -gt 0 ]]; then + cat "$REQFILE" >> "$TMP_REQFILE" + fi + + TARGET_REQFILE="$TMP_REQFILE" + fi + + if [[ -f $TARGET_REQFILE && "$(cat $TARGET_REQFILE | wc -l)" -gt 0 ]]; then + vlog "Upgrading Pip..." + pip install $PIP_FLAGS --upgrade pip + vlog "Installing all Pip requirements..." + pip install $PIP_FLAGS -r "$TARGET_REQFILE" + fi + + # Remove packages that were only required for build. + apk del $APK_FLAGS $BUILD_PACKAGES "${BUILD_REQUIREMENTS[@]}" + + vlog "Installing APK requirements..." + apk add $APK_FLAGS "${APK_REQUIREMENTS[@]}" + + touch /requirements.installed +else + vlog "/requirements.installed file exists-- skipping requirements installs." +fi + + +if [[ ! -z "$@" ]]; then + # If the user has given us a command, run it. + $@ +else + # Otherwise, default to running 'python'. + python +fi diff --git a/3.7-slim/Dockerfile b/3.7-slim/Dockerfile new file mode 100644 index 0000000..d6b7888 --- /dev/null +++ b/3.7-slim/Dockerfile @@ -0,0 +1,115 @@ +FROM alpine:3.11 + +# VERSIONS +ENV ALPINE_VERSION=3.11 \ + PYTHON_VERSION=3.7.3 + +# PATHS +ENV PYTHON_PATH=/usr/local/bin/ \ + PATH="/usr/local/lib/python$PYTHON_VERSION/bin/:/usr/local/lib/pyenv/versions/$PYTHON_VERSION/bin:${PATH}" \ + # These are always installed. Notes: + # * dumb-init: a proper init system for containers, to reap zombie children + # * bash: For entrypoint, and debugging + # * ca-certificates: for SSL verification during Pip and easy_install + PACKAGES="\ + dumb-init \ + bash \ + ca-certificates \ + " \ + # PACKAGES needed to built python + PYTHON_BUILD_PACKAGES="\ + bzip2-dev \ + coreutils \ + dpkg-dev dpkg \ + expat-dev \ + findutils \ + gcc \ + gdbm-dev \ + libc-dev \ + libffi-dev \ + libnsl-dev \ + libtirpc-dev \ + linux-headers \ + make \ + ncurses-dev \ + libressl-dev \ + pax-utils \ + readline-dev \ + sqlite-dev \ + tcl-dev \ + tk \ + tk-dev \ + util-linux-dev \ + xz-dev \ + zlib-dev \ + git \ + " \ + # These packages are not installed immediately, but are added at runtime or ONBUILD to shrink the image as much as possible. Notes: + # * build-base: used so we include the basic development packages (gcc) + # * linux-headers: commonly needed, and an unusual package name from Alpine. + # * lib6-compat: compatibility libraries for glibc + # * git: to ease up clones of repos + BUILD_PACKAGES="\ + build-base \ + linux-headers \ + libc6-compat \ + git \ + " + +# Copy in the entrypoint script -- this installs prerequisites on container start. +COPY entrypoint.sh /entrypoint.sh + +RUN set -ex ;\ + # find MAJOR and MINOR python versions based on $PYTHON_VERSION + export PYTHON_MAJOR_VERSION=$(echo "${PYTHON_VERSION}" | rev | cut -d"." -f3- | rev) ;\ + export PYTHON_MINOR_VERSION=$(echo "${PYTHON_VERSION}" | rev | cut -d"." -f2- | rev) ;\ + # replacing default repositories with edge ones + echo "http://dl-cdn.alpinelinux.org/alpine/v$ALPINE_VERSION/community" >> /etc/apk/repositories ;\ + echo "http://dl-cdn.alpinelinux.org/alpine/v$ALPINE_VERSION/main" >> /etc/apk/repositories ;\ + # Add the packages, with a CDN-breakage fallback if needed + apk add --no-cache $PACKAGES || \ + (sed -i -e 's/dl-cdn/dl-4/g' /etc/apk/repositories && apk add --no-cache $PACKAGES) ;\ + # Add packages just for the python build process with a CDN-breakage fallback if needed + apk add --no-cache --virtual .build-deps $PYTHON_BUILD_PACKAGES || \ + (sed -i -e 's/dl-cdn/dl-4/g' /etc/apk/repositories && apk add --no-cache --virtual .build-deps $PYTHON_BUILD_PACKAGES) ;\ + # turn back the clock -- so hacky! + echo "http://dl-cdn.alpinelinux.org/alpine/v$ALPINE_VERSION/main/" > /etc/apk/repositories ;\ + # echo "@community http://dl-cdn.alpinelinux.org/alpine/v$ALPINE_VERSION/community" >> /etc/apk/repositories ;\ + # echo "@testing http://dl-cdn.alpinelinux.org/alpine/v$ALPINE_VERSION/testing" >> /etc/apk/repositories ;\ + # echo "@edge-main http://dl-cdn.alpinelinux.org/alpine/edge/main" >> /etc/apk/repositories ;\ + # use pyenv to download and compile specific python version + git clone --depth 1 https://github.com/pyenv/pyenv /usr/local/lib/pyenv ;\ + # install + GNU_ARCH="$(dpkg-architecture --query DEB_BUILD_GNU_TYPE)" ;\ + PYENV_ROOT=/usr/local/lib/pyenv CONFIGURE_OPTS="--build=$GNU_ARCH --enable-loadable-sqlite-extensions --enable-shared --with-system-expat --with-system-ffi --without-ensurepip --with-shared" /usr/local/lib/pyenv/bin/pyenv install $PYTHON_VERSION ;\ + # keep the needed .so files + # ignore libpython - that one comes from the pyenv instalation + find /usr/local -type f -executable -not \( -name '*tkinter*' \) -exec scanelf --needed --nobanner --format '%n#p' '{}' ';' \ + | tr ',' '\n' \ + | sort -u \ + | awk 'system("[ -e /usr/local/lib/" $1 " ]") == 0 { next } { print "so:" $1 }' \ + | grep -ve 'libpython' \ + | xargs -rt apk add --no-cache --virtual .python-rundeps ;\ + # for debug + # | xargs -n1 echo ;\ + # delete everything from pyenv except the installed version + # this throws an error but we ignore it + find /usr/local/lib/pyenv/ -mindepth 1 -name versions -prune -o -exec rm -rf {} \; || true ;\ + # delete files to to reduce container size + # tips taken from main python docker repo + find /usr/local/lib/pyenv/versions/$PYTHON_VERSION/ -depth \( -name '*.pyo' -o -name '*.pyc' -o -name 'test' -o -name 'tests' \) -exec rm -rf '{}' + ;\ + # symlink the binaries + ln -s /usr/local/lib/pyenv/versions/$PYTHON_VERSION/bin/* $PYTHON_PATH ;\ + ln -s /usr/local/lib/pyenv/versions/$PYTHON_VERSION/include/python$PYTHON_MINOR_VERSION /usr/include/ ;\ + # remove build dependencies and any leftover apk cache + apk del --no-cache --purge .build-deps ;\ + rm -rf /var/cache/apk/* ;\ + # set proper permission to run entrypoint script + chmod a+x /entrypoint.sh + +# This script installs APK and Pip prerequisites on container start, or ONBUILD. Notes: +# * Reads the -a flags and /apk-requirements.txt for install requests +# * Reads the -b flags and /build-requirements.txt for build packages -- removed when build is complete +# * Reads the -p flags and /requirements.txt for Pip packages +# * Reads the -r flag to specify a different file path for /requirements.txt +ENTRYPOINT ["/usr/bin/dumb-init", "bash", "/entrypoint.sh"] diff --git a/3.7-slim/entrypoint.sh b/3.7-slim/entrypoint.sh new file mode 100644 index 0000000..50c2ab1 --- /dev/null +++ b/3.7-slim/entrypoint.sh @@ -0,0 +1,153 @@ +#!/usr/bin/dumb-init /bin/bash +set -e + +APK_REQUIREMENTS=() +BUILD_REQUIREMENTS=() +PIP_REQUIREMENTS=() +APKFILE='/apk-requirements.txt' +BUILDFILE='/build-requirements.txt' +REQFILE='/requirements.txt' +VERBOSITY=1 + +TMP_REQFILE='/tmp/requirements.txt' + +function usage () { + cat <<"EOF" +Usage: $0 [-a -b -p -A -B -P -r -q -x] [--] + -a : APK requirement. Can be specified multiple times. + -b : APK build requirement. These will be removed at the end to save space. + -p : Pip requirement. Can be specified multiple times. + + -A : apk-requirements.txt file location, default: /apk-requirements.txt + -B : build-requirements.txt file location, default: /build-requirements.txt + -P : requirements.txt file location, default: /requirements.txt + -r : same as above, just to match Pip's -r flag. + + -q : quiet, doesn't print anything at all. + -x : Bash debug mode. Extremely verbose! + + -- : Separator for flags and your command + + Whatever you provide after your arguments is run at the end. +EOF + exit 1 +} + +function vlog () { + if [ $VERBOSITY -gt 0 ]; then + echo $1 + fi +} + +# Get and process arguments +while getopts ":a:b:p:A:B:P:r:qx" opt; do + case $opt in + a) APK_REQUIREMENTS+=("$OPTARG") ;; + b) BUILD_REQUIREMENTS+=("$OPTARG") ;; + p) PIP_REQUIREMENTS+=("$OPTARG") ;; + A) APKFILE="$OPTARG" ;; + B) BUILDFILE="$OPTARG" ;; + P) REQFILE="$OPTARG" ;; + r) REQFILE="$OPTARG" ;; + q) VERBOSITY=0 ;; + x) set -x ;; + \?) + echo "Invalid option: -$OPTARG" >&2 + usage + ;; + :) + echo "Option -$OPTARG requires an argument." >&2 + usage + ;; + esac +done + +# Bad arguments +if [ $? -ne 0 ]; +then + usage +fi + +# Strip out all the arguments that have been processed +shift $((OPTIND-1)) + +# If there's a double dash at the end, get that off +[[ $1 = "--" ]] && shift + +# Make some common flags objects +PIP_FLAGS='' +if [ $VERBOSITY -eq 0 ]; then + PIP_FLAGS="$PIP_FLAGS -q" +fi + +APK_FLAGS='--no-cache --no-progress' +if [ $VERBOSITY -eq 0 ]; then + APK_FLAGS="$APK_FLAGS -q" +fi + +# Don't do anything if we've already done this. +if [[ ! -f /requirements.installed ]]; then + vlog "First run, checking for any requirements..." + + # Install any APK requirements + if [[ -f "$APKFILE" ]]; then + vlog "APK requirements file detected!" + APK_REQUIREMENTS+=($( cat "$APKFILE" )) + fi + + if [[ -f "$BUILDFILE" ]]; then + vlog "Build requirements file detected!" + BUILD_REQUIREMENTS+=($( cat "$BUILDFILE" )) + fi + + # Unfortunately the Alpine repositories are in a slightly inconsistent state for now-- python2 only exists in 'edge', not main. + # if [[ "$PYTHON_VERSION" == '2' ]]; then BUILD_PACKAGES="$(echo $BUILD_PACKAGES | sed -e 's/python2/python/g')"; fi \ + vlog "Installing all APK requirements..." + apk add $APK_FLAGS $BUILD_PACKAGES "${APK_REQUIREMENTS[@]}" "${BUILD_REQUIREMENTS[@]}" + + # Install any Pip requirements + if [[ -f "$REQFILE" && "$(cat $REQFILE | wc -l)" -gt 0 ]]; then + # Do this check a little early-- since we merge cli in with file, + # we'd get a false positive for logging otherwise. + vlog "Pip requirements file detected!" + fi + + # If we use CLI parameters, we'll have to reassign this. + TARGET_REQFILE="$REQFILE" + if [[ ${#PIP_REQUIREMENTS[@]} -gt 0 ]]; then + # Put all Pip requirements into the same file. + printf "%s\n" "${PIP_REQUIREMENTS[@]}" >> "$TMP_REQFILE" + + if [[ -f "$REQFILE" && "$(cat $REQFILE | wc -l)" -gt 0 ]]; then + cat "$REQFILE" >> "$TMP_REQFILE" + fi + + TARGET_REQFILE="$TMP_REQFILE" + fi + + if [[ -f $TARGET_REQFILE && "$(cat $TARGET_REQFILE | wc -l)" -gt 0 ]]; then + vlog "Upgrading Pip..." + pip install $PIP_FLAGS --upgrade pip + vlog "Installing all Pip requirements..." + pip install $PIP_FLAGS -r "$TARGET_REQFILE" + fi + + # Remove packages that were only required for build. + apk del $APK_FLAGS $BUILD_PACKAGES "${BUILD_REQUIREMENTS[@]}" + + vlog "Installing APK requirements..." + apk add $APK_FLAGS "${APK_REQUIREMENTS[@]}" + + touch /requirements.installed +else + vlog "/requirements.installed file exists-- skipping requirements installs." +fi + + +if [[ ! -z "$@" ]]; then + # If the user has given us a command, run it. + $@ +else + # Otherwise, default to running 'python'. + python +fi diff --git a/3.7/Dockerfile b/3.7/Dockerfile new file mode 100644 index 0000000..4c5c2ac --- /dev/null +++ b/3.7/Dockerfile @@ -0,0 +1,113 @@ +FROM alpine:3.11 + +# VERSIONS +ENV ALPINE_VERSION=3.11 \ + PYTHON_VERSION=3.7.3 + +# PATHS +ENV PYTHON_PATH=/usr/local/bin/ \ + PATH="/usr/local/lib/python$PYTHON_VERSION/bin/:/usr/local/lib/pyenv/versions/$PYTHON_VERSION/bin:${PATH}" \ + # These are always installed. + # * dumb-init: a proper init system for containers, to reap zombie children + # * musl: standard C library + # * lib6-compat: compatibility libraries for glibc + # * linux-headers: commonly needed, and an unusual package name from Alpine. + # * build-base: used so we include the basic development packages (gcc) + # * bash: so we can access /bin/bash + # * git: to ease up clones of repos + # * ca-certificates: for SSL verification during Pip and easy_install + PACKAGES="\ + dumb-init \ + musl \ + libc6-compat \ + linux-headers \ + build-base \ + bash \ + git \ + ca-certificates \ + libressl-dev \ + libffi-dev \ + tzdata \ + " \ + # PACKAGES needed to built python + PYTHON_BUILD_PACKAGES="\ + bzip2-dev \ + coreutils \ + dpkg-dev dpkg \ + expat-dev \ + findutils \ + gcc \ + gdbm-dev \ + libc-dev \ + libffi-dev \ + libnsl-dev \ + libtirpc-dev \ + linux-headers \ + make \ + ncurses-dev \ + libressl-dev \ + pax-utils \ + readline-dev \ + sqlite-dev \ + tcl-dev \ + tk \ + tk-dev \ + util-linux-dev \ + xz-dev \ + zlib-dev \ + git \ + " + +RUN set -ex ;\ + # find MAJOR and MINOR python versions based on $PYTHON_VERSION + export PYTHON_MAJOR_VERSION=$(echo "${PYTHON_VERSION}" | rev | cut -d"." -f3- | rev) ;\ + export PYTHON_MINOR_VERSION=$(echo "${PYTHON_VERSION}" | rev | cut -d"." -f2- | rev) ;\ + # replacing default repositories with edge ones + echo "http://dl-cdn.alpinelinux.org/alpine/v$ALPINE_VERSION/community" >> /etc/apk/repositories ;\ + echo "http://dl-cdn.alpinelinux.org/alpine/v$ALPINE_VERSION/main" >> /etc/apk/repositories ;\ + # Add the packages, with a CDN-breakage fallback if needed + apk add --no-cache $PACKAGES || \ + (sed -i -e 's/dl-cdn/dl-4/g' /etc/apk/repositories && apk add --no-cache $PACKAGES) ;\ + # Add packages just for the python build process with a CDN-breakage fallback if needed + apk add --no-cache --virtual .build-deps $PYTHON_BUILD_PACKAGES || \ + (sed -i -e 's/dl-cdn/dl-4/g' /etc/apk/repositories && apk add --no-cache --virtual .build-deps $PYTHON_BUILD_PACKAGES) ;\ + # turn back the clock -- so hacky! + echo "http://dl-cdn.alpinelinux.org/alpine/v$ALPINE_VERSION/main/" > /etc/apk/repositories ;\ + # echo "@community http://dl-cdn.alpinelinux.org/alpine/v$ALPINE_VERSION/community" >> /etc/apk/repositories ;\ + # echo "@testing http://dl-cdn.alpinelinux.org/alpine/v$ALPINE_VERSION/testing" >> /etc/apk/repositories ;\ + # echo "@edge-main http://dl-cdn.alpinelinux.org/alpine/edge/main" >> /etc/apk/repositories ;\ + # use pyenv to download and compile specific python version + git clone --depth 1 https://github.com/pyenv/pyenv /usr/local/lib/pyenv ;\ + # install + GNU_ARCH="$(dpkg-architecture --query DEB_BUILD_GNU_TYPE)" ;\ + # flag explanation: + # --with-shared : python-dev + PYENV_ROOT=/usr/local/lib/pyenv CONFIGURE_OPTS="--build=$GNU_ARCH --enable-loadable-sqlite-extensions --enable-shared --with-system-expat --with-system-ffi --without-ensurepip --with-shared" /usr/local/lib/pyenv/bin/pyenv install $PYTHON_VERSION ;\ + # keep the needed .so files + # ignore libpython - that one comes from the pyenv instalation + find /usr/local -type f -executable -not \( -name '*tkinter*' \) -exec scanelf --needed --nobanner --format '%n#p' '{}' ';' \ + | tr ',' '\n' \ + | sort -u \ + | awk 'system("[ -e /usr/local/lib/" $1 " ]") == 0 { next } { print "so:" $1 }' \ + | grep -ve 'libpython' \ + | xargs -rt apk add --no-cache --virtual .python-rundeps ;\ + # for debug + # | xargs -n1 echo ;\ + # delete everything from pyenv except the installed version + # this throws an error but we ignore it + find /usr/local/lib/pyenv/ -mindepth 1 -name versions -prune -o -exec rm -rf {} \; || true ;\ + # delete files to to reduce container size + # tips taken from main python docker repo + find /usr/local/lib/pyenv/versions/$PYTHON_VERSION/ -depth \( -name '*.pyo' -o -name '*.pyc' -o -name 'test' -o -name 'tests' \) -exec rm -rf '{}' + ;\ + # symlink the binaries + ln -s /usr/local/lib/pyenv/versions/$PYTHON_VERSION/bin/* $PYTHON_PATH ;\ + ln -s /usr/local/lib/pyenv/versions/$PYTHON_VERSION/include/python$PYTHON_MINOR_VERSION /usr/include/ ;\ + # set timezone info + ln -fs /usr/share/zoneinfo/Etc/UTC /etc/localtime ;\ + # remove build dependencies and any leftover apk cache + apk del --no-cache --purge .build-deps ;\ + rm -rf /var/cache/apk/* + +# since we will be "always" mounting the volume, we can set this up +ENTRYPOINT ["/usr/bin/dumb-init"] +CMD ["python"] diff --git a/3.8-onbuild/Dockerfile b/3.8-onbuild/Dockerfile new file mode 100644 index 0000000..dc13051 --- /dev/null +++ b/3.8-onbuild/Dockerfile @@ -0,0 +1,124 @@ +FROM alpine:3.11 + +# VERSIONS +ENV ALPINE_VERSION=3.11 \ + PYTHON_VERSION=3.8.2 + +# PATHS +ENV PYTHON_PATH=/usr/local/bin/ \ + PATH="/usr/local/lib/python$PYTHON_VERSION/bin/:/usr/local/lib/pyenv/versions/$PYTHON_VERSION/bin:${PATH}" \ + # These are always installed. + # * dumb-init: a proper init system for containers, to reap zombie children + # * musl: standard C library + # * lib6-compat: compatibility libraries for glibc + # * linux-headers: commonly needed, and an unusual package name from Alpine. + # * build-base: used so we include the basic development packages (gcc) + # * bash: so we can access /bin/bash + # * git: to ease up clones of repos + # * ca-certificates: for SSL verification during Pip and easy_install + PACKAGES="\ + dumb-init \ + musl \ + libc6-compat \ + linux-headers \ + build-base \ + bash \ + git \ + ca-certificates \ + libressl-dev \ + tzdata \ + " \ + # PACKAGES needed to built python + PYTHON_BUILD_PACKAGES="\ + bzip2-dev \ + coreutils \ + dpkg \ + dpkg-dev \ + expat-dev \ + findutils \ + gcc \ + gdbm-dev \ + libc-dev \ + libffi-dev \ + libnsl-dev \ + libtirpc-dev \ + linux-headers \ + make \ + ncurses-dev \ + libressl-dev \ + pax-utils \ + readline-dev \ + sqlite-dev \ + tcl-dev \ + tk \ + tk-dev \ + util-linux-dev \ + xz-dev \ + zlib-dev \ + " + +# Copy in the entrypoint script -- this installs prerequisites on container start. +COPY entrypoint.sh /entrypoint.sh + +RUN set -ex ;\ + # find MAJOR and MINOR python versions based on $PYTHON_VERSION + export PYTHON_MAJOR_VERSION=$(echo "${PYTHON_VERSION}" | rev | cut -d"." -f3- | rev) ;\ + export PYTHON_MINOR_VERSION=$(echo "${PYTHON_VERSION}" | rev | cut -d"." -f2- | rev) ;\ + # replacing default repositories with edge ones + echo "http://dl-cdn.alpinelinux.org/alpine/v$ALPINE_VERSION/community" >> /etc/apk/repositories ;\ + echo "http://dl-cdn.alpinelinux.org/alpine/v$ALPINE_VERSION/main" >> /etc/apk/repositories ;\ + # Add the packages, with a CDN-breakage fallback if needed + apk add --no-cache $PACKAGES || \ + (sed -i -e 's/dl-cdn/dl-4/g' /etc/apk/repositories && apk add --no-cache $PACKAGES) ;\ + # Add packages just for the python build process with a CDN-breakage fallback if needed + apk add --no-cache --virtual .build-deps $PYTHON_BUILD_PACKAGES || \ + (sed -i -e 's/dl-cdn/dl-4/g' /etc/apk/repositories && apk add --no-cache --virtual .build-deps $PYTHON_BUILD_PACKAGES) ;\ + # turn back the clock -- so hacky! + echo "http://dl-cdn.alpinelinux.org/alpine/v$ALPINE_VERSION/main/" > /etc/apk/repositories ;\ + # echo "@community http://dl-cdn.alpinelinux.org/alpine/v$ALPINE_VERSION/community" >> /etc/apk/repositories ;\ + # echo "@testing http://dl-cdn.alpinelinux.org/alpine/v$ALPINE_VERSION/testing" >> /etc/apk/repositories ;\ + # echo "@edge-main http://dl-cdn.alpinelinux.org/alpine/edge/main" >> /etc/apk/repositories ;\ + # use pyenv to download and compile specific python version + git clone --depth 1 https://github.com/pyenv/pyenv /usr/local/lib/pyenv ;\ + # install + GNU_ARCH="$(dpkg-architecture --query DEB_BUILD_GNU_TYPE)" ;\ + PYENV_ROOT=/usr/local/lib/pyenv CONFIGURE_OPTS="--build=$GNU_ARCH --enable-loadable-sqlite-extensions --enable-shared --with-system-expat --with-system-ffi --without-ensurepip --with-shared" /usr/local/lib/pyenv/bin/pyenv install $PYTHON_VERSION ;\ + # keep the needed .so files + # ignore libpython - that one comes from the pyenv instalation + find /usr/local -type f -executable -not \( -name '*tkinter*' \) -exec scanelf --needed --nobanner --format '%n#p' '{}' ';' \ + | tr ',' '\n' \ + | sort -u \ + | awk 'system("[ -e /usr/local/lib/" $1 " ]") == 0 { next } { print "so:" $1 }' \ + | grep -ve 'libpython' \ + | xargs -rt apk add --no-cache --virtual .python-rundeps ;\ + # for debug + # | xargs -n1 echo ;\ + # delete everything from pyenv except the installed version + # this throws an error but we ignore it + find /usr/local/lib/pyenv/ -mindepth 1 -name versions -prune -o -exec rm -rf {} \; || true ;\ + # delete files to to reduce container size + # tips taken from main python docker repo + find /usr/local/lib/pyenv/versions/$PYTHON_VERSION/ -depth \( -name '*.pyo' -o -name '*.pyc' -o -name 'test' -o -name 'tests' \) -exec rm -rf '{}' + ;\ + # symlink the binaries + ln -s /usr/local/lib/pyenv/versions/$PYTHON_VERSION/bin/* $PYTHON_PATH ;\ + ln -s /usr/local/lib/pyenv/versions/$PYTHON_VERSION/include/python$PYTHON_MINOR_VERSION /usr/include/ ;\ + # set timezone info + ln -fs /usr/share/zoneinfo/Etc/UTC /etc/localtime ;\ + # remove build dependencies and any leftover apk cache + apk del --no-cache --purge .build-deps ;\ + rm -rf /var/cache/apk/* ;\ + # set proper permission to run entrypoint script + chmod a+x /entrypoint.sh + +# install requirements +# this way when you build you won't need to install again +# and since COPY is cached we don't need to wait +ONBUILD COPY requirements.txt /tmp/requirements.txt + +# Run the dependencies installer and then allow it to be run again if needed. +ONBUILD RUN /entrypoint.sh -r /tmp/requirements.txt +ONBUILD RUN rm -f /requirements.installed + +# since we will be "always" mounting the volume, we can set this up +ENTRYPOINT ["/usr/bin/dumb-init"] +CMD ["python"] diff --git a/3.8-onbuild/entrypoint.sh b/3.8-onbuild/entrypoint.sh new file mode 100755 index 0000000..50c2ab1 --- /dev/null +++ b/3.8-onbuild/entrypoint.sh @@ -0,0 +1,153 @@ +#!/usr/bin/dumb-init /bin/bash +set -e + +APK_REQUIREMENTS=() +BUILD_REQUIREMENTS=() +PIP_REQUIREMENTS=() +APKFILE='/apk-requirements.txt' +BUILDFILE='/build-requirements.txt' +REQFILE='/requirements.txt' +VERBOSITY=1 + +TMP_REQFILE='/tmp/requirements.txt' + +function usage () { + cat <<"EOF" +Usage: $0 [-a -b -p -A -B -P -r -q -x] [--] + -a : APK requirement. Can be specified multiple times. + -b : APK build requirement. These will be removed at the end to save space. + -p : Pip requirement. Can be specified multiple times. + + -A : apk-requirements.txt file location, default: /apk-requirements.txt + -B : build-requirements.txt file location, default: /build-requirements.txt + -P : requirements.txt file location, default: /requirements.txt + -r : same as above, just to match Pip's -r flag. + + -q : quiet, doesn't print anything at all. + -x : Bash debug mode. Extremely verbose! + + -- : Separator for flags and your command + + Whatever you provide after your arguments is run at the end. +EOF + exit 1 +} + +function vlog () { + if [ $VERBOSITY -gt 0 ]; then + echo $1 + fi +} + +# Get and process arguments +while getopts ":a:b:p:A:B:P:r:qx" opt; do + case $opt in + a) APK_REQUIREMENTS+=("$OPTARG") ;; + b) BUILD_REQUIREMENTS+=("$OPTARG") ;; + p) PIP_REQUIREMENTS+=("$OPTARG") ;; + A) APKFILE="$OPTARG" ;; + B) BUILDFILE="$OPTARG" ;; + P) REQFILE="$OPTARG" ;; + r) REQFILE="$OPTARG" ;; + q) VERBOSITY=0 ;; + x) set -x ;; + \?) + echo "Invalid option: -$OPTARG" >&2 + usage + ;; + :) + echo "Option -$OPTARG requires an argument." >&2 + usage + ;; + esac +done + +# Bad arguments +if [ $? -ne 0 ]; +then + usage +fi + +# Strip out all the arguments that have been processed +shift $((OPTIND-1)) + +# If there's a double dash at the end, get that off +[[ $1 = "--" ]] && shift + +# Make some common flags objects +PIP_FLAGS='' +if [ $VERBOSITY -eq 0 ]; then + PIP_FLAGS="$PIP_FLAGS -q" +fi + +APK_FLAGS='--no-cache --no-progress' +if [ $VERBOSITY -eq 0 ]; then + APK_FLAGS="$APK_FLAGS -q" +fi + +# Don't do anything if we've already done this. +if [[ ! -f /requirements.installed ]]; then + vlog "First run, checking for any requirements..." + + # Install any APK requirements + if [[ -f "$APKFILE" ]]; then + vlog "APK requirements file detected!" + APK_REQUIREMENTS+=($( cat "$APKFILE" )) + fi + + if [[ -f "$BUILDFILE" ]]; then + vlog "Build requirements file detected!" + BUILD_REQUIREMENTS+=($( cat "$BUILDFILE" )) + fi + + # Unfortunately the Alpine repositories are in a slightly inconsistent state for now-- python2 only exists in 'edge', not main. + # if [[ "$PYTHON_VERSION" == '2' ]]; then BUILD_PACKAGES="$(echo $BUILD_PACKAGES | sed -e 's/python2/python/g')"; fi \ + vlog "Installing all APK requirements..." + apk add $APK_FLAGS $BUILD_PACKAGES "${APK_REQUIREMENTS[@]}" "${BUILD_REQUIREMENTS[@]}" + + # Install any Pip requirements + if [[ -f "$REQFILE" && "$(cat $REQFILE | wc -l)" -gt 0 ]]; then + # Do this check a little early-- since we merge cli in with file, + # we'd get a false positive for logging otherwise. + vlog "Pip requirements file detected!" + fi + + # If we use CLI parameters, we'll have to reassign this. + TARGET_REQFILE="$REQFILE" + if [[ ${#PIP_REQUIREMENTS[@]} -gt 0 ]]; then + # Put all Pip requirements into the same file. + printf "%s\n" "${PIP_REQUIREMENTS[@]}" >> "$TMP_REQFILE" + + if [[ -f "$REQFILE" && "$(cat $REQFILE | wc -l)" -gt 0 ]]; then + cat "$REQFILE" >> "$TMP_REQFILE" + fi + + TARGET_REQFILE="$TMP_REQFILE" + fi + + if [[ -f $TARGET_REQFILE && "$(cat $TARGET_REQFILE | wc -l)" -gt 0 ]]; then + vlog "Upgrading Pip..." + pip install $PIP_FLAGS --upgrade pip + vlog "Installing all Pip requirements..." + pip install $PIP_FLAGS -r "$TARGET_REQFILE" + fi + + # Remove packages that were only required for build. + apk del $APK_FLAGS $BUILD_PACKAGES "${BUILD_REQUIREMENTS[@]}" + + vlog "Installing APK requirements..." + apk add $APK_FLAGS "${APK_REQUIREMENTS[@]}" + + touch /requirements.installed +else + vlog "/requirements.installed file exists-- skipping requirements installs." +fi + + +if [[ ! -z "$@" ]]; then + # If the user has given us a command, run it. + $@ +else + # Otherwise, default to running 'python'. + python +fi diff --git a/3.8-slim/Dockerfile b/3.8-slim/Dockerfile new file mode 100644 index 0000000..06aa447 --- /dev/null +++ b/3.8-slim/Dockerfile @@ -0,0 +1,116 @@ +FROM alpine:3.11 + +# VERSIONS +ENV ALPINE_VERSION=3.11 \ + PYTHON_VERSION=3.8.2 + +# PATHS +ENV PYTHON_PATH=/usr/local/bin/ \ + PATH="/usr/local/lib/python$PYTHON_VERSION/bin/:/usr/local/lib/pyenv/versions/$PYTHON_VERSION/bin:${PATH}" \ + # These are always installed. Notes: + # * dumb-init: a proper init system for containers, to reap zombie children + # * bash: For entrypoint, and debugging + # * ca-certificates: for SSL verification during Pip and easy_install + PACKAGES="\ + dumb-init \ + bash \ + ca-certificates \ + " \ + # PACKAGES needed to built python + PYTHON_BUILD_PACKAGES="\ + bzip2-dev \ + coreutils \ + dpkg \ + dpkg-dev \ + expat-dev \ + findutils \ + gcc \ + gdbm-dev \ + git \ + libc-dev \ + libffi-dev \ + libnsl-dev \ + libtirpc-dev \ + linux-headers \ + make \ + ncurses-dev \ + libressl-dev \ + pax-utils \ + readline-dev \ + sqlite-dev \ + tcl-dev \ + tk \ + tk-dev \ + util-linux-dev \ + xz-dev \ + zlib-dev \ + " \ + # These packages are not installed immediately, but are added at runtime or ONBUILD to shrink the image as much as possible. Notes: + # * build-base: used so we include the basic development packages (gcc) + # * linux-headers: commonly needed, and an unusual package name from Alpine. + # * lib6-compat: compatibility libraries for glibc + # * git: to ease up clones of repos + BUILD_PACKAGES="\ + build-base \ + linux-headers \ + libc6-compat \ + git \ + " + +# Copy in the entrypoint script -- this installs prerequisites on container start. +COPY entrypoint.sh /entrypoint.sh + +RUN set -ex ;\ + # find MAJOR and MINOR python versions based on $PYTHON_VERSION + export PYTHON_MAJOR_VERSION=$(echo "${PYTHON_VERSION}" | rev | cut -d"." -f3- | rev) ;\ + export PYTHON_MINOR_VERSION=$(echo "${PYTHON_VERSION}" | rev | cut -d"." -f2- | rev) ;\ + # replacing default repositories with edge ones + echo "http://dl-cdn.alpinelinux.org/alpine/v$ALPINE_VERSION/community" >> /etc/apk/repositories ;\ + echo "http://dl-cdn.alpinelinux.org/alpine/v$ALPINE_VERSION/main" >> /etc/apk/repositories ;\ + # Add the packages, with a CDN-breakage fallback if needed + apk add --no-cache $PACKAGES || \ + (sed -i -e 's/dl-cdn/dl-4/g' /etc/apk/repositories && apk add --no-cache $PACKAGES) ;\ + # Add packages just for the python build process with a CDN-breakage fallback if needed + apk add --no-cache --virtual .build-deps $PYTHON_BUILD_PACKAGES || \ + (sed -i -e 's/dl-cdn/dl-4/g' /etc/apk/repositories && apk add --no-cache --virtual .build-deps $PYTHON_BUILD_PACKAGES) ;\ + # turn back the clock -- so hacky! + echo "http://dl-cdn.alpinelinux.org/alpine/v$ALPINE_VERSION/main/" > /etc/apk/repositories ;\ + # echo "@community http://dl-cdn.alpinelinux.org/alpine/v$ALPINE_VERSION/community" >> /etc/apk/repositories ;\ + # echo "@testing http://dl-cdn.alpinelinux.org/alpine/v$ALPINE_VERSION/testing" >> /etc/apk/repositories ;\ + # echo "@edge-main http://dl-cdn.alpinelinux.org/alpine/edge/main" >> /etc/apk/repositories ;\ + # use pyenv to download and compile specific python version + git clone --depth 1 https://github.com/pyenv/pyenv /usr/local/lib/pyenv ;\ + # install + GNU_ARCH="$(dpkg-architecture --query DEB_BUILD_GNU_TYPE)" ;\ + PYENV_ROOT=/usr/local/lib/pyenv CONFIGURE_OPTS="--build=$GNU_ARCH --enable-loadable-sqlite-extensions --enable-shared --with-system-expat --with-system-ffi --without-ensurepip --with-shared" /usr/local/lib/pyenv/bin/pyenv install $PYTHON_VERSION ;\ + # keep the needed .so files + # ignore libpython - that one comes from the pyenv instalation + find /usr/local -type f -executable -not \( -name '*tkinter*' \) -exec scanelf --needed --nobanner --format '%n#p' '{}' ';' \ + | tr ',' '\n' \ + | sort -u \ + | awk 'system("[ -e /usr/local/lib/" $1 " ]") == 0 { next } { print "so:" $1 }' \ + | grep -ve 'libpython' \ + | xargs -rt apk add --no-cache --virtual .python-rundeps ;\ + # for debug + # | xargs -n1 echo ;\ + # delete everything from pyenv except the installed version + # this throws an error but we ignore it + find /usr/local/lib/pyenv/ -mindepth 1 -name versions -prune -o -exec rm -rf {} \; || true ;\ + # delete files to to reduce container size + # tips taken from main python docker repo + find /usr/local/lib/pyenv/versions/$PYTHON_VERSION/ -depth \( -name '*.pyo' -o -name '*.pyc' -o -name 'test' -o -name 'tests' \) -exec rm -rf '{}' + ;\ + # symlink the binaries + ln -s /usr/local/lib/pyenv/versions/$PYTHON_VERSION/bin/* $PYTHON_PATH ;\ + ln -s /usr/local/lib/pyenv/versions/$PYTHON_VERSION/include/python$PYTHON_MINOR_VERSION /usr/include/ ;\ + # remove build dependencies and any leftover apk cache + apk del --no-cache --purge .build-deps ;\ + rm -rf /var/cache/apk/* ;\ + # set proper permission to run entrypoint script + chmod a+x /entrypoint.sh + +# This script installs APK and Pip prerequisites on container start, or ONBUILD. Notes: +# * Reads the -a flags and /apk-requirements.txt for install requests +# * Reads the -b flags and /build-requirements.txt for build packages -- removed when build is complete +# * Reads the -p flags and /requirements.txt for Pip packages +# * Reads the -r flag to specify a different file path for /requirements.txt +ENTRYPOINT ["/usr/bin/dumb-init", "bash", "/entrypoint.sh"] diff --git a/3.8-slim/entrypoint.sh b/3.8-slim/entrypoint.sh new file mode 100644 index 0000000..50c2ab1 --- /dev/null +++ b/3.8-slim/entrypoint.sh @@ -0,0 +1,153 @@ +#!/usr/bin/dumb-init /bin/bash +set -e + +APK_REQUIREMENTS=() +BUILD_REQUIREMENTS=() +PIP_REQUIREMENTS=() +APKFILE='/apk-requirements.txt' +BUILDFILE='/build-requirements.txt' +REQFILE='/requirements.txt' +VERBOSITY=1 + +TMP_REQFILE='/tmp/requirements.txt' + +function usage () { + cat <<"EOF" +Usage: $0 [-a -b -p -A -B -P -r -q -x] [--] + -a : APK requirement. Can be specified multiple times. + -b : APK build requirement. These will be removed at the end to save space. + -p : Pip requirement. Can be specified multiple times. + + -A : apk-requirements.txt file location, default: /apk-requirements.txt + -B : build-requirements.txt file location, default: /build-requirements.txt + -P : requirements.txt file location, default: /requirements.txt + -r : same as above, just to match Pip's -r flag. + + -q : quiet, doesn't print anything at all. + -x : Bash debug mode. Extremely verbose! + + -- : Separator for flags and your command + + Whatever you provide after your arguments is run at the end. +EOF + exit 1 +} + +function vlog () { + if [ $VERBOSITY -gt 0 ]; then + echo $1 + fi +} + +# Get and process arguments +while getopts ":a:b:p:A:B:P:r:qx" opt; do + case $opt in + a) APK_REQUIREMENTS+=("$OPTARG") ;; + b) BUILD_REQUIREMENTS+=("$OPTARG") ;; + p) PIP_REQUIREMENTS+=("$OPTARG") ;; + A) APKFILE="$OPTARG" ;; + B) BUILDFILE="$OPTARG" ;; + P) REQFILE="$OPTARG" ;; + r) REQFILE="$OPTARG" ;; + q) VERBOSITY=0 ;; + x) set -x ;; + \?) + echo "Invalid option: -$OPTARG" >&2 + usage + ;; + :) + echo "Option -$OPTARG requires an argument." >&2 + usage + ;; + esac +done + +# Bad arguments +if [ $? -ne 0 ]; +then + usage +fi + +# Strip out all the arguments that have been processed +shift $((OPTIND-1)) + +# If there's a double dash at the end, get that off +[[ $1 = "--" ]] && shift + +# Make some common flags objects +PIP_FLAGS='' +if [ $VERBOSITY -eq 0 ]; then + PIP_FLAGS="$PIP_FLAGS -q" +fi + +APK_FLAGS='--no-cache --no-progress' +if [ $VERBOSITY -eq 0 ]; then + APK_FLAGS="$APK_FLAGS -q" +fi + +# Don't do anything if we've already done this. +if [[ ! -f /requirements.installed ]]; then + vlog "First run, checking for any requirements..." + + # Install any APK requirements + if [[ -f "$APKFILE" ]]; then + vlog "APK requirements file detected!" + APK_REQUIREMENTS+=($( cat "$APKFILE" )) + fi + + if [[ -f "$BUILDFILE" ]]; then + vlog "Build requirements file detected!" + BUILD_REQUIREMENTS+=($( cat "$BUILDFILE" )) + fi + + # Unfortunately the Alpine repositories are in a slightly inconsistent state for now-- python2 only exists in 'edge', not main. + # if [[ "$PYTHON_VERSION" == '2' ]]; then BUILD_PACKAGES="$(echo $BUILD_PACKAGES | sed -e 's/python2/python/g')"; fi \ + vlog "Installing all APK requirements..." + apk add $APK_FLAGS $BUILD_PACKAGES "${APK_REQUIREMENTS[@]}" "${BUILD_REQUIREMENTS[@]}" + + # Install any Pip requirements + if [[ -f "$REQFILE" && "$(cat $REQFILE | wc -l)" -gt 0 ]]; then + # Do this check a little early-- since we merge cli in with file, + # we'd get a false positive for logging otherwise. + vlog "Pip requirements file detected!" + fi + + # If we use CLI parameters, we'll have to reassign this. + TARGET_REQFILE="$REQFILE" + if [[ ${#PIP_REQUIREMENTS[@]} -gt 0 ]]; then + # Put all Pip requirements into the same file. + printf "%s\n" "${PIP_REQUIREMENTS[@]}" >> "$TMP_REQFILE" + + if [[ -f "$REQFILE" && "$(cat $REQFILE | wc -l)" -gt 0 ]]; then + cat "$REQFILE" >> "$TMP_REQFILE" + fi + + TARGET_REQFILE="$TMP_REQFILE" + fi + + if [[ -f $TARGET_REQFILE && "$(cat $TARGET_REQFILE | wc -l)" -gt 0 ]]; then + vlog "Upgrading Pip..." + pip install $PIP_FLAGS --upgrade pip + vlog "Installing all Pip requirements..." + pip install $PIP_FLAGS -r "$TARGET_REQFILE" + fi + + # Remove packages that were only required for build. + apk del $APK_FLAGS $BUILD_PACKAGES "${BUILD_REQUIREMENTS[@]}" + + vlog "Installing APK requirements..." + apk add $APK_FLAGS "${APK_REQUIREMENTS[@]}" + + touch /requirements.installed +else + vlog "/requirements.installed file exists-- skipping requirements installs." +fi + + +if [[ ! -z "$@" ]]; then + # If the user has given us a command, run it. + $@ +else + # Otherwise, default to running 'python'. + python +fi diff --git a/3.8/Dockerfile b/3.8/Dockerfile new file mode 100644 index 0000000..1c141c4 --- /dev/null +++ b/3.8/Dockerfile @@ -0,0 +1,113 @@ +FROM alpine:3.11 + +# VERSIONS +ENV ALPINE_VERSION=3.11 \ + PYTHON_VERSION=3.8.2 + +# PATHS +ENV PYTHON_PATH=/usr/local/bin/ \ + PATH="/usr/local/lib/python$PYTHON_VERSION/bin/:/usr/local/lib/pyenv/versions/$PYTHON_VERSION/bin:${PATH}" \ + # These are always installed. + # * dumb-init: a proper init system for containers, to reap zombie children + # * musl: standard C library + # * lib6-compat: compatibility libraries for glibc + # * linux-headers: commonly needed, and an unusual package name from Alpine. + # * build-base: used so we include the basic development packages (gcc) + # * bash: so we can access /bin/bash + # * git: to ease up clones of repos + # * ca-certificates: for SSL verification during Pip and easy_install + PACKAGES="\ + dumb-init \ + musl \ + libc6-compat \ + linux-headers \ + build-base \ + bash \ + git \ + ca-certificates \ + libressl-dev \ + libffi-dev \ + tzdata \ + " \ + # PACKAGES needed to built python + PYTHON_BUILD_PACKAGES="\ + bzip2-dev \ + coreutils \ + dpkg \ + dpkg-dev \ + expat-dev \ + findutils \ + gcc \ + gdbm-dev \ + libc-dev \ + libffi-dev \ + libnsl-dev \ + libtirpc-dev \ + linux-headers \ + make \ + ncurses-dev \ + libressl-dev \ + pax-utils \ + readline-dev \ + sqlite-dev \ + tcl-dev \ + tk \ + tk-dev \ + util-linux-dev \ + xz-dev \ + zlib-dev \ + " + +RUN set -ex ;\ + # find MAJOR and MINOR python versions based on $PYTHON_VERSION + export PYTHON_MAJOR_VERSION=$(echo "${PYTHON_VERSION}" | rev | cut -d"." -f3- | rev) ;\ + export PYTHON_MINOR_VERSION=$(echo "${PYTHON_VERSION}" | rev | cut -d"." -f2- | rev) ;\ + # replacing default repositories with edge ones + echo "http://dl-cdn.alpinelinux.org/alpine/v$ALPINE_VERSION/community" >> /etc/apk/repositories ;\ + echo "http://dl-cdn.alpinelinux.org/alpine/v$ALPINE_VERSION/main" >> /etc/apk/repositories ;\ + # Add the packages, with a CDN-breakage fallback if needed + apk add --no-cache $PACKAGES || \ + (sed -i -e 's/dl-cdn/dl-4/g' /etc/apk/repositories && apk add --no-cache $PACKAGES) ;\ + # Add packages just for the python build process with a CDN-breakage fallback if needed + apk add --no-cache --virtual .build-deps $PYTHON_BUILD_PACKAGES || \ + (sed -i -e 's/dl-cdn/dl-4/g' /etc/apk/repositories && apk add --no-cache --virtual .build-deps $PYTHON_BUILD_PACKAGES) ;\ + # turn back the clock -- so hacky! + echo "http://dl-cdn.alpinelinux.org/alpine/v$ALPINE_VERSION/main/" > /etc/apk/repositories ;\ + # echo "@community http://dl-cdn.alpinelinux.org/alpine/v$ALPINE_VERSION/community" >> /etc/apk/repositories ;\ + # echo "@testing http://dl-cdn.alpinelinux.org/alpine/v$ALPINE_VERSION/testing" >> /etc/apk/repositories ;\ + # echo "@edge-main http://dl-cdn.alpinelinux.org/alpine/edge/main" >> /etc/apk/repositories ;\ + # use pyenv to download and compile specific python version + git clone --depth 1 https://github.com/pyenv/pyenv /usr/local/lib/pyenv ;\ + # install + GNU_ARCH="$(dpkg-architecture --query DEB_BUILD_GNU_TYPE)" ;\ + # flag explanation: + # --with-shared : python-dev + PYENV_ROOT=/usr/local/lib/pyenv CONFIGURE_OPTS="--build=$GNU_ARCH --enable-loadable-sqlite-extensions --enable-shared --with-system-expat --with-system-ffi --without-ensurepip --with-shared" /usr/local/lib/pyenv/bin/pyenv install $PYTHON_VERSION ;\ + # keep the needed .so files + # ignore libpython - that one comes from the pyenv instalation + find /usr/local -type f -executable -not \( -name '*tkinter*' \) -exec scanelf --needed --nobanner --format '%n#p' '{}' ';' \ + | tr ',' '\n' \ + | sort -u \ + | awk 'system("[ -e /usr/local/lib/" $1 " ]") == 0 { next } { print "so:" $1 }' \ + | grep -ve 'libpython' \ + | xargs -rt apk add --no-cache --virtual .python-rundeps ;\ + # for debug + # | xargs -n1 echo ;\ + # delete everything from pyenv except the installed version + # this throws an error but we ignore it + find /usr/local/lib/pyenv/ -mindepth 1 -name versions -prune -o -exec rm -rf {} \; || true ;\ + # delete files to to reduce container size + # tips taken from main python docker repo + find /usr/local/lib/pyenv/versions/$PYTHON_VERSION/ -depth \( -name '*.pyo' -o -name '*.pyc' -o -name 'test' -o -name 'tests' \) -exec rm -rf '{}' + ;\ + # symlink the binaries + ln -s /usr/local/lib/pyenv/versions/$PYTHON_VERSION/bin/* $PYTHON_PATH ;\ + ln -s /usr/local/lib/pyenv/versions/$PYTHON_VERSION/include/python$PYTHON_MINOR_VERSION /usr/include/ ;\ + # set timezone info + ln -fs /usr/share/zoneinfo/Etc/UTC /etc/localtime ;\ + # remove build dependencies and any leftover apk cache + apk del --no-cache --purge .build-deps ;\ + rm -rf /var/cache/apk/* + +# since we will be "always" mounting the volume, we can set this up +ENTRYPOINT ["/usr/bin/dumb-init"] +CMD ["python"] diff --git a/README.md b/README.md index 6c28a91..1b362a8 100644 --- a/README.md +++ b/README.md @@ -2,59 +2,114 @@ [![Docker Stars](https://img.shields.io/docker/stars/jfloff/alpine-python.svg)][hub] [![Docker Pulls](https://img.shields.io/docker/pulls/jfloff/alpine-python.svg)][hub] -[![Docker Layers](https://badge.imagelayers.io/jfloff/alpine-python:latest.svg)](https://imagelayers.io/?images=jfloff/alpine-python:latest 'Get your own badge on imagelayers.io') +[![Build Status](https://travis-ci.org/jfloff/alpine-python.svg?branch=master)][travis] [hub]: https://hub.docker.com/r/jfloff/alpine-python/ - -A small Python Docker image based on [Alpine Linux](http://alpinelinux.org/). The image is only 225 MB and it includes `python3-dev`. - +[travis]: https://travis-ci.org/jfloff/alpine-python + +A small Python Docker image based on [Alpine Linux](http://alpinelinux.org/). + + + +- [alpine-python](#alpine-python) + - [Supported tags](#supported-tags) + - [Why?](#why) + - [Details](#details) + - [Usage](#usage) + - [Usage of `onbuild` images](#usage-of-onbuild-images) + - [Usage of `slim` images](#usage-of-slim-images) + - [Via `docker run`](#via-docker-run) + - [Pip Dependencies](#pip-dependencies) + - [Run-Time Dependencies](#run-time-dependencies) + - [Build-Time Dependencies](#build-time-dependencies) + - [Creating Images](#creating-images) + - [Debugging](#debugging) + - [Additional Arguments](#additional-arguments) + - [Ecosystem](#ecosystem) + - [Contribution](#contribution) + - [License](#license) + + ## Supported tags -* **2.7 ([Dockerfile](https://github.com/jfloff/alpine-python/blob/master/2.7/Dockerfile))** -* **2.7-onbuild ([Dockerfile](https://github.com/jfloff/alpine-python/blob/master/2.7/onbuild/Dockerfile))** -* **3.4 ([Dockerfile](https://github.com/jfloff/alpine-python/blob/master/3.4/Dockerfile))** -* **3.4-onbuild ([Dockerfile](https://github.com/jfloff/alpine-python/blob/master/3.4/onbuild/Dockerfile))** - -**NOTE:** `onbuild` images install the `requirements.txt` of your project from the get go. This allows you to cache your requirements right in the build. _Make sure you are in the same directory of your `requirements.txt` file_. +* **`2.7` ([2.7/Dockerfile](https://github.com/jfloff/alpine-python/blob/master/2.7/Dockerfile))** +* **`2.7-onbuild` ([2.7-onbuild/Dockerfile](https://github.com/jfloff/alpine-python/blob/master/2.7-onbuild/Dockerfile))** +* **`2.7-slim` ([2.7-slim/Dockerfile](https://github.com/jfloff/alpine-python/blob/master/2.7-slim/Dockerfile))** +* **`3.6` ([Dockerfile](https://github.com/jfloff/alpine-python/blob/master/3.6/Dockerfile))** +* **`3.6-onbuild` ([Dockerfile](https://github.com/jfloff/alpine-python/blob/master/3.6-onbuild/Dockerfile))** +* **`3.6-slim` ([Dockerfile](https://github.com/jfloff/alpine-python/blob/master/3.6-slim/Dockerfile))** +* **`3.7` ([Dockerfile](https://github.com/jfloff/alpine-python/blob/master/3.7/Dockerfile))** +* **`3.7-onbuild` ([Dockerfile](https://github.com/jfloff/alpine-python/blob/master/3.7-onbuild/Dockerfile))** +* **`3.7-slim` ([Dockerfile](https://github.com/jfloff/alpine-python/blob/master/3.7-slim/Dockerfile))** +* **`3.8` `latest` ([Dockerfile](https://github.com/jfloff/alpine-python/blob/master/3.8/Dockerfile))** +* **`3.8-onbuild` `latest-onbuild` ([Dockerfile](https://github.com/jfloff/alpine-python/blob/master/3.8-onbuild/Dockerfile))** +* **`3.8-slim` `latest-slim` ([Dockerfile](https://github.com/jfloff/alpine-python/blob/master/3.8-slim/Dockerfile))** + +**NOTES:** +- `onbuild` images install the `requirements.txt` of your project from the get go. This allows you to cache your requirements right in the build. _Make sure you are in the same directory of your `requirements.txt` file_. ## Why? -The default docker python images are too [big](https://github.com/docker-library/python/issues/45), much larger than they need to be. Hence I built this simple image based on [docker-alpine](https://github.com/gliderlabs/docker-alpine), that has everything needed for the most common python projects - including `python3-dev` (which is not common in most minimal alpine python packages). - -``` -REPOSITORY TAG VIRTUAL SIZE -jfloff/alpine-python 3.4 225.7 MB -python 3.4 685.5 MB -python 3.4-slim 215.1 MB -``` - -We actually get the same size as `python:3.4-slim` *but* with `python3-dev` installed (that's around 55MB). - -Perhaps this could be even more smaller, but I'm not an Alpine guru. **Feel free to post a PR.** +The default docker python images are too [big](https://github.com/docker-library/python/issues/45), much larger than they need to be. Hence I built this simple image based on [docker-alpine](https://github.com/gliderlabs/docker-alpine), that has everything needed for the most common python projects - including `python-dev` (which is not common in most minimal alpine python packages). + +|REPOSITORY|TAG|SIZE| +|-|-|-| +|jfloff/alpine-python|2.7-slim|60MB| +|python|2.7-slim|120MB| +|python|2.7-alpine|61.2MB| +|||| +|jfloff/alpine-python|2.7|235MB| +|python|2.7|912MB| +|||| +|jfloff/alpine-python|3.6-slim|76.3MB| +|python|3.6-slim|138MB| +|python|3.6-alpine|79MB| +|||| +|jfloff/alpine-python|3.6|252MB| +|python|3.6|922MB| +|||| +|jfloff/alpine-python|3.7-slim|80.4MB| +|python|3.7-slim|86.7MB| +|python|3.7-alpine|143MB| +|||| +|jfloff/alpine-python|3.7|256MB| +|python|3.7|927MB| + +Perhaps this could be even smaller, but I'm not an Alpine guru. **Feel free to post a PR.** +## Details +* Installs `build-base` and `python-dev`, allowing the use of more advanced packages such as `gevent` +* Installs `bash` allowing interaction with the container +* Just like the main `python` docker image, it creates useful symlinks that are expected to exist, e.g. `python3` > `python`, `pip2.7` > `pip`, etc.) +* Added `testing` and `community` repositories to Alpine's `/etc/apk/repositories` file ## Usage + This image runs `python` command on `docker run`. You can either specify your own command, e.g: ```shell docker run --rm -ti jfloff/alpine-python python hello.py ``` -Or extend this images using your custom `Dockerfile`, e.g: +You can also access `bash` inside the container: +```shell +docker run --rm -ti jfloff/alpine-python bash +``` + +## Usage of `onbuild` images + +These images can be used to bake your dependencies into an image by extending the plain python images. To do so, create a custom `Dockerfile` like this: ```dockerfile -FROM jfloff/alpine-python:3.4-onbuild +FROM jfloff/alpine-python:3.6-onbuild # for a flask server EXPOSE 5000 CMD python manage.py runserver ``` -Dont' forget to build _your_ image: +Don't forget to build that `Dockerfile`: ```shell docker build --rm=true -t jfloff/app . -``` -You can also access `bash` inside the container: -```shell -docker run --rm -ti jfloff/alpine-python /bin/bash +docker run --rm -t jfloff/app ``` Personally, I build an extended `Dockerfile` version (like shown above), and mount my specific application inside the container: @@ -62,12 +117,125 @@ Personally, I build an extended `Dockerfile` version (like shown above), and mou docker run --rm -v "$(pwd)":/home/app -w /home/app -p 5000:5000 -ti jfloff/app ``` -## Details -* Installs `python-dev` allowing the use of more advanced packages such as `gevent` -* Installs `bash` allowing interaction with the container -* Just like the main `python` docker image, it creates useful symlinks that are expected to exist, e.g. `python3.4` > `python`, `pip2.7` > `pip`, etc.) -* Added `testing` repository to Alpine's `/etc/apk/repositories` file +## Usage of `slim` images + +These images are very small to download, and can install requirements at run-time via flags. The install only happens the first time the container is run, and dependencies can be baked in (see Creating Images). + +#### Via `docker run` +These images can be run in multiple ways. With no arguments, it will run `python` interactively: +```shell +docker run --rm -ti jfloff/alpine-python:2.7-slim +``` + +If you specify a command, they will run that: +```shell +docker run --rm -ti jfloff/alpine-python:2.7-slim python hello.py +``` + +#### Pip Dependencies +Pip dependencies can be installed by the `-p` switch, or a `requirements.txt` file. + +If the file is at `/requirements.txt` it will be automatically read for dependencies. If not, use the `-P` or `-r` switch to specify a file. +```shell +# This runs interactive Python with 'simplejson' and 'requests' installed +docker run --rm -ti jfloff/alpine-python:2.7-slim -p simplejson -p requests + +# Don't forget to add '--' after your dependencies to run a custom command: +docker run --rm -ti jfloff/alpine-python:2.7-slim -p simplejson -p requests -- python hello.py + +# This accomplishes the same thing by mounting a requirements.txt in: +echo 'simplejson' > requirements.txt +echo 'requests' > requirements.txt +docker run --rm -ti \ + -v requirements.txt:/requirements.txt \ + jfloff/alpine-python:2.7-slim python hello.py + +# This does too, but with the file somewhere else: +echo 'simplejson requests' > myapp/requirements.txt +docker run --rm -ti \ + -v myapp:/usr/src/app \ + jfloff/alpine-python:2.7-slim \ + -r /usr/src/app/requirements.txt \ + -- python /usr/src/app/hello.py +``` + +#### Run-Time Dependencies +Alpine package dependencies can be installed by the `-a` switch, or an `apk-requirements.txt` file. + +If the file is at `/apk-requirements.txt` it will be automatically read for dependencies. If not, use the `-A` switch to specify a file. + +You can also try installing some Python modules via this method, but it is possible for Pip to interfere if it detects a version problem. +```shell +# Unknown why you'd need to do this, but you can! +docker run --rm -ti jfloff/alpine-python:2.7-slim -a openssl -- python hello.py + +# This installs libxml2 module faster than via Pip, but then Pip reinstalls it because Ajenti's dependencies make it think it's the wrong version. +docker run --rm -ti jfloff/alpine-python:2.7-slim -a py-libxml2 -p ajenti +``` + +#### Build-Time Dependencies +Build-time Alpine package dependencies (such as compile headers) can be installed by the `-b` switch, or a `build-requirements.txt` file. They will be removed after the dependencies are installed to save space. + +If the file is at `/build-requirements.txt` it will be automatically read for dependencies. If not, use the `-B` switch to specify a file. + +`build-base`, `linux-headers` and `python-dev` are always build dependencies, you don't need to include them. +```shell +docker run --rm -ti jfloff/alpine-python:2.7-slim \ + -p gevent \ + -p libxml2 \ + -b libxslt-dev \ + -b libxml-dev \ + -- python hello.py +``` + +#### Creating Images +Similar to the onbuild images, dependencies can be baked into a new image by using a custom `Dockerfile`, e.g: +```dockerfile +FROM jfloff/alpine-python:2.7-slim +RUN /entrypoint.sh \ + -p ajenti-panel \ + -p ajenti.plugin.dashboard \ + -p ajenti.plugin.settings \ + -p ajenti.plugin.plugins \ + -b libxml2-dev \ + -b libxslt-dev \ + -b libffi-dev \ + -b openssl-dev \ +&& echo +CMD ["ajenti-panel"] +# you won't be able to add more dependencies later though-- see 'Debugging' +``` + +#### Debugging +The `/entrypoint.sh` script that manages dependencies in the slim images creates an empty file, `/requirements.installed`, telling the script not to install any dependencies after the container's first run. Removing this file will allow the script to work again if it is needed. + +You can use the `-x` flag to see everything the `/entrypoint.sh` script is doing. + +You can also access `bash` inside the container: +```shell +docker run --rm -ti jfloff/alpine-python:2.7-slim bash +``` + +#### Additional Arguments + +`-q`: silences output from `/entrypoint.sh` +`-x`: turns on Bash debugging, making the output very verbose. + +## Ecosystem + +These are some of the images that use `jfloff/alpine-python` as base image. *If you have another image that uses this as base image, please submit an issue or PR for it to be added. Image has to be published on Docker Hub.* + +- **[jfloff/alscipy](https://github.com/jfloff/docker-alscipy)** [![Docker Stars](https://img.shields.io/docker/stars/jfloff/alscipy.svg)][alscipy-hub] [![Docker Pulls](https://img.shields.io/docker/pulls/jfloff/alscipy.svg)][alscipy-hub] : image with common packages for Science in Alpine Python. +- **[jfloff/pywfm](https://github.com/jfloff/docker-pywfm)** [![Docker Stars](https://img.shields.io/docker/stars/jfloff/pywfm.svg)][pywfm-hub] [![Docker Pulls](https://img.shields.io/docker/pulls/jfloff/pywfm.svg)][pywfm-hub] : image from the python wrapper for Steffen Rendle's factorization machines library libFM. +- **[bismuthfoundation/Bismuth-Docker](https://github.com/bismuthfoundation/Bismuth-Docker)** [![Docker Stars](https://img.shields.io/docker/stars/eggdrasyl/bismuth-node.svg)][busmuth-hub] [![Docker Pulls](https://img.shields.io/docker/pulls/eggdrasyl/bismuth-node.svg)][busmuth-hub] : node and associated services, from scratch crypto-currency with Python codebase. + +[alscipy-hub]: https://hub.docker.com/r/jfloff/alscipy/ +[pywfm-hub]: https://hub.docker.com/r/jfloff/pywfm/ +[busmuth-hub]: https://hub.docker.com/r/eggdrasyl/bismuth-node/ + +## Contribution +Feel free to contribute with whatever you feel like this image is missing. There is also some changes that happen often like, updating Alpine or Python versions. Do not forget that this repo folders mirror **Python** version and **_not_** Alpine versions. ## License The code in this repository, unless otherwise noted, is MIT licensed. See the `LICENSE` file in this repository. diff --git a/build-all.sh b/build-all.sh new file mode 100755 index 0000000..221a350 --- /dev/null +++ b/build-all.sh @@ -0,0 +1,41 @@ +#!/bin/bash +repo='jfloff/alpine-python' +bold="$(tput bold)" +red="$(tput setaf 1)" +green="$(tput setaf 2)" +reset="$(tput sgr0)" + +build_with_status () { + tag="$1" + + printf "${b}Building ${tag} ... " + docker build . -t $tag &> /dev/null + if [[ $? -ne 0 ]]; then + printf "\n${bold}${red}BUILD FAILED${reset}" + exit 1 + fi + + echo 'print("something")' | docker run --rm -i $tag &> /dev/null + if [[ $? -ne 0 ]]; then + printf "${bold}${red}TEST FAILED${reset}" + exit 1 + else + printf "${bold}${green}SUCCESS${reset}" + fi + printf "\n" +} + +# quiet versions +pushd() { builtin pushd $1 > /dev/null; } +popd() { builtin popd > /dev/null; } + +# Move to where the script is +HERE="$( cd "$(dirname "$0")" ; pwd -P )" +cd "$HERE" + +# Find all the top-level dirs +for version in $(find -maxdepth 1 -not -name '.*' -type d -printf '%P\n' | sort); do + pushd $version + build_with_status $repo:$version + popd +done