Skip to content

Commit

Permalink
feat: introducing distroless-python
Browse files Browse the repository at this point in the history
  • Loading branch information
autumnjolitz committed Aug 3, 2024
0 parents commit 0216959
Show file tree
Hide file tree
Showing 11 changed files with 664 additions and 0 deletions.
215 changes: 215 additions & 0 deletions .github/workflows/main.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,215 @@
name: main

on:
push:
branches: main
pull_request:
branches: main

jobs:
docker:
strategy:
fail-fast: false
matrix:
repository:
- 'ghcr.io'
- 'docker.io'
python:
- '3.12'
- '3.11'
- '3.10'
- '3.9'
- '3.8'
alpine:
- '3.20'
os:
- 'ubuntu-latest'

runs-on: ${{ matrix.os }}
permissions:
packages: write

steps:
-
name: Checkout
uses: actions/checkout@v4
-
name: Set up QEMU
uses: docker/setup-qemu-action@v3
-
name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
-
id: image_env
run: |
. ./env.sh \
'${{ matrix.alpine }}' \
'${{ matrix.python }}' \
'${{ github.repository_owner }}' \
'${{ matrix.repository }}'
docker pull "${SOURCE_IMAGE}"
echo ALPINE_VERSION="${ALPINE_VERSION}" >> "$GITHUB_OUTPUT"
echo PYTHON_VERSION="${PYTHON_VERSION}" >> "$GITHUB_OUTPUT"
echo SOURCE_IMAGE="${SOURCE_IMAGE}" >> "$GITHUB_OUTPUT"
echo IMAGE_TAG="${IMAGE_TAG}" >> "$GITHUB_OUTPUT"
echo REPOSITORY="${REPOSITORY}" >> "$GITHUB_OUTPUT"
echo BASE_IMAGE_DIGEST="$(digest_of "$SOURCE_IMAGE")" >> "$GITHUB_OUTPUT"
-
name: Buildroot
uses: docker/build-push-action@v6
with:
platforms: |
linux/amd64
linux/arm64
context: "."
file: Dockerfile.alpine
target: buildroot
cache-from: |
type=registry,ref=${{ steps.image_env.outputs.IMAGE_TAG }}-buildroot
type=registry,ref=docker.io/python@${{ steps.image_env.outputs.BASE_IMAGE_DIGEST }}
build-args: |
ALPINE_VERSION=${{ steps.image_env.outputs.ALPINE_VERSION }}
BASE_IMAGE_DIGEST=${{ steps.image_env.outputs.BASE_IMAGE_DIGEST }}
PYTHON_VERSION=${{ steps.image_env.outputs.PYTHON_VERSION }}
SOURCE_IMAGE=${{ steps.image_env.outputs.SOURCE_IMAGE }}
BUILD_ROOT=/d
tags: "${{ steps.image_env.outputs.IMAGE_TAG }}-buildroot"
-
name: distroless
uses: docker/build-push-action@v6
with:
platforms: |
linux/amd64
linux/arm64
context: "."
file: Dockerfile.alpine
# target: distroless-python
cache-from: |
type=registry,ref=${{ steps.image_env.outputs.IMAGE_TAG }}
type=registry,ref=${{ steps.image_env.outputs.IMAGE_TAG }}-buildroot
type=registry,ref=docker.io/python@${{ steps.image_env.outputs.BASE_IMAGE_DIGEST }}
build-args: |
ALPINE_VERSION=${{ steps.image_env.outputs.ALPINE_VERSION }}
BASE_IMAGE_DIGEST=${{ steps.image_env.outputs.BASE_IMAGE_DIGEST }}
PYTHON_VERSION=${{ steps.image_env.outputs.PYTHON_VERSION }}
SOURCE_IMAGE=${{ steps.image_env.outputs.SOURCE_IMAGE }}
BUILD_ROOT=/d
tags: "${{ steps.image_env.outputs.IMAGE_TAG }}"
# -
# name: distroless-tests
# uses: docker/build-push-action@v6
# with:
# context: "."
# platforms: |
# linux/amd64
# linux/arm64
# file: Dockerfile.alpine
# target: tests
# cache-from: |
# type=registry,ref=${{ steps.image_env.outputs.IMAGE_TAG }}
# type=registry,ref=${{ steps.image_env.outputs.IMAGE_TAG }}-buildroot
# type=registry,ref=docker.io/python@${{ steps.image_env.outputs.BASE_IMAGE_DIGEST }}
# build-args: |
# ALPINE_VERSION=${{ steps.image_env.outputs.ALPINE_VERSION }}
# BASE_IMAGE_DIGEST=${{ steps.image_env.outputs.BASE_IMAGE_DIGEST }}
# PYTHON_VERSION=${{ steps.image_env.outputs.PYTHON_VERSION }}
# SOURCE_IMAGE=${{ steps.image_env.outputs.SOURCE_IMAGE }}
# BUILD_ROOT=/d
# tags: "${{ steps.image_env.outputs.IMAGE_TAG }}-test"
-
name: export annotations
id: inspect
run: |
echo 'annotations<<EOF' >> "$GITHUB_OUTPUT"
docker inspect '${{ steps.image_env.outputs.IMAGE_TAG }}' | jq -r '.[].Config.Labels| keys[] as $k | "\($k)=\(.[$k])"' >> "$GITHUB_OUTPUT"
echo 'EOF' >> "$GITHUB_OUTPUT"
-
name: Login to GitHub Container Registry
if: ${{ matrix.repository == 'ghcr.io' }}
uses: docker/login-action@v3
with:
registry: 'ghcr.io'
username: ${{ github.repository_owner }}
password: ${{ secrets.GITHUB_TOKEN }}
-
name: Login to DockerHub
if: ${{ matrix.repository == 'docker.io' }}
uses: docker/login-action@v3
with:
registry: 'docker.io'
username: ${{ github.repository_owner }}
password: ${{ secrets.DOCKERHUB_TOKEN }}

