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
88 changes: 53 additions & 35 deletions .github/workflows/build-images.yml
Original file line number Diff line number Diff line change
Expand Up @@ -26,28 +26,46 @@ jobs:
fail-fast: false
matrix:
include:
- base_image: eclipse-temurin
base_tag: 21-jre-jammy
os: ubuntu
- base_image: eclipse-temurin
base_tag: 25-jre-jammy
os: ubuntu
- base_image: eclipse-temurin
base_tag: 21-jre-alpine
os: alpine
- base_image: eclipse-temurin
base_tag: 25-jre-alpine
os: alpine
- base_image: node
base_tag: 24-alpine
os: alpine
- base_image: node
base_tag: 24-alpine
base_tag_suffix: -runtime
os: alpine-runtime
- base_image: python
base_tag: python3.13-alpine
os: alpine
- image_name: eclipse-temurin
context: images/java/eclipse-temurin
dockerfile: images/java/eclipse-temurin/Dockerfile.java21
tag_prefix: 21-jre-jammy
publish_latest: false
- image_name: eclipse-temurin
context: images/java/eclipse-temurin
dockerfile: images/java/eclipse-temurin/Dockerfile.java25
tag_prefix: 25-jre-jammy
publish_latest: true
- image_name: distroless-java
context: images/java/distroless
dockerfile: images/java/distroless/Dockerfile.java21
tag_prefix: 21-jre
publish_latest: false
- image_name: distroless-java
context: images/java/distroless
dockerfile: images/java/distroless/Dockerfile.java25
tag_prefix: 25-jre
publish_latest: true
- image_name: node
context: images/node/alpine
dockerfile: images/node/alpine/Dockerfile
tag_prefix: 24-alpine
publish_latest: false
- image_name: node
context: images/node/alpine-runtime
dockerfile: images/node/alpine-runtime/Dockerfile
tag_prefix: 24-alpine-runtime
publish_latest: true
- image_name: distroless-node
context: images/node/distroless
dockerfile: images/node/distroless/Dockerfile
tag_prefix: 24
publish_latest: true
- image_name: python
context: images/python/alpine
dockerfile: images/python/alpine/Dockerfile
tag_prefix: python3.13-alpine
publish_latest: true
permissions:
contents: read
packages: write
Expand All @@ -67,17 +85,18 @@ jobs:
id: meta
uses: docker/metadata-action@80c7e94dd9b9319bd5eb7a0e0fe9291e23a2a2e9 # v6
with:
images: ${{ env.REGISTRY }}/${{ github.repository_owner }}/hmpps-${{ matrix.base_image }}
images: ${{ env.REGISTRY }}/${{ github.repository_owner }}/hmpps-${{ matrix.image_name }}
flavor: |
latest=true
latest=false
tags: |
# Prefix dynamic tags with the variant to prevent overwrites between matrix entries
type=schedule,pattern={{date 'YYYYMMDD'}},prefix=${{ matrix.base_tag }}${{ matrix.base_tag_suffix }}-
type=ref,event=branch,prefix=${{ matrix.base_tag }}${{ matrix.base_tag_suffix }}-
type=ref,event=tag,prefix=${{ matrix.base_tag }}${{ matrix.base_tag_suffix }}-
type=ref,event=pr,prefix=${{ matrix.base_tag }}${{ matrix.base_tag_suffix }}-
type=sha,prefix=${{ matrix.base_tag }}${{ matrix.base_tag_suffix }}-
type=raw,value=${{ matrix.base_tag }}${{ matrix.base_tag_suffix }}
type=schedule,pattern={{date 'YYYYMMDD'}},prefix=${{ matrix.tag_prefix }}-
type=ref,event=branch,prefix=${{ matrix.tag_prefix }}-
type=ref,event=tag,prefix=${{ matrix.tag_prefix }}-
type=ref,event=pr,prefix=${{ matrix.tag_prefix }}-
type=sha,prefix=${{ matrix.tag_prefix }}-
type=raw,value=${{ matrix.tag_prefix }}
type=raw,value=latest,enable=${{ matrix.publish_latest }}

