Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 33 additions & 0 deletions .github/workflows/publish.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -49,9 +49,42 @@ jobs:
IMAGE="${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}"
echo "IMAGE_LOWERCASE=$(echo $IMAGE | tr '[:upper:]' '[:lower:]')" >> $GITHUB_OUTPUT

test:
strategy:
fail-fast: false
# This job runs on two different runners: one for x86_64 and one for ARM.
# The ARM runner is used to test the image on an ARM architecture.
# The x86_64 runner is used to test the image on an x86_64 architecture.
matrix:
include:
- platform: linux/amd64
runner: ubuntu-24.04
- platform: linux/arm64
runner: ubuntu-24.04-arm
runs-on: ${{ matrix.runner }}
steps:
- name: Checkout repository
uses: actions/checkout@v4

- name: Set up Docker Buildx
uses: docker/setup-buildx-action@b5ca514318bd6ebac0fb2aedd5d36ec1b5c232a2 # v3.10.0

- name: Install container-structure-test
run: |
if [[ "${{ matrix.platform }}" == *"arm64"* ]]; then
curl -LO https://github.com/GoogleContainerTools/container-structure-test/releases/latest/download/container-structure-test-linux-arm64 && chmod +x container-structure-test-linux-arm64 && sudo mv container-structure-test-linux-arm64 /usr/local/bin/container-structure-test
else
curl -LO https://github.com/GoogleContainerTools/container-structure-test/releases/latest/download/container-structure-test-linux-amd64 && chmod +x container-structure-test-linux-amd64 && sudo mv container-structure-test-linux-amd64 /usr/local/bin/container-structure-test
fi

- name: Run tests
run: |
./test.sh ${{ matrix.platform }}

build_and_push_image:
needs:
- prep
- test
env:
IMAGE_LOWERCASE: ${{ needs.prep.outputs.IMAGE_LOWERCASE }}
strategy:
Expand Down
12 changes: 12 additions & 0 deletions .vscode/extensions.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
{
"recommendations": [
"timonwong.shellcheck",
"ms-azuretools.vscode-docker",
"exiasr.hadolint",
"foxundermoon.shell-format",
"davidanson.vscode-markdownlint",
"github.vscode-github-actions",
"redhat.vscode-yaml",
"docker.docker"
]
}
13 changes: 13 additions & 0 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
{
"[shellscript]": {
"editor.defaultFormatter": "foxundermoon.shell-format"
},
"[dockerfile]": {
"editor.defaultFormatter": "ms-azuretools.vscode-docker"
},
"[markdown]": {
"editor.defaultFormatter": "DavidAnson.vscode-markdownlint"
},
"editor.formatOnSave": true,
"yaml.format.enable": true,
}
5 changes: 5 additions & 0 deletions Brewfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
tap "homebrew/bundle"
brew "container-structure-test"
brew "hadolint"
brew "shellcheck"
brew "shfmt"
78 changes: 42 additions & 36 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -1,67 +1,73 @@
FROM python:3.12-bookworm
FROM ubuntu:24.04 AS base

ENV PYTHONDONTWRITEBYTECODE=1
ENV PYTHONUNBUFFERED=1
ENV JAVA_HOME=/usr/lib/jvm/java-openjdk

SHELL ["/bin/bash", "-o", "pipefail", "-c"]

