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/3.4/slim/entrypoint.sh b/2.7-onbuild/entrypoint.sh similarity index 54% rename from 3.4/slim/entrypoint.sh rename to 2.7-onbuild/entrypoint.sh index c8c419c..50c2ab1 100755 --- a/3.4/slim/entrypoint.sh +++ b/2.7-onbuild/entrypoint.sh @@ -7,10 +7,13 @@ PIP_REQUIREMENTS=() APKFILE='/apk-requirements.txt' BUILDFILE='/build-requirements.txt' REQFILE='/requirements.txt' +VERBOSITY=1 + +TMP_REQFILE='/tmp/requirements.txt' function usage () { - echo <<"EOF" -Usage: $0 [-a -b -p -A -B -P -r] [--] + 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. @@ -20,6 +23,9 @@ Usage: $0 [-a -b -p -A -B -P -r] [--] -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. @@ -27,8 +33,14 @@ 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:" opt; do +while getopts ":a:b:p:A:B:P:r:qx" opt; do case $opt in a) APK_REQUIREMENTS+=("$OPTARG") ;; b) BUILD_REQUIREMENTS+=("$OPTARG") ;; @@ -36,7 +48,9 @@ while getopts ":a:b:p:A:B:P:r:" opt; do A) APKFILE="$OPTARG" ;; B) BUILDFILE="$OPTARG" ;; P) REQFILE="$OPTARG" ;; - r) REQFILE="$OPTARG" ;; + r) REQFILE="$OPTARG" ;; + q) VERBOSITY=0 ;; + x) set -x ;; \?) echo "Invalid option: -$OPTARG" >&2 usage @@ -60,37 +74,73 @@ 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 \ - - apk add --no-cache $BUILD_PACKAGES "${APK_REQUIREMENTS[@]}" "${BUILD_REQUIREMENTS[@]}" + vlog "Installing all APK requirements..." + apk add $APK_FLAGS $BUILD_PACKAGES "${APK_REQUIREMENTS[@]}" "${BUILD_REQUIREMENTS[@]}" # Install any Pip requirements - if [[ -f "$REQFILE" ]]; then - PIP_REQUIREMENTS+=($( cat "$REQFILE" )) - fi - - if [[ ${#PIP_REQUIREMENTS[@]} -gt 0 ]]; then - pip install --upgrade pip - pip install "${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 $BUILD_PACKAGES "${BUILD_REQUIREMENTS[@]}" + 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 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/onbuild/entrypoint.sh b/2.7-slim/entrypoint.sh similarity index 54% rename from 2.7/onbuild/entrypoint.sh rename to 2.7-slim/entrypoint.sh index c8c419c..50c2ab1 100755 --- a/2.7/onbuild/entrypoint.sh +++ b/2.7-slim/entrypoint.sh @@ -7,10 +7,13 @@ PIP_REQUIREMENTS=() APKFILE='/apk-requirements.txt' BUILDFILE='/build-requirements.txt' REQFILE='/requirements.txt' +VERBOSITY=1 + +TMP_REQFILE='/tmp/requirements.txt' function usage () { - echo <<"EOF" -Usage: $0 [-a -b -p -A -B -P -r] [--] + 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. @@ -20,6 +23,9 @@ Usage: $0 [-a -b -p -A -B -P -r] [--] -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. @@ -27,8 +33,14 @@ 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:" opt; do +while getopts ":a:b:p:A:B:P:r:qx" opt; do case $opt in a) APK_REQUIREMENTS+=("$OPTARG") ;; b) BUILD_REQUIREMENTS+=("$OPTARG") ;; @@ -36,7 +48,9 @@ while getopts ":a:b:p:A:B:P:r:" opt; do A) APKFILE="$OPTARG" ;; B) BUILDFILE="$OPTARG" ;; P) REQFILE="$OPTARG" ;; - r) REQFILE="$OPTARG" ;; + r) REQFILE="$OPTARG" ;; + q) VERBOSITY=0 ;; + x) set -x ;; \?) echo "Invalid option: -$OPTARG" >&2 usage @@ -60,37 +74,73 @@ 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 \ - - apk add --no-cache $BUILD_PACKAGES "${APK_REQUIREMENTS[@]}" "${BUILD_REQUIREMENTS[@]}" + vlog "Installing all APK requirements..." + apk add $APK_FLAGS $BUILD_PACKAGES "${APK_REQUIREMENTS[@]}" "${BUILD_REQUIREMENTS[@]}" # Install any Pip requirements - if [[ -f "$REQFILE" ]]; then - PIP_REQUIREMENTS+=($( cat "$REQFILE" )) - fi - - if [[ ${#PIP_REQUIREMENTS[@]} -gt 0 ]]; then - pip install --upgrade pip - pip install "${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 $BUILD_PACKAGES "${BUILD_REQUIREMENTS[@]}" + 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 diff --git a/2.7/Dockerfile b/2.7/Dockerfile index 30efe5c..01384e4 100644 --- a/2.7/Dockerfile +++ b/2.7/Dockerfile @@ -1,55 +1,109 @@ -FROM alpine:3.4 +FROM alpine:3.11 -# Install needed packages. Notes: -# * dumb-init: a proper init system for containers, to reap zombie children -# * musl: standard C library -# * 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 -# * python: the binaries themselves -# * python-dev: are used for gevent e.g. -# * py-setuptools: required only in major version 2, installs easy_install so we can install Pip. -ENV PACKAGES="\ - dumb-init \ - musl \ - linux-headers \ - build-base \ - bash \ - git \ - ca-certificates \ - python2 \ - python2-dev \ - py-setuptools \ -" +# VERSIONS +ENV ALPINE_VERSION=3.11 \ + PYTHON_VERSION=2.7.16 -RUN echo \ - # replacing default repositories with edge ones - && echo "http://dl-cdn.alpinelinux.org/alpine/edge/testing" > /etc/apk/repositories \ - && echo "http://dl-cdn.alpinelinux.org/alpine/edge/community" >> /etc/apk/repositories \ - && echo "http://dl-cdn.alpinelinux.org/alpine/edge/main" >> /etc/apk/repositories \ +# 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 \ + " - # 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) \ - - # turn back the clock -- so hacky! - && echo "http://dl-cdn.alpinelinux.org/alpine/v3.4/main/" > /etc/apk/repositories \ - && echo "@edge-testing http://dl-cdn.alpinelinux.org/alpine/edge/testing" >> /etc/apk/repositories \ - && echo "@edge-community http://dl-cdn.alpinelinux.org/alpine/edge/community" >> /etc/apk/repositories \ - && echo "@edge-main http://dl-cdn.alpinelinux.org/alpine/edge/main" >> /etc/apk/repositories \ - - # make some useful symlinks that are expected to exist - && if [[ ! -e /usr/bin/python ]]; then ln -sf /usr/bin/python2.7 /usr/bin/python; fi \ - && if [[ ! -e /usr/bin/python-config ]]; then ln -sf /usr/bin/python2.7-config /usr/bin/python-config; fi \ - && if [[ ! -e /usr/bin/easy_install ]]; then ln -sf /usr/bin/easy_install-2.7 /usr/bin/easy_install; fi \ - - # Install and upgrade Pip - && easy_install pip \ - && pip install --upgrade pip \ - && if [[ ! -e /usr/bin/pip ]]; then ln -sf /usr/bin/pip2.7 /usr/bin/pip; fi \ - && echo +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"] diff --git a/2.7/onbuild/Dockerfile b/2.7/onbuild/Dockerfile deleted file mode 100644 index ac09e2a..0000000 --- a/2.7/onbuild/Dockerfile +++ /dev/null @@ -1,13 +0,0 @@ -FROM jfloff/alpine-python:2.7 - -# Copy in the entrypoint script -- this installs prerequisites on container start. -COPY entrypoint.sh /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 diff --git a/2.7/slim/Dockerfile b/2.7/slim/Dockerfile deleted file mode 100644 index 6d8f27f..0000000 --- a/2.7/slim/Dockerfile +++ /dev/null @@ -1,63 +0,0 @@ -FROM alpine:3.4 - -# 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 -# * python: the binaries themselves -# * py-setuptools: required only in major version 2, installs easy_install so we can install Pip. -ENV PACKAGES="\ - dumb-init \ - bash \ - ca-certificates \ - python2 \ - py-setuptools \ -" - -RUN echo \ - # replacing default repositories with edge ones - && echo "http://dl-cdn.alpinelinux.org/alpine/edge/testing" > /etc/apk/repositories \ - && echo "http://dl-cdn.alpinelinux.org/alpine/edge/community" >> /etc/apk/repositories \ - && echo "http://dl-cdn.alpinelinux.org/alpine/edge/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) \ - - # turn back the clock -- so hacky! - && echo "http://dl-cdn.alpinelinux.org/alpine/v3.4/main/" > /etc/apk/repositories \ - && echo "@edge-testing http://dl-cdn.alpinelinux.org/alpine/edge/testing" >> /etc/apk/repositories \ - && echo "@edge-community http://dl-cdn.alpinelinux.org/alpine/edge/community" >> /etc/apk/repositories \ - && echo "@edge-main http://dl-cdn.alpinelinux.org/alpine/edge/main" >> /etc/apk/repositories \ - - # make some useful symlinks that are expected to exist - && if [[ ! -e /usr/bin/python ]]; then ln -sf /usr/bin/python2.7 /usr/bin/python; fi \ - && if [[ ! -e /usr/bin/python-config ]]; then ln -sf /usr/bin/python2.7-config /usr/bin/python-config; fi \ - && if [[ ! -e /usr/bin/easy_install ]]; then ln -sf /usr/bin/easy_install-2.7 /usr/bin/easy_install; fi \ - - # Install and upgrade Pip - && easy_install pip \ - && pip install --upgrade pip \ - && if [[ ! -e /usr/bin/pip ]]; then ln -sf /usr/bin/pip2.7 /usr/bin/pip; fi \ - && echo - -# Copy in the entrypoint script -- this installs prerequisites on container start. -COPY entrypoint.sh /entrypoint.sh - -# 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. -# * python-dev: are used for gevent e.g. -# * py-pip: provides pip, not needed once the software is built -ENV BUILD_PACKAGES="\ - build-base \ - linux-headers \ - python2-dev \ -" - -# 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.4/Dockerfile b/3.4/Dockerfile deleted file mode 100644 index 2cee94f..0000000 --- a/3.4/Dockerfile +++ /dev/null @@ -1,55 +0,0 @@ -FROM alpine:3.4 - -# Install needed packages. Notes: -# * dumb-init: a proper init system for containers, to reap zombie children -# * musl: standard C library -# * 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 -# * python: the binaries themselves -# * python-dev: are used for gevent e.g. -ENV PACKAGES="\ - dumb-init \ - musl \ - linux-headers \ - build-base \ - bash \ - git \ - ca-certificates \ - python3.4 \ - python3.4-dev \ -" - -RUN echo \ - # replacing default repositories with edge ones - && echo "http://dl-cdn.alpinelinux.org/alpine/edge/testing" > /etc/apk/repositories \ - && echo "http://dl-cdn.alpinelinux.org/alpine/edge/community" >> /etc/apk/repositories \ - && echo "http://dl-cdn.alpinelinux.org/alpine/edge/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) \ - - # turn back the clock -- so hacky! - && echo "http://dl-cdn.alpinelinux.org/alpine/v3.4/main/" > /etc/apk/repositories \ - && echo "@edge-testing http://dl-cdn.alpinelinux.org/alpine/edge/testing" >> /etc/apk/repositories \ - && echo "@edge-community http://dl-cdn.alpinelinux.org/alpine/edge/community" >> /etc/apk/repositories \ - && echo "@edge-main http://dl-cdn.alpinelinux.org/alpine/edge/main" >> /etc/apk/repositories \ - - # make some useful symlinks that are expected to exist - && if [[ ! -e /usr/bin/python ]]; then ln -sf /usr/bin/python3.4 /usr/bin/python; fi \ - && if [[ ! -e /usr/bin/python-config ]]; then ln -sf /usr/bin/python3.4-config /usr/bin/python-config; fi \ - && if [[ ! -e /usr/bin/idle ]]; then ln -sf /usr/bin/idle3.4 /usr/bin/idle; fi \ - && if [[ ! -e /usr/bin/pydoc ]]; then ln -sf /usr/bin/pydoc3.4 /usr/bin/pydoc; fi \ - && if [[ ! -e /usr/bin/easy_install ]]; then ln -sf /usr/bin/easy_install-3.4 /usr/bin/easy_install; fi \ - - # Install and upgrade Pip - && easy_install pip \ - && pip install --upgrade pip \ - && if [[ ! -e /usr/bin/pip ]]; then ln -sf /usr/bin/pip3.4 /usr/bin/pip; fi - -# since we will be "always" mounting the volume, we can set this up -ENTRYPOINT ["/usr/bin/dumb-init"] -CMD ["python"] diff --git a/3.4/onbuild/Dockerfile b/3.4/onbuild/Dockerfile deleted file mode 100644 index a4ada9a..0000000 --- a/3.4/onbuild/Dockerfile +++ /dev/null @@ -1,13 +0,0 @@ -FROM jfloff/alpine-python:3.4 - -# Copy in the entrypoint script -- this installs prerequisites on container start. -COPY entrypoint.sh /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 diff --git a/3.4/slim/Dockerfile b/3.4/slim/Dockerfile deleted file mode 100644 index 17410d3..0000000 --- a/3.4/slim/Dockerfile +++ /dev/null @@ -1,63 +0,0 @@ -FROM alpine:3.4 - -# 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 -# * python: the binaries themselves -ENV PACKAGES="\ - dumb-init \ - bash \ - ca-certificates \ - python3.4 \ -" - -RUN echo \ - # replacing default repositories with edge ones - && echo "http://dl-cdn.alpinelinux.org/alpine/edge/testing" > /etc/apk/repositories \ - && echo "http://dl-cdn.alpinelinux.org/alpine/edge/community" >> /etc/apk/repositories \ - && echo "http://dl-cdn.alpinelinux.org/alpine/edge/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) \ - - # turn back the clock -- so hacky! - && echo "http://dl-cdn.alpinelinux.org/alpine/v3.4/main/" > /etc/apk/repositories \ - && echo "@edge-testing http://dl-cdn.alpinelinux.org/alpine/edge/testing" >> /etc/apk/repositories \ - && echo "@edge-community http://dl-cdn.alpinelinux.org/alpine/edge/community" >> /etc/apk/repositories \ - && echo "@edge-main http://dl-cdn.alpinelinux.org/alpine/edge/main" >> /etc/apk/repositories \ - - # make some useful symlinks that are expected to exist - && if [[ ! -e /usr/bin/python ]]; then ln -sf /usr/bin/python3.4 /usr/bin/python; fi \ - && if [[ ! -e /usr/bin/python-config ]]; then ln -sf /usr/bin/python-config3.4 /usr/bin/python-config; fi \ - && if [[ ! -e /usr/bin/idle ]]; then ln -sf /usr/bin/idle3.4 /usr/bin/idle; fi \ - && if [[ ! -e /usr/bin/pydoc ]]; then ln -sf /usr/bin/pydoc3.4 /usr/bin/pydoc; fi \ - && if [[ ! -e /usr/bin/easy_install ]]; then ln -sf /usr/bin/easy_install-3.4 /usr/bin/easy_install; fi \ - - # Install and upgrade Pip - && easy_install pip \ - && pip install --upgrade pip \ - && if [[ ! -e /usr/bin/pip ]]; then ln -sf /usr/bin/pip3.4 /usr/bin/pip; fi \ - && echo - -# Copy in the entrypoint script -- this installs prerequisites on container start. -COPY entrypoint.sh /entrypoint.sh - -# 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. -# * python-dev: are used for gevent e.g. -# * py-pip: provides pip, not needed once the software is built -ENV BUILD_PACKAGES="\ - build-base \ - linux-headers \ - python3.4-dev \ -" - -# 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-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.4/onbuild/entrypoint.sh b/3.6-onbuild/entrypoint.sh similarity index 54% rename from 3.4/onbuild/entrypoint.sh rename to 3.6-onbuild/entrypoint.sh index c8c419c..50c2ab1 100755 --- a/3.4/onbuild/entrypoint.sh +++ b/3.6-onbuild/entrypoint.sh @@ -7,10 +7,13 @@ PIP_REQUIREMENTS=() APKFILE='/apk-requirements.txt' BUILDFILE='/build-requirements.txt' REQFILE='/requirements.txt' +VERBOSITY=1 + +TMP_REQFILE='/tmp/requirements.txt' function usage () { - echo <<"EOF" -Usage: $0 [-a -b -p -A -B -P -r] [--] + 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. @@ -20,6 +23,9 @@ Usage: $0 [-a -b -p -A -B -P -r] [--] -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. @@ -27,8 +33,14 @@ 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:" opt; do +while getopts ":a:b:p:A:B:P:r:qx" opt; do case $opt in a) APK_REQUIREMENTS+=("$OPTARG") ;; b) BUILD_REQUIREMENTS+=("$OPTARG") ;; @@ -36,7 +48,9 @@ while getopts ":a:b:p:A:B:P:r:" opt; do A) APKFILE="$OPTARG" ;; B) BUILDFILE="$OPTARG" ;; P) REQFILE="$OPTARG" ;; - r) REQFILE="$OPTARG" ;; + r) REQFILE="$OPTARG" ;; + q) VERBOSITY=0 ;; + x) set -x ;; \?) echo "Invalid option: -$OPTARG" >&2 usage @@ -60,37 +74,73 @@ 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 \ - - apk add --no-cache $BUILD_PACKAGES "${APK_REQUIREMENTS[@]}" "${BUILD_REQUIREMENTS[@]}" + vlog "Installing all APK requirements..." + apk add $APK_FLAGS $BUILD_PACKAGES "${APK_REQUIREMENTS[@]}" "${BUILD_REQUIREMENTS[@]}" # Install any Pip requirements - if [[ -f "$REQFILE" ]]; then - PIP_REQUIREMENTS+=($( cat "$REQFILE" )) - fi - - if [[ ${#PIP_REQUIREMENTS[@]} -gt 0 ]]; then - pip install --upgrade pip - pip install "${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 $BUILD_PACKAGES "${BUILD_REQUIREMENTS[@]}" + 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 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/2.7/slim/entrypoint.sh b/3.6-slim/entrypoint.sh similarity index 54% rename from 2.7/slim/entrypoint.sh rename to 3.6-slim/entrypoint.sh index c8c419c..50c2ab1 100755 --- a/2.7/slim/entrypoint.sh +++ b/3.6-slim/entrypoint.sh @@ -7,10 +7,13 @@ PIP_REQUIREMENTS=() APKFILE='/apk-requirements.txt' BUILDFILE='/build-requirements.txt' REQFILE='/requirements.txt' +VERBOSITY=1 + +TMP_REQFILE='/tmp/requirements.txt' function usage () { - echo <<"EOF" -Usage: $0 [-a -b -p -A -B -P -r] [--] + 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. @@ -20,6 +23,9 @@ Usage: $0 [-a -b -p -A -B -P -r] [--] -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. @@ -27,8 +33,14 @@ 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:" opt; do +while getopts ":a:b:p:A:B:P:r:qx" opt; do case $opt in a) APK_REQUIREMENTS+=("$OPTARG") ;; b) BUILD_REQUIREMENTS+=("$OPTARG") ;; @@ -36,7 +48,9 @@ while getopts ":a:b:p:A:B:P:r:" opt; do A) APKFILE="$OPTARG" ;; B) BUILDFILE="$OPTARG" ;; P) REQFILE="$OPTARG" ;; - r) REQFILE="$OPTARG" ;; + r) REQFILE="$OPTARG" ;; + q) VERBOSITY=0 ;; + x) set -x ;; \?) echo "Invalid option: -$OPTARG" >&2 usage @@ -60,37 +74,73 @@ 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 \ - - apk add --no-cache $BUILD_PACKAGES "${APK_REQUIREMENTS[@]}" "${BUILD_REQUIREMENTS[@]}" + vlog "Installing all APK requirements..." + apk add $APK_FLAGS $BUILD_PACKAGES "${APK_REQUIREMENTS[@]}" "${BUILD_REQUIREMENTS[@]}" # Install any Pip requirements - if [[ -f "$REQFILE" ]]; then - PIP_REQUIREMENTS+=($( cat "$REQFILE" )) - fi - - if [[ ${#PIP_REQUIREMENTS[@]} -gt 0 ]]; then - pip install --upgrade pip - pip install "${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 $BUILD_PACKAGES "${BUILD_REQUIREMENTS[@]}" + 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 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 dee2c1a..1b362a8 100644 --- a/README.md +++ b/README.md @@ -2,73 +2,84 @@ [![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] +[![Build Status](https://travis-ci.org/jfloff/alpine-python.svg?branch=master)][travis] [hub]: https://hub.docker.com/r/jfloff/alpine-python/ +[travis]: https://travis-ci.org/jfloff/alpine-python A small Python Docker image based on [Alpine Linux](http://alpinelinux.org/). -- [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) -- [License](#license) -- [TODO](#todo) +- [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` ([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.4` ([3.4/Dockerfile](https://github.com/jfloff/alpine-python/blob/master/3.4/Dockerfile))** -* **`3.4-onbuild` ([3.4/onbuild/Dockerfile](https://github.com/jfloff/alpine-python/blob/master/3.4/onbuild/Dockerfile))** -* **`3.4-slim` ([3.4/slim/Dockerfile](https://github.com/jfloff/alpine-python/blob/master/3.4/slim/Dockerfile))** -* **`latest` ([Dockerfile](https://github.com/jfloff/alpine-python/blob/master/3.4/Dockerfile))** -* **`latest-onbuild` ([Dockerfile](https://github.com/jfloff/alpine-python/blob/master/3.4/onbuild/Dockerfile))** -* **`latest-slim` ([Dockerfile](https://github.com/jfloff/alpine-python/blob/master/3.4/slim/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-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 SIZE -jfloff/alpine-python 2.7-slim 52.86 MB -python 2.7-slim 180.8 MB - -jfloff/alpine-python 2.7 234.2 MB -python 2.7 676.2 MB - -jfloff/alpine-python 3.4-slim 110.4 MB -python 3.4-slim 193.9 MB - -jfloff/alpine-python 3.4 280 MB -python 3.4 681.5 MB - -jfloff/alpine-python latest 248.8 MB -python 3.5 685.4 MB - -jfloff/alpine-python latest-slim 79.11 MB -python 3.5-slim 197.8 MB -``` - -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.4` > `python`, `pip2.7` > `pip`, etc.) +* 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 @@ -87,7 +98,7 @@ docker run --rm -ti jfloff/alpine-python bash 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 @@ -198,15 +209,33 @@ CMD ["ajenti-panel"] #### 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 ``` -## License -The code in this repository, unless otherwise noted, is MIT licensed. See the `LICENSE` file in this repository. +#### 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.* -## TODO -At this moment with Alpine APK we are not able to install previous packages versions, i.e., its virtually impossible to provide multiple versions of Python. This is limits us to only provide the latest `python3` package that's available in Alpine APK. +- **[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. -While I was able to provide a 3.4 tag (due to existence of `python3.4` packages), this is a temporary fix. Ideally we would support a solution like the official `python` images does (see example [here](https://github.com/docker-library/python/blob/master/3.4/alpine/Dockerfile)), where the specific Python version is downloaded and compiled. Yet, I don't want to follow the pitfall of copy-pasting that solution, otherwise we end up with almost the same image. ***I'm requesting PRs on this issue, either by optimizing official solution, or other.*** +[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 index aacc582..221a350 100755 --- a/build-all.sh +++ b/build-all.sh @@ -8,14 +8,21 @@ reset="$(tput sgr0)" build_with_status () { tag="$1" - echo -n "Building $tag:" + printf "${b}Building ${tag} ... " docker build . -t $tag &> /dev/null - if [[ $? -eq 0 ]]; then - echo "$bold$green GOOD$reset" - else - echo "$bold$red BAD$reset" + 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 @@ -23,22 +30,12 @@ pushd() { builtin pushd $1 > /dev/null; } popd() { builtin popd > /dev/null; } # Move to where the script is -HERE="$(dirname $(readlink -f $0 || realpath $0))" -cd $HERE +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'); do +for version in $(find -maxdepth 1 -not -name '.*' -type d -printf '%P\n' | sort); do pushd $version - - # Build the flat version build_with_status $repo:$version - - # Build any sub-versions - for subtype in $(find -maxdepth 1 -not -name '.*' -type d -printf '%P\n'); do - pushd $subtype - build_with_status $repo:$version-$subtype - popd - done - popd done diff --git a/latest/Dockerfile b/latest/Dockerfile deleted file mode 100644 index 11771ea..0000000 --- a/latest/Dockerfile +++ /dev/null @@ -1,55 +0,0 @@ -FROM alpine:3.4 - -# Install needed packages. Notes: -# * dumb-init: a proper init system for containers, to reap zombie children -# * musl: standard C library -# * linux-headers: commonly needed, and an unusual package name from Alpine. -# * build-base: used so we include the basic development packages (gcc) -# * git: to ease up clones of repos -# * bash: so we can access /bin/bash -# * ca-certificates: for SSL verification during Pip and easy_install -# * python: the binaries themselves -# * python-dev: are used for gevent e.g. -ENV PACKAGES="\ - dumb-init \ - musl \ - linux-headers \ - build-base \ - bash \ - git \ - ca-certificates \ - python3 \ - python3-dev \ -" - -RUN echo \ - # replacing default repositories with edge ones - && echo "http://dl-cdn.alpinelinux.org/alpine/edge/testing" > /etc/apk/repositories \ - && echo "http://dl-cdn.alpinelinux.org/alpine/edge/community" >> /etc/apk/repositories \ - && echo "http://dl-cdn.alpinelinux.org/alpine/edge/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) \ - - # turn back the clock -- so hacky! - && echo "http://dl-cdn.alpinelinux.org/alpine/v3.4/main/" > /etc/apk/repositories \ - && echo "@edge-testing http://dl-cdn.alpinelinux.org/alpine/edge/testing" >> /etc/apk/repositories \ - && echo "@edge-community http://dl-cdn.alpinelinux.org/alpine/edge/community" >> /etc/apk/repositories \ - && echo "@edge-main http://dl-cdn.alpinelinux.org/alpine/edge/main" >> /etc/apk/repositories \ - - # make some useful symlinks that are expected to exist - && if [[ ! -e /usr/bin/python ]]; then ln -sf /usr/bin/python3 /usr/bin/python; fi \ - && if [[ ! -e /usr/bin/python-config ]]; then ln -sf /usr/bin/python3-config /usr/bin/python-config; fi \ - && if [[ ! -e /usr/bin/idle ]]; then ln -sf /usr/bin/idle3 /usr/bin/idle; fi \ - && if [[ ! -e /usr/bin/pydoc ]]; then ln -sf /usr/bin/pydoc3 /usr/bin/pydoc; fi \ - && if [[ ! -e /usr/bin/easy_install ]]; then ln -sf /usr/bin/easy_install-3* /usr/bin/easy_install; fi \ - - # Install and upgrade Pip - && easy_install pip \ - && pip install --upgrade pip \ - && if [[ ! -e /usr/bin/pip ]]; then ln -sf /usr/bin/pip3 /usr/bin/pip; fi - -# since we will be "always" mounting the volume, we can set this up -ENTRYPOINT ["/usr/bin/dumb-init"] -CMD ["python"] diff --git a/latest/onbuild/Dockerfile b/latest/onbuild/Dockerfile deleted file mode 100644 index a4ada9a..0000000 --- a/latest/onbuild/Dockerfile +++ /dev/null @@ -1,13 +0,0 @@ -FROM jfloff/alpine-python:3.4 - -# Copy in the entrypoint script -- this installs prerequisites on container start. -COPY entrypoint.sh /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 diff --git a/latest/onbuild/entrypoint.sh b/latest/onbuild/entrypoint.sh deleted file mode 100644 index c8c419c..0000000 --- a/latest/onbuild/entrypoint.sh +++ /dev/null @@ -1,103 +0,0 @@ -#!/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' - -function usage () { - echo <<"EOF" -Usage: $0 [-a -b -p -A -B -P -r] [--] - -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. - - -- : Separator for flags and your command - - Whatever you provide after your arguments is run at the end. -EOF - exit 1 -} - -# Get and process arguments -while getopts ":a:b:p:A:B:P:r:" 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" ;; - \?) - 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 - -# Don't do anything if we've already done this. -if [[ ! -f /requirements.installed ]]; then - - # Install any APK requirements - if [[ -f "$APKFILE" ]]; then - APK_REQUIREMENTS+=($( cat "$APKFILE" )) - fi - - if [[ -f "$BUILDFILE" ]]; then - 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 \ - - apk add --no-cache $BUILD_PACKAGES "${APK_REQUIREMENTS[@]}" "${BUILD_REQUIREMENTS[@]}" - - # Install any Pip requirements - if [[ -f "$REQFILE" ]]; then - PIP_REQUIREMENTS+=($( cat "$REQFILE" )) - fi - - if [[ ${#PIP_REQUIREMENTS[@]} -gt 0 ]]; then - pip install --upgrade pip - pip install "${PIP_REQUIREMENTS[@]}" - fi - - # Remove packages that were only required for build. - apk del $BUILD_PACKAGES "${BUILD_REQUIREMENTS[@]}" - - touch /requirements.installed -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/latest/slim/Dockerfile b/latest/slim/Dockerfile deleted file mode 100644 index 52bc1d3..0000000 --- a/latest/slim/Dockerfile +++ /dev/null @@ -1,63 +0,0 @@ -FROM alpine:3.4 - -# 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 -# * python: the binaries themselves -ENV PACKAGES="\ - dumb-init \ - bash \ - ca-certificates \ - python3 \ -" - -RUN echo \ - # replacing default repositories with edge ones - && echo "http://dl-cdn.alpinelinux.org/alpine/edge/testing" > /etc/apk/repositories \ - && echo "http://dl-cdn.alpinelinux.org/alpine/edge/community" >> /etc/apk/repositories \ - && echo "http://dl-cdn.alpinelinux.org/alpine/edge/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) \ - - # turn back the clock -- so hacky! - && echo "http://dl-cdn.alpinelinux.org/alpine/v3.4/main/" > /etc/apk/repositories \ - && echo "@edge-testing http://dl-cdn.alpinelinux.org/alpine/edge/testing" >> /etc/apk/repositories \ - && echo "@edge-community http://dl-cdn.alpinelinux.org/alpine/edge/community" >> /etc/apk/repositories \ - && echo "@edge-main http://dl-cdn.alpinelinux.org/alpine/edge/main" >> /etc/apk/repositories \ - - # make some useful symlinks that are expected to exist - && if [[ ! -e /usr/bin/python ]]; then ln -sf /usr/bin/python3 /usr/bin/python; fi \ - && if [[ ! -e /usr/bin/python-config ]]; then ln -sf /usr/bin/python-config3 /usr/bin/python-config; fi \ - && if [[ ! -e /usr/bin/idle ]]; then ln -sf /usr/bin/idle3 /usr/bin/idle; fi \ - && if [[ ! -e /usr/bin/pydoc ]]; then ln -sf /usr/bin/pydoc3 /usr/bin/pydoc; fi \ - && if [[ ! -e /usr/bin/easy_install ]]; then ln -sf /usr/bin/easy_install-3* /usr/bin/easy_install; fi \ - - # Install and upgrade Pip - && easy_install pip \ - && pip install --upgrade pip \ - && if [[ ! -e /usr/bin/pip ]]; then ln -sf /usr/bin/pip3 /usr/bin/pip; fi \ - && echo - -# Copy in the entrypoint script -- this installs prerequisites on container start. -COPY entrypoint.sh /entrypoint.sh - -# 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. -# * python-dev: are used for gevent e.g. -# * py-pip: provides pip, not needed once the software is built -ENV BUILD_PACKAGES="\ - build-base \ - linux-headers \ - python3-dev \ -" - -# 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/latest/slim/entrypoint.sh b/latest/slim/entrypoint.sh deleted file mode 100644 index c8c419c..0000000 --- a/latest/slim/entrypoint.sh +++ /dev/null @@ -1,103 +0,0 @@ -#!/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' - -function usage () { - echo <<"EOF" -Usage: $0 [-a -b -p -A -B -P -r] [--] - -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. - - -- : Separator for flags and your command - - Whatever you provide after your arguments is run at the end. -EOF - exit 1 -} - -# Get and process arguments -while getopts ":a:b:p:A:B:P:r:" 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" ;; - \?) - 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 - -# Don't do anything if we've already done this. -if [[ ! -f /requirements.installed ]]; then - - # Install any APK requirements - if [[ -f "$APKFILE" ]]; then - APK_REQUIREMENTS+=($( cat "$APKFILE" )) - fi - - if [[ -f "$BUILDFILE" ]]; then - 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 \ - - apk add --no-cache $BUILD_PACKAGES "${APK_REQUIREMENTS[@]}" "${BUILD_REQUIREMENTS[@]}" - - # Install any Pip requirements - if [[ -f "$REQFILE" ]]; then - PIP_REQUIREMENTS+=($( cat "$REQFILE" )) - fi - - if [[ ${#PIP_REQUIREMENTS[@]} -gt 0 ]]; then - pip install --upgrade pip - pip install "${PIP_REQUIREMENTS[@]}" - fi - - # Remove packages that were only required for build. - apk del $BUILD_PACKAGES "${BUILD_REQUIREMENTS[@]}" - - touch /requirements.installed -fi - - -if [[ ! -z "$@" ]]; then - # If the user has given us a command, run it. - $@ -else - # Otherwise, default to running 'python'. - python -fi