-
name: Upload Buildroot
uses: docker/build-push-action@v6
with:
push: true
platforms: |
linux/amd64
linux/arm64
context: "."
file: Dockerfile.alpine
target: buildroot
cache-from: |
type=registry,ref=${{ steps.image_env.outputs.IMAGE_TAG }}-buildroot
type=registry,ref=docker.io/python@${{ steps.image_env.outputs.BASE_IMAGE_DIGEST }}
build-args: |
ALPINE_VERSION=${{ steps.image_env.outputs.ALPINE_VERSION }}
BASE_IMAGE_DIGEST=${{ steps.image_env.outputs.BASE_IMAGE_DIGEST }}
PYTHON_VERSION=${{ steps.image_env.outputs.PYTHON_VERSION }}
SOURCE_IMAGE=${{ steps.image_env.outputs.SOURCE_IMAGE }}
BUILD_ROOT=/d
tags: "${{ steps.image_env.outputs.IMAGE_TAG }}-buildroot"
-
name: Docker meta
uses: docker/metadata-action@v5
with:
images: name/app
-
name: Upload
uses: docker/build-push-action@v6
with:
push: true
context: "."
platforms: |
linux/amd64
linux/arm64
file: Dockerfile.alpine
cache-from: |
type=registry,ref=${{ steps.image_env.outputs.IMAGE_TAG }}
type=registry,ref=${{ steps.image_env.outputs.IMAGE_TAG }}-buildroot
type=registry,ref=docker.io/python@${{ steps.image_env.outputs.BASE_IMAGE_DIGEST }}
build-args: |
ALPINE_VERSION=${{ steps.image_env.outputs.ALPINE_VERSION }}
BASE_IMAGE_DIGEST=${{ steps.image_env.outputs.BASE_IMAGE_DIGEST }}
PYTHON_VERSION=${{ steps.image_env.outputs.PYTHON_VERSION }}
SOURCE_IMAGE=${{ steps.image_env.outputs.SOURCE_IMAGE }}
BUILD_ROOT=/d
tags: "${{ steps.image_env.outputs.IMAGE_TAG }}"
labels: ${{steps.image_env.outputs.IMAGE_LABELS}}
annotations: ${{ steps.inspect.annotations }}