# https://docs.docker.com/build/cache/optimize/#use-cache-mounts
RUN --mount=type=cache,target=/var/cache/apt \
--mount=type=cache,target=/var/lib/apt \
--mount=type=cache,target=/root/.cache/pip \
RUN --mount=type=cache,target=/var/cache/apt,sharing=locked \
--mount=type=cache,target=/var/lib/apt,sharing=locked \
apt-get update \
&& apt-get upgrade -y \
&& apt-get install -y --no-install-recommends --no-install-suggests \
# Required for pyre vscode extension
watchman \
# Required for sonarqube vscode extension
openjdk-17-jre-headless \
nodejs \
# Required for shellcheck vscode extension
shellcheck \
# Required for general purpose compilation
gcc \
# General purpose tools
curl \
git \
jq \
zsh \
&& pip install --no-cache-dir -U pip setuptools wheel \
&& pip install --no-cache-dir uv \
# Required for pyre vscode extension
watchman \
# Required for sonarqube vscode extension
openjdk-17-jre-headless \
nodejs \
# Required for shellcheck vscode extension
shellcheck \
# Required for general purpose compilation
gcc \
# General purpose tools
curl \
git \
jq \
zsh \
# Install uv:
&& curl -LsSf https://astral.sh/uv/install.sh | env UV_INSTALL_DIR="/usr/local/bin" sh \
# Install Pulumi:
&& curl -fsSL https://get.pulumi.com | sh \
&& mv /root/.pulumi/bin/pulumi /usr/local/bin \
# Install reviewdog:
&& curl -sfL https://raw.githubusercontent.com/reviewdog/reviewdog/master/install.sh \
| sh -s -- -b /usr/local/bin \
| sh -s -- -b /usr/local/bin \
# Make sure java runtime is found for sonarqube:
&& ln -s "$(dirname "$(dirname "$(readlink -f "$(which java)")")")" "$JAVA_HOME" \
# Install other tools:
&& export ACTIONLINT_VERSION=$(curl -s https://api.github.com/repos/rhysd/actionlint/releases/latest | jq -r '.tag_name' | sed "s/v//") \
&& export HADOLINT_VERSION=$(curl -s https://api.github.com/repos/hadolint/hadolint/releases/latest | jq -r '.tag_name') \
&& export SHFMT_VERSION=$(curl -s https://api.github.com/repos/mvdan/sh/releases/latest | jq -r '.tag_name') \
&& if [ "$(uname -m)" = "aarch64" ]; then \
curl -o /usr/local/bin/snyk -L https://static.snyk.io/cli/latest/snyk-linux-arm64 \
&& curl -o /usr/local/bin/hadolint -L https://github.com/hadolint/hadolint/releases/download/${HADOLINT_VERSION}/hadolint-Linux-arm64 \
&& curl -o /usr/local/bin/shfmt https://github.com/patrickvane/shfmt/releases/download/master/shfmt_linux_arm \
&& curl -sL "https://github.com/rhysd/actionlint/releases/download/v${ACTIONLINT_VERSION}/actionlint_${ACTIONLINT_VERSION}_linux_arm64.tar.gz" | tar -xzf - -C /usr/local/bin actionlint ; \
curl -o /usr/local/bin/snyk -L https://static.snyk.io/cli/latest/snyk-linux-arm64 \
&& curl -o /usr/local/bin/hadolint -L https://github.com/hadolint/hadolint/releases/download/${HADOLINT_VERSION}/hadolint-Linux-arm64 \
&& curl -o /usr/local/bin/shfmt -L https://github.com/mvdan/sh/releases/download/${SHFMT_VERSION}/shfmt_${SHFMT_VERSION}_linux_arm64 \
&& curl -sL "https://github.com/rhysd/actionlint/releases/download/v${ACTIONLINT_VERSION}/actionlint_${ACTIONLINT_VERSION}_linux_arm64.tar.gz" | tar -xzf - -C /usr/local/bin actionlint ; \
else \
curl -o /usr/local/bin/snyk -L https://static.snyk.io/cli/latest/snyk-linux \
&& curl -o /usr/local/bin/hadolint -L https://github.com/hadolint/hadolint/releases/download/${HADOLINT_VERSION}/hadolint-Linux-x86_64 \
&& curl -o /usr/local/bin/shfmt https://github.com/patrickvane/shfmt/releases/download/master/shfmt_linux_amd64 \
&& curl -sL "https://github.com/rhysd/actionlint/releases/download/v${ACTIONLINT_VERSION}/actionlint_${ACTIONLINT_VERSION}_linux_amd64.tar.gz" | tar -xzf - -C /usr/local/bin actionlint ; \
curl -o /usr/local/bin/snyk -L https://static.snyk.io/cli/latest/snyk-linux \
&& curl -o /usr/local/bin/hadolint -L https://github.com/hadolint/hadolint/releases/download/${HADOLINT_VERSION}/hadolint-Linux-x86_64 \
&& curl -o /usr/local/bin/shfmt -L https://github.com/mvdan/sh/releases/download/${SHFMT_VERSION}/shfmt_${SHFMT_VERSION}_linux_amd64 \
&& curl -sL "https://github.com/rhysd/actionlint/releases/download/v${ACTIONLINT_VERSION}/actionlint_${ACTIONLINT_VERSION}_linux_amd64.tar.gz" | tar -xzf - -C /usr/local/bin actionlint ; \
fi \
&& chmod +x /usr/local/bin/snyk \
&& chmod +x /usr/local/bin/hadolint \
&& chmod +x /usr/local/bin/shfmt \
&& chmod +x /usr/local/bin/actionlint

WORKDIR /app
ENV PATH="/opt/venv/bin:$PATH"

# Copy from the cache instead of linking since it's a mounted volume
ENV UV_LINK_MODE=copy
ENV UV_SYSTEM_PYTHON=true
ENV UV_BREAK_SYSTEM_PACKAGES=true
ENV UV_PROJECT_ENVIRONMENT=/usr/local

ENV UV_PROJECT_ENVIRONMENT=/opt/venv
ENV UV_PYTHON_INSTALL_DIR=/opt/pythons

# Install the project's dependencies using the lockfile and settings
ONBUILD RUN --mount=type=cache,target=/root/.cache/uv \
--mount=type=bind,source=uv.lock,target=uv.lock \
--mount=type=bind,source=pyproject.toml,target=pyproject.toml \
uv sync --frozen --no-install-project
--mount=type=bind,source=uv.lock,target=uv.lock,readonly \
--mount=type=bind,source=pyproject.toml,target=pyproject.toml,readonly \
--mount=type=bind,source=.python-version,target=.python-version,readonly \
uv venv \
&& uv sync --frozen --no-install-project
55 changes: 46 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,46 +1,83 @@
# Python base image for development purpose

Specially created Docker image for Python to work as a devcontainer for development purposes.
Specially created Docker image for Python to work as a devcontainer for development purposes.

NOTE: This image is NOT meant to be used as a production image.

## Contains

It contains:

- `python` - Python sourced from official Python image on Docker Hub. We are using now Python version 3.12 as it is now the latest Python version supported by the type checkers we use.
- `uv` - for Python package management
- `python` - Python version that is defined in `.python-version` file in the child image.

It contains the neccesary dependencies for running various linters and type checkers:
It contains the necessary dependencies for running various linters and type checkers:

- `watchman` - for running Pyre Python type checker
- `watchman` - for running Pyre Python type checker
- `shfmt` - for shell script formatting
- `shellcheck` - for shell script linting
- `reviewdog` - for code review
- `hadolint` - for linting Dockerfile
- `actionlint` - static checker for GitHub Actions workflow files

Other tools:

- `pulumi` - Pulumi CLI for infrastructure as code

## Usage

We host the image on [Github packages](https://github.com/NextGenContributions/python-dev-image/pkgs/container/python-dev-image).

You can use it the like this:

### As a standalone image

Command line:

```shell
docker pull ghcr.io/nextgencontributions/python-dev-image
docker run --rm ghcr.io/nextgencontributions/python-dev-image
```

### As a base image

In your project's `Dockerfile`:

```Dockerfile
FROM ghcr.io/nextgencontributions/python-dev-image
FROM ghcr.io/nextgencontributions/python-dev-image # AS scratch

# Do your own customizations here...
```

NOTE: When using this as a base image, you should have the following files available in your build context:

- `pyproject.toml`
- `uv.lock`
- `.python-version` - this will be used to install the Python version in the container.

If you don't have these files, you can create them by running the following commands:

```shell
uv init
uv lock
uv python pin 3.12 # or replace with any other Python version you want
```

Or in a single command in your project's root directory:

```shell
docker run --rm -v $(pwd):/app ghcr.io/nextgencontributions/python-dev-image \
sh -c "cd /app && uv init && uv lock && uv python pin 3.12"
```

### As a devcontainer

With VSCode in `.devcontainer/devcontainer.json`:

```jsonc
// For format details, see https://aka.ms/devcontainer.json.
{
"name": "Python 3",
"image": "ghcr.io/nextgencontributions/python-dev-image"
// Do your own customizations here...
"name": "Python 3",
"image": "ghcr.io/nextgencontributions/python-dev-image"
// Do your own customizations here...
}
```
51 changes: 51 additions & 0 deletions test.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
#!/usr/bin/env bash
#
# This script builds and tests a Docker image for multiple platforms (amd64 and arm64).
# It uses Docker Buildx to build the image and container-structure-test to run tests on the built image.
#
# Usage: ./test.sh [platform]
#
# If no platform is specified, it defaults to "linux/amd64 linux/arm64".

set -euxo pipefail

BASE_IMAGE="python-dev-image"
TEST_IMAGE="python-dev-test-image"

platforms="${1:-linux/amd64 linux/arm64}"

# Build and test function that takes platform as parameter
build_and_test() {
local platform=$1
local tag=$2

BASE_PLATFORM_IMAGE="$BASE_IMAGE-$platform:$tag"
TEST_PLATFORM_IMAGE="$TEST_IMAGE-$platform:$tag"

echo "Building and testing for platform: $platform with tag: $tag"

# Build the base image
docker build --load --platform "$platform" -t "$BASE_PLATFORM_IMAGE" -f Dockerfile .

# Build the test image (use classic docker build for local-only workaround)
docker build --load --platform "$platform" -t "$TEST_PLATFORM_IMAGE" --build-arg BASE_IMAGE="$BASE_PLATFORM_IMAGE" --build-arg PLATFORM="$platform" -f tests/test-data/build-context/Dockerfile tests/test-data/build-context

docker run --platform "$platform" --rm "$TEST_PLATFORM_IMAGE" uname -m

if [ "$platform" == "linux/amd64" ]; then
container-structure-test test --platform "$platform" --image "$TEST_PLATFORM_IMAGE" --config tests/amd64.yaml
else
container-structure-test test --platform "$platform" --image "$TEST_PLATFORM_IMAGE" --config tests/arm64.yaml
fi

# Run the tests
container-structure-test test --platform "$platform" --image "$TEST_PLATFORM_IMAGE" --config tests/specs.yaml

# Clean up
docker rmi $TEST_IMAGE:"$tag" || true
}

for platform in $platforms; do
# Build and test for each platform
build_and_test "$platform" "latest"
done
7 changes: 7 additions & 0 deletions tests/amd64.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
schemaVersion: "2.0.0"

commandTests:
- name: "is correct architecture"
command: "uname"
args: ["-m"]
expectedOutput: ["x86_64"]
7 changes: 7 additions & 0 deletions tests/arm64.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
schemaVersion: "2.0.0"

commandTests:
- name: "is correct architecture"
command: "uname"
args: ["-m"]
expectedOutput: ["aarch64"]
Loading