- name: Log in to GitHub Container Registry
uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4
Expand All @@ -89,14 +108,13 @@ jobs:
- name: Build and push container image
uses: docker/build-push-action@f9f3042f7e2789586610d6e8b85c8f03e5195baf # v7
with:
context: ./images/${{ matrix.base_image }}/${{ matrix.os }}
context: ./${{ matrix.context }}
file: ./${{ matrix.dockerfile }}
platforms: linux/amd64,linux/arm64
pull: true
push: ${{ github.ref == 'refs/heads/main' || github.event_name == 'schedule' }}
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
build-args: |
BASE_TAG=${{ matrix.base_tag }}
cache-from: type=gha
cache-to: type=gha,mode=max
no-cache-filters: security-upgrades
Expand All @@ -115,13 +133,13 @@ jobs:
"attachments": [
{
"color": "danger",
"fallback": "${{ matrix.base_image }} Image Build Failed - ${{ github.workflow }} in ${{ github.repository }}",
"fallback": "${{ matrix.image_name }} Image Build Failed - ${{ github.workflow }} in ${{ github.repository }}",
"blocks": [
{
"type": "header",
"text": {
"type": "plain_text",
"text": "🔨 ${{ matrix.base_image }} Image Build Failed"
"text": "🔨 ${{ matrix.image_name }} Image Build Failed"
}
},
{
Expand Down
44 changes: 28 additions & 16 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,25 +1,29 @@
# HMPPS Base Container Images

Lean, security-focused base images for Java (Temurin JRE) and Node.js applications used across HMPPS.
Lean, security-focused base images for Java (Temurin and distroless), Node.js (Alpine and distroless), and Python applications used across HMPPS.

## Repositories

| Image family | Repository | Example pull |
|--------------|------------|--------------|
| Java (Temurin JRE) | `ghcr.io/ministryofjustice/hmpps-eclipse-temurin` | `docker pull ghcr.io/ministryofjustice/hmpps-eclipse-temurin:21-jre-jammy` |
| Node.js | `ghcr.io/ministryofjustice/hmpps-node` | `docker pull ghcr.io/ministryofjustice/hmpps-node:24-alpine` |
| Java (Distroless) | `ghcr.io/ministryofjustice/hmpps-distroless-java` | `docker pull ghcr.io/ministryofjustice/hmpps-distroless-java:25-jre` |
| Node.js (Alpine) | `ghcr.io/ministryofjustice/hmpps-node` | `docker pull ghcr.io/ministryofjustice/hmpps-node:24-alpine` |
| Node.js (Distroless) | `ghcr.io/ministryofjustice/hmpps-distroless-node` | `docker pull ghcr.io/ministryofjustice/hmpps-distroless-node:24` |
| Python | `ghcr.io/ministryofjustice/hmpps-python` | `docker pull ghcr.io/ministryofjustice/hmpps-python:python3.13-alpine` |

## Variants

Java:
- `21-jre-jammy`
- `25-jre-jammy`
- `21-jre-alpine`
- `25-jre-alpine`
- `21-jre` (distroless)
- `25-jre` (distroless)

Node:
- `24-alpine`
- `24-alpine-runtime` — same as `24-alpine` but with package managers (npm, yarn, corepack) removed
- `24` (distroless)

Python:
- `python3.13-alpine`
Expand All @@ -38,38 +42,44 @@ Each variant always has its raw variant tag (e.g. `21-jre-jammy`). Additional dy
| Git SHA | `21-jre-jammy-sha-<shortsha>` | All builds |
| Raw variant | `21-jre-jammy` | All builds |

The `:latest` tag is applied per repository (currently points to the most recently built variant for that family). Consumers should prefer explicit variant tags.
The `:latest` tag is selectively enabled per matrix entry in CI. Consumers should prefer explicit variant tags.

## CI/CD Overview

- Weekday scheduled build: 05:00 UTC (creates date tags)
- Push to `main`, PR, and manual dispatch builds are enabled
- Multi-platform build/push via Buildx
- Trivy scan: table output for branch builds; SARIF uploaded on scheduled (or designated) runs
- Slack notification on workflow failure

## Upgrading Base Versions (Workflow-Only)

To upgrade image versions, update the matrix in `.github/workflows/build-images.yml` instead of editing Dockerfiles.

The workflow matrix controls:
- `base_image`: image family/repository path (for example `eclipse-temurin`, `node`, `python`)
- `base_tag`: upstream tag to build from (for example `25-jre-jammy`, `24-alpine`, `python3.13-alpine`)
- `image_name`: output repository suffix (for example `eclipse-temurin`, `distroless-node`, `python`)
- `context`: Docker build context directory
- `dockerfile`: Dockerfile path
- `tag_prefix`: raw variant tag and prefix for dynamic tags
- `publish_latest`: whether to publish `latest` for that matrix entry

Example matrix entry:

```yaml
- base_image: eclipse-temurin
base_tag: 25-jre-jammy
os: ubuntu
- image_name: eclipse-temurin
context: images/java/eclipse-temurin
dockerfile: images/java/eclipse-temurin/Dockerfile.java25
tag_prefix: 25-jre-jammy
publish_latest: true
```

How updates work:
- Existing tag refresh: keep the same `base_tag`; the scheduled weekday rebuild (`cron`) runs with `pull: true`, so the latest upstream image for that tag is pulled automatically.
- New tag adoption: change `base_tag` (or add another matrix entry) to the new upstream tag you want to publish.
- New variant publish: add a new matrix row with the required `base_image`, `base_tag`, and `os` so it is built and pushed as its own variant.
- Existing tag refresh: keep the same `tag_prefix`; scheduled rebuilds (`cron`) run with `pull: true`, so the latest upstream base layers are pulled automatically.
- New tag adoption: update `dockerfile` (and/or its `FROM` image tag), then keep or change `tag_prefix` to the published tag you want.
- New variant publish: add a new matrix row with `image_name`, `context`, `dockerfile`, `tag_prefix`, and `publish_latest`.

When moving to a new tag, verify:
- The upstream tag exists and is supported for your target architecture (`linux/amd64`, `linux/arm64`).
- The selected `os` matches an existing Dockerfile location under `images/<base_image>/<os>`.
- The upstream tag in the Dockerfile `FROM` exists and is supported for your target architecture (`linux/amd64`, `linux/arm64`).
- The selected `context`/`dockerfile` paths match repository layout.
- Consumers pin to the explicit variant tag (for example `25-jre-jammy`) rather than relying on `latest`.

## Security Upgrades
Expand All @@ -85,3 +95,5 @@ RUN apk upgrade --no-cache # or apt-get upgrade for Ubuntu
```

The CI workflow uses BuildKit's `--no-cache-filter=security-upgrades` to skip the cache for this stage, ensuring `apk upgrade` / `apt-get upgrade` always fetches the latest patches — even when other layers are cached.

Distroless images use prep/runtime stages and do not include a `security-upgrades` stage.
61 changes: 26 additions & 35 deletions images/eclipse-temurin/README.md → images/java/README.md
Original file line number Diff line number Diff line change
@@ -1,20 +1,31 @@
# HMPPS Java Base Image (Eclipse Temurin)
# HMPPS Java Base Image

Standardized base image for JVM applications in HMPPS. Provides a consistent, lean, non‑root runtime across Ubuntu (Jammy) and Alpine variants of Eclipse Temurin JRE.
Standardized base image for JVM applications in HMPPS. Provides a consistent, lean, non‑root runtime across Ubuntu (Jammy) and distroless variants.

## Supported Variants

| Variant Tag | OS | Arch (multi-platform) | Notes |
|-------------------|--------|-----------------------|-------|
| 21-jre-jammy | Ubuntu | amd64, arm64 | LTS line (Java 21) |
| 25-jre-jammy | Ubuntu | amd64, arm64 | Latest (preview until GA) |
| 21-jre-alpine | Alpine | amd64, arm64 | Smaller footprint |
| 25-jre-alpine | Alpine | amd64, arm64 | Smaller footprint |
| Image | Variant Tag | OS | Arch (multi-platform) | Notes |
|-------|-------------|----|-----------------------|-------|
| hmpps-eclipse-temurin | 21-jre-jammy | Ubuntu | amd64, arm64 | LTS line (Java 21) |
| hmpps-eclipse-temurin | 25-jre-jammy | Ubuntu | amd64, arm64 | Current default line |
| hmpps-distroless-java | 21-jre | Distroless | amd64, arm64 | Minimal runtime footprint (no shell/package manager) |
| hmpps-distroless-java | 25-jre | Distroless | amd64, arm64 | Minimal runtime footprint (no shell/package manager) |

## Distroless Variant

The distroless variants use Google's minimal distroless base images instead of full OS distributions, reducing attack surface and image size.

**Notes:**

- No shell/package manager in runtime image (debugging harder).
- Two-stage build: prepare assets in Debian (full OS), run on distroless.
- Requires explicit binary/library copies; fewer implicit dependencies.

Images are published to:

```
ghcr.io/ministryofjustice/hmpps-eclipse-temurin
ghcr.io/ministryofjustice/hmpps-distroless-java
```

Tags applied (via GitHub Actions metadata):
Expand All @@ -24,28 +35,13 @@ Tags applied (via GitHub Actions metadata):
| Date schedule | 20241120 | Daily rebuild identifier |
| Branch / PR | initial-commit | Trace source ref |
| SHA | sha-<short> | Exact source immutability |
| latest | latest | Convenience (points to most recent build of a variant) |
| Raw variant | 21-jre-alpine | Upstream base variant clarity |


## Build Args
| latest | latest | Published only for variants explicitly enabled in CI |
| Raw variant | 25-jre-jammy | Stable, explicit variant tag |

Dockerfile exposes:
Current latest mappings in CI:

```
ARG BASE_IMAGE=eclipse-temurin
ARG BASE_TAG=<variant>
```

These are passed by CI matrix; you can override locally when rebuilding a specific variant:

```bash
docker build \
--build-arg BASE_TAG=21-jre-alpine \
--build-arg BASE_IMAGE=eclipse-temurin \
-t hmpps-eclipse-temurin:21-jre-alpine \
images/eclipse-temurin/alpine
```
- `ghcr.io/ministryofjustice/hmpps-eclipse-temurin:latest` -> `25-jre-jammy`
- `ghcr.io/ministryofjustice/hmpps-distroless-java:latest` -> `25-jre`

## JVM Defaults Explained

Expand Down Expand Up @@ -88,11 +84,6 @@ USER 2000
CMD ["java", "-jar", "app.jar"]
```

## Security & Scanning

CI builds multi-arch images daily (weekdays 05:00 UTC). Trivy scans:
## CI Build Behavior

| Branch | Output | Blocking |
|--------|--------|----------|
| Default (non-main) | Table (critical/high) | Non-blocking |
| main / scheduled | SARIF uploaded to Security tab | Non-blocking exit code |
CI builds multi-arch images (linux/amd64, linux/arm64) on push, PR, manual dispatch, and weekdays at 05:00 UTC.
36 changes: 36 additions & 0 deletions images/java/distroless/Dockerfile.java21
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
# HMPPS Java Base Image (Distroless Java 21)

FROM debian:13 AS prep

ARG APP_UID=2000
ARG APP_GID=2000

# Create app user/group and RDS cert path in a full userland stage.
RUN groupadd --gid ${APP_GID} --system appgroup && \
useradd --uid ${APP_UID} --system --gid ${APP_GID} --shell /usr/sbin/nologin appuser
RUN install -d -o appuser -g appgroup /home/appuser/.postgresql
ADD https://truststore.pki.rds.amazonaws.com/global/global-bundle.pem /home/appuser/.postgresql/root.crt
RUN chown appuser:appgroup /home/appuser/.postgresql/root.crt && \
chmod 0644 /home/appuser/.postgresql/root.crt

FROM gcr.io/distroless/java21-debian13:nonroot AS runtime

ARG APP_UID=2000
ARG APP_GID=2000

LABEL org.opencontainers.image.title="HMPPS Java Base Image (Distroless Java 21)"
LABEL org.opencontainers.image.description="Distroless runtime image for Java 21 applications in HMPPS"
LABEL org.opencontainers.image.source="https://github.com/ministryofjustice/hmpps-base-container-images"
LABEL org.opencontainers.image.vendor="Ministry of Justice"
LABEL hmpps.java.base_variant="distroless"
LABEL org.opencontainers.image.base.name="gcr.io/distroless/java21-debian13:nonroot"

WORKDIR /app

# Sensible JVM defaults for containers
ENV TZ="Europe/London"
ENV JAVA_TOOL_OPTIONS="-XX:+ExitOnOutOfMemoryError -XX:MaxRAMPercentage=50.0"

COPY --from=prep --chown=${APP_UID}:${APP_GID} /home/appuser/.postgresql/root.crt /home/appuser/.postgresql/root.crt

USER ${APP_UID}:${APP_GID}
36 changes: 36 additions & 0 deletions images/java/distroless/Dockerfile.java25
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
# HMPPS Java Base Image (Distroless Java 25)

FROM debian:13 AS prep

ARG APP_UID=2000
ARG APP_GID=2000

# Create app user/group and RDS cert path in a full userland stage.
RUN groupadd --gid ${APP_GID} --system appgroup && \
useradd --uid ${APP_UID} --system --gid ${APP_GID} --shell /usr/sbin/nologin appuser
RUN install -d -o appuser -g appgroup /home/appuser/.postgresql
ADD https://truststore.pki.rds.amazonaws.com/global/global-bundle.pem /home/appuser/.postgresql/root.crt
RUN chown appuser:appgroup /home/appuser/.postgresql/root.crt && \
chmod 0644 /home/appuser/.postgresql/root.crt

FROM gcr.io/distroless/java25-debian13:nonroot AS runtime

ARG APP_UID=2000
ARG APP_GID=2000

LABEL org.opencontainers.image.title="HMPPS Java Base Image (Distroless Java 25)"
LABEL org.opencontainers.image.description="Distroless runtime image for Java 25 applications in HMPPS"
LABEL org.opencontainers.image.source="https://github.com/ministryofjustice/hmpps-base-container-images"
LABEL org.opencontainers.image.vendor="Ministry of Justice"
LABEL hmpps.java.base_variant="distroless"
LABEL org.opencontainers.image.base.name="gcr.io/distroless/java25-debian13:nonroot"

WORKDIR /app

# Sensible JVM defaults for containers
ENV TZ="Europe/London"
ENV JAVA_TOOL_OPTIONS="-XX:+ExitOnOutOfMemoryError -XX:MaxRAMPercentage=50.0"

COPY --from=prep --chown=${APP_UID}:${APP_GID} /home/appuser/.postgresql/root.crt /home/appuser/.postgresql/root.crt

USER ${APP_UID}:${APP_GID}
Loading
Loading