-
name: Convert README.rst to markdown
uses: docker://pandoc/core:2.9
if: ${{ matrix.repository == 'docker.io' }}
with:
args: >-
-s
--wrap=none
-t gfm
-o README.md
README.rst
- name: Update repo description
if: ${{ matrix.repository == 'docker.io' }}
uses: peter-evans/dockerhub-description@e98e4d1628a5f3be2be7c231e50981aee98723ae # v4.0.0
with:
username: ${{ github.repository_owner }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
short-description: ${{ github.event.repository.description }}
16 changes: 16 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
*.egg-info/
.idea/*
*.idx
*.sublime-*
*.pyc
dist/
.cache/
*.dat
.DS_Store
python/
.pytest_cache/
build/*
.pytype/*
instruct/about.py
python*/
.coverage
110 changes: 110 additions & 0 deletions Dockerfile.alpine
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
ARG ALPINE_VERSION=3.20
ARG PYTHON_VERSION=3.12
ARG SOURCE_IMAGE=docker.io/python:${PYTHON_VERSION}-alpine${ALPINE_VERSION}

FROM --platform=$BUILDPLATFORM $SOURCE_IMAGE AS buildroot
ARG PYTHON_VERSION=3.12

ARG BUILD_ROOT='/dest'
ARG CACHE_ROOT='/cache'
ENV BUILD_ROOT=$BUILD_ROOT \
CACHE_ROOT=$CACHE_ROOT \
PYTHON_VERSION=$PYTHON_VERSION \
_sys_apk_add="/usr/bin/env apk add --no-cache" \
_apk_add="/usr/bin/env apk add --root $BUILD_ROOT --no-cache" \
_apk_del="/usr/bin/env apk del --root $BUILD_ROOT --purge" \
_sh="chroot $BUILD_ROOT sh" \
_ln="chroot $BUILD_ROOT ln" \
_chroot="chroot $BUILD_ROOT"

RUN set -eu ; \
# Add to buildroot:
$_sys_apk_add \
# dash is used as a /bin/sh replacement
dash \
# TLS certs
ca-certificates \
# zip is used to take all the bytecode compiled standard
# library and create a pythonXY.zip file that will
# be imported from. This makes the stdlib immutable.
zip \
; \
# remove all ``__pycache__`` directories
find /usr/local/lib/python$PYTHON_VERSION -type d -name '__pycache__' -print0 | xargs -0 rm -rf ; \
# compile all py to an adjacent pyc and remove the original, leaving only the bytecode
python -m compileall -b /usr/local/lib/python$PYTHON_VERSION ; \
find -type f -name '*.py' -exec sh -c "[ -f \"{}c\" ] && echo 'Removing \"{}\"' && rm -f \"{}\"" \; ;\
# make the new root:
mkdir -p \
$CACHE_ROOT/ \
$BUILD_ROOT/etc \
$BUILD_ROOT/bin \
$BUILD_ROOT/usr/local/lib/python$PYTHON_VERSION/site-packages \
$BUILD_ROOT/usr/local/bin \
; \
# use a symlink to hold the apk related confs
ln -s /etc/apk $BUILD_ROOT/etc/apk ; \
$_apk_add --initdb ; \
$_apk_add \
alpine-baselayout-data \
alpine-release \
musl \
libffi \
# needed for update-ca-certificates to work:
run-parts \
# install the runtime dependencies for python
$(apk info -R .python-rundeps | grep -vE ':$') \
; \
cp -p /bin/busybox $BUILD_ROOT/bin/busybox ; \
ls -lt $BUILD_ROOT/bin/busybox ; \
chroot $BUILD_ROOT /bin/busybox ln -sf /bin/busybox /bin/ln ; \
# copy dash into the container so we can use it as the default bin/sh
tar -C / -cpf - $(\
apk info -L \
dash \
ca-certificates \
| grep -vE ':$' \
) | tar -C $BUILD_ROOT -xpf - ; \
$_ln -sf /usr/bin/dash /bin/sh ; \
(\
cd /usr/local/lib && \
tar -C /usr/local/lib -cpf - python$PYTHON_VERSION/lib-dynload libpython* | tar -C $BUILD_ROOT/usr/local/lib -xpf - ; \
tar -C /usr/local/bin -cpf - python* | tar -C $BUILD_ROOT/usr/local/bin -xpf -; \
(cd python$PYTHON_VERSION && zip -9 -X $BUILD_ROOT/usr/local/lib/python$(echo $PYTHON_VERSION | tr -d '.').zip $(\
find . | grep -vE "(__pycache__|^\./(test|site-packages|lib-dynload|idlelib|lib2to3|tkinter|turtle|ensurepip|pydoc))" \
)); \
cp -p python$PYTHON_VERSION/os.pyc $BUILD_ROOT/usr/local/lib/python$PYTHON_VERSION/os.pyc ; \
touch $BUILD_ROOT/usr/local/lib/python$PYTHON_VERSION/ensurepip.py ; \
rm $BUILD_ROOT/usr/local/lib/python$PYTHON_VERSION/lib-dynload/_tkinter* ; \
) && \
$_ln -sf /usr/local/bin/python$PYTHON_VERSION /usr/local/bin/python3 && \
$_ln -sf /usr/local/bin/python$PYTHON_VERSION /usr/local/bin/python && \
tar -C "$BUILD_ROOT" -cpf - etc/apk bin/ln bin/busybox var/cache/apk usr/share/apk | tar -C "$CACHE_ROOT" -xpf - ; \
rm -rf $BUILD_ROOT/bin/ln $BUILD_ROOT/bin/busybox $BUILD_ROOT/etc/apk $BUILD_ROOT/var/cache/apk /usr/share/apk && \
# regenerate the ca-certs!
chroot $BUILD_ROOT update-ca-certificates


FROM scratch AS distroless-python
ARG ALPINE_VERSION=3.20
ARG PYTHON_VERSION=3.12
ARG SOURCE_IMAGE=docker.io/python:${PYTHON_VERSION}-alpine${ALPINE_VERSION}
ARG BASE_IMAGE_DIGEST
ARG BUILD_ROOT='/dest'
ENV BUILD_ROOT=$BUILD_ROOT \
PYTHON_VERSION=$PYTHON_VERSION \
ALPINE_VERSION=$ALPINE_VERSION

COPY --from=buildroot $BUILD_ROOT /
LABEL \
org.opencontainers.image.authors="distroless-python image developers <autumn.jolitz+distroless-python@gmail.com>" \
org.opencontainers.image.source="https://github.com/autumnjolitz/distroless-python" \
org.opencontainers.image.title="Distroless Python ${PYTHON_VERSION} on alpine${ALPINE_VERSION}" \
org.opencontainers.image.description="Distroless, optimized Python images distilled from the DockerHub official Python images. These images only have a python interpreter and the dash shell." \
org.opencontainers.image.base.digest="${BASE_IMAGE_DIGEST}" \
org.opencontainers.image.base.name="$SOURCE_IMAGE" \
distroless.python-version="${PYTHON_VERSION}" \
distroless.alpine-version="${ALPINE_VERSION}" \
distroless.base-image="alpine${ALPINE_VERSION}"

ENTRYPOINT [ "/usr/local/bin/python" ]
24 changes: 24 additions & 0 deletions LICENSE
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
Copyright (c) 2024, Autumn Jolitz
All rights reserved.

Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
* Redistributions of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright
notice, this list of conditions and the following disclaimer in the
documentation and/or other materials provided with the distribution.
* Neither the name of the distroless-python project nor the
names of its contributors may be used to endorse or promote products
derived from this software without specific prior written permission.

THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL DISTROLESS-PYTHON PROJECT CONTRIBUTORS BE LIABLE FOR ANY
DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
Loading

0 comments on commit 0216959

Please sign in to comment.