diff --git a/.github/workflows/build-images.yml b/.github/workflows/build-images.yml index 62dc124..a511add 100644 --- a/.github/workflows/build-images.yml +++ b/.github/workflows/build-images.yml @@ -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 @@ -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 @@ -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 @@ -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" } }, { diff --git a/README.md b/README.md index 70612da..904174e 100644 --- a/README.md +++ b/README.md @@ -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` @@ -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-` | 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//`. +- 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 @@ -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. diff --git a/images/eclipse-temurin/README.md b/images/java/README.md similarity index 56% rename from images/eclipse-temurin/README.md rename to images/java/README.md index 01e4e9a..cadabec 100644 --- a/images/eclipse-temurin/README.md +++ b/images/java/README.md @@ -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): @@ -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- | 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= -``` - -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 @@ -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. diff --git a/images/java/distroless/Dockerfile.java21 b/images/java/distroless/Dockerfile.java21 new file mode 100644 index 0000000..80bbc10 --- /dev/null +++ b/images/java/distroless/Dockerfile.java21 @@ -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} diff --git a/images/java/distroless/Dockerfile.java25 b/images/java/distroless/Dockerfile.java25 new file mode 100644 index 0000000..c7ed434 --- /dev/null +++ b/images/java/distroless/Dockerfile.java25 @@ -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} diff --git a/images/eclipse-temurin/ubuntu/Dockerfile b/images/java/eclipse-temurin/Dockerfile.java21 similarity index 74% rename from images/eclipse-temurin/ubuntu/Dockerfile rename to images/java/eclipse-temurin/Dockerfile.java21 index 1b713a7..bfcf9c3 100644 --- a/images/eclipse-temurin/ubuntu/Dockerfile +++ b/images/java/eclipse-temurin/Dockerfile.java21 @@ -1,21 +1,14 @@ -# HMPPS Java Base Image -ARG BASE_IMAGE=eclipse-temurin -ARG BASE_TAG=21-jre-jammy -FROM ${BASE_IMAGE}:${BASE_TAG} AS base - -# Re-declare build args within this stage for later use in labels -ARG BASE_IMAGE -ARG BASE_TAG +# HMPPS Java Base Image (Eclipse Temurin Java 21) +FROM eclipse-temurin:21-jre-jammy AS base # Metadata -LABEL org.opencontainers.image.title="HMPPS Java Base Image" -LABEL org.opencontainers.image.description="Base container image for Java applications in HMPPS" +LABEL org.opencontainers.image.title="HMPPS Java Base Image (Java 21)" +LABEL org.opencontainers.image.description="Base container 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="${BASE_TAG}" -LABEL hmpps.java.base_image="${BASE_IMAGE}" -LABEL org.opencontainers.image.base.name="${BASE_IMAGE}:${BASE_TAG}" - +LABEL hmpps.java.base_variant="21-jre-jammy" +LABEL hmpps.java.base_image="eclipse-temurin" +LABEL org.opencontainers.image.base.name="eclipse-temurin:21-jre-jammy" WORKDIR /app diff --git a/images/eclipse-temurin/alpine/Dockerfile b/images/java/eclipse-temurin/Dockerfile.java25 similarity index 51% rename from images/eclipse-temurin/alpine/Dockerfile rename to images/java/eclipse-temurin/Dockerfile.java25 index c525f02..27ef778 100644 --- a/images/eclipse-temurin/alpine/Dockerfile +++ b/images/java/eclipse-temurin/Dockerfile.java25 @@ -1,34 +1,29 @@ -# HMPPS Java Base Image (Temurin Alpine variant) -ARG BASE_IMAGE=eclipse-temurin -ARG BASE_TAG=21-jre-alpine -FROM ${BASE_IMAGE}:${BASE_TAG} AS base - -# Re-declare args in-stage for labels -ARG BASE_IMAGE -ARG BASE_TAG +# HMPPS Java Base Image (Eclipse Temurin Java 25) +FROM eclipse-temurin:25-jre-jammy AS base # Metadata -LABEL org.opencontainers.image.title="HMPPS Java Base Image (Alpine)" -LABEL org.opencontainers.image.description="Base container image for Java applications in HMPPS (Alpine variant)" +LABEL org.opencontainers.image.title="HMPPS Java Base Image (Java 25)" +LABEL org.opencontainers.image.description="Base container 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="${BASE_TAG}" -LABEL hmpps.java.base_image="${BASE_IMAGE}" -LABEL org.opencontainers.image.base.name="${BASE_IMAGE}:${BASE_TAG}" +LABEL hmpps.java.base_variant="25-jre-jammy" +LABEL hmpps.java.base_image="eclipse-temurin" +LABEL org.opencontainers.image.base.name="eclipse-temurin:25-jre-jammy" WORKDIR /app -# Set timezone (Alpine) -ENV TZ="Europe/London" -RUN ln -snf /usr/share/zoneinfo/${TZ} /etc/localtime && echo ${TZ} > /etc/timezone - -# Create app user/group (configurable) +# Add default app user and group (configurable) ARG APP_UID=2000 ARG APP_GID=2000 -RUN addgroup -g ${APP_GID} -S appgroup && \ - adduser -u ${APP_UID} -S appuser -G appgroup +RUN addgroup --gid ${APP_GID} --system appgroup && \ + adduser --uid ${APP_UID} --system appuser --gid ${APP_GID} + +# Set timezone +ENV TZ="Europe/London" +RUN ln -snf /usr/share/zoneinfo/${TZ} /etc/localtime && \ + echo ${TZ} > /etc/timezone -# Install AWS RDS Root cert for Postgres clients +# Install AWS RDS Root cert for Postgres clients (libpq convention) 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 && \ @@ -37,7 +32,9 @@ RUN chown appuser:appgroup /home/appuser/.postgresql/root.crt && \ # Security upgrades in separate stage for cache control # Build with: docker buildx build --no-cache-filter=security-upgrades ... FROM base AS security-upgrades -RUN apk upgrade --no-cache +RUN apt-get update && \ + DEBIAN_FRONTEND=noninteractive apt-get -y upgrade && \ + apt-get autoremove -y && apt-get clean && rm -rf /var/lib/apt/lists/* # Sensible JVM defaults for containers ENV JAVA_TOOL_OPTIONS="-XX:+ExitOnOutOfMemoryError -XX:MaxRAMPercentage=50.0" diff --git a/images/node/README.md b/images/node/README.md index 32d26d5..182c1f5 100644 --- a/images/node/README.md +++ b/images/node/README.md @@ -1,25 +1,47 @@ # Node.js Base Image -Lean, standardized Node.js base for HMPPS apps (Alpine variants). +Lean, standardized Node.js base for HMPPS apps across Alpine and distroless variants. ## Variants -| Tag | Description | -|-----|-------------| -| `24-alpine` | Full Node.js 24 Alpine image including npm, yarn, and corepack | -| `24-alpine-runtime` | Runtime-only image with package managers removed — smaller attack surface for production and fewer reported vulnerabilities from scanning tools | +| Image | Tag | Description | +|-------|-----|-------------| +| `hmpps-node` | `24-alpine` | Full Node.js 24 Alpine image including npm, yarn, and corepack | +| `hmpps-node` | `24-alpine-runtime` | Runtime-only image with package managers removed for production | +| `hmpps-distroless-node` | `24` | Distroless runtime image with Node.js only — minimal attack surface (no shell/package manager) | + +## Distroless Variant + +The distroless variant uses Google's minimal distroless base image, reducing attack surface and image size. + +**Notes:** + +- No shell/package manager in runtime image (debugging harder). +- Two-stage build: prepare assets in full Node image, run on distroless. +- Requires explicit binary/library copies; fewer implicit dependencies. ## Features -- Node.js Alpine (24 variant) +- Node.js 24 variants for Alpine and distroless runtimes - Non‑root user `appuser` (UID/GID 2000) and `WORKDIR /app` - Timezone: `Europe/London` -- Security updates applied at build (`apk upgrade --no-cache`) +- Security upgrades stage for Alpine variants (`apk upgrade --no-cache`) - OCI labels: `hmpps.node.base_image`, `hmpps.node.base_variant`, `org.opencontainers.image.base.name` -Registry: `ghcr.io/ministryofjustice/hmpps-node` +Registries: + +- `ghcr.io/ministryofjustice/hmpps-node` +- `ghcr.io/ministryofjustice/hmpps-distroless-node` + +Common tags: + +- `hmpps-node`: `24-alpine`, `24-alpine-runtime`, date tags (YYYYMMDD), `latest` +- `hmpps-distroless-node`: `24`, date tags (YYYYMMDD), `latest` + +Current latest mappings in CI: -Common tags: `24-alpine`, `24-alpine-runtime`, date tags (YYYYMMDD), `latest` +- `ghcr.io/ministryofjustice/hmpps-node:latest` -> `24-alpine-runtime` +- `ghcr.io/ministryofjustice/hmpps-distroless-node:latest` -> `24` ## Usage (simple) @@ -37,7 +59,7 @@ CMD ["npm", "start"] ## Notes - Add build tools (git, curl, etc.) in your app image only if needed. -- To switch Node version, pick the matching tag (e.g. `24-alpine`). +- To switch Node version, pick the matching published tag. - Use `24-alpine-runtime` for the final stage of multi-stage builds — npm/yarn are not needed at runtime and their removal reduces the attack surface. ## Usage (multi-stage) diff --git a/images/node/alpine-runtime/Dockerfile b/images/node/alpine-runtime/Dockerfile index 9c3ec23..287aa63 100644 --- a/images/node/alpine-runtime/Dockerfile +++ b/images/node/alpine-runtime/Dockerfile @@ -1,21 +1,14 @@ # HMPPS Node.js Runtime Base Image - -ARG BASE_IMAGE=node -ARG BASE_TAG=24-alpine -FROM ${BASE_IMAGE}:${BASE_TAG} AS base - -# Re-declare build args in-stage for label use -ARG BASE_IMAGE -ARG BASE_TAG +FROM node:24-alpine AS base # Metadata LABEL org.opencontainers.image.title="HMPPS Node.js Runtime Base Image" LABEL org.opencontainers.image.description="Runtime-only container image for Node.js applications in HMPPS (package managers removed)" LABEL org.opencontainers.image.source="https://github.com/ministryofjustice/hmpps-base-container-images" LABEL org.opencontainers.image.vendor="Ministry of Justice" -LABEL hmpps.node.base_variant="${BASE_TAG}" -LABEL hmpps.node.base_image="${BASE_IMAGE}" -LABEL org.opencontainers.image.base.name="${BASE_IMAGE}:${BASE_TAG}" +LABEL hmpps.node.base_variant="24-alpine" +LABEL hmpps.node.base_image="node" +LABEL org.opencontainers.image.base.name="node:24-alpine" # Create non-root app user (consistent with Java images) ARG APP_UID=2000 diff --git a/images/node/alpine/Dockerfile b/images/node/alpine/Dockerfile index 8bc5f11..a7c4076 100644 --- a/images/node/alpine/Dockerfile +++ b/images/node/alpine/Dockerfile @@ -1,21 +1,14 @@ # HMPPS Node.js Base Image - -ARG BASE_IMAGE=node -ARG BASE_TAG=24-alpine -FROM ${BASE_IMAGE}:${BASE_TAG} AS base - -# Re-declare build args in-stage for label use -ARG BASE_IMAGE -ARG BASE_TAG +FROM node:24-alpine AS base # Metadata LABEL org.opencontainers.image.title="HMPPS Node.js Base Image" LABEL org.opencontainers.image.description="Base container image for Node.js 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.node.base_variant="${BASE_TAG}" -LABEL hmpps.node.base_image="${BASE_IMAGE}" -LABEL org.opencontainers.image.base.name="${BASE_IMAGE}:${BASE_TAG}" +LABEL hmpps.node.base_variant="24-alpine" +LABEL hmpps.node.base_image="node" +LABEL org.opencontainers.image.base.name="node:24-alpine" # Create non-root app user (consistent with Java images) ARG APP_UID=2000 diff --git a/images/node/distroless/Dockerfile b/images/node/distroless/Dockerfile new file mode 100644 index 0000000..404177c --- /dev/null +++ b/images/node/distroless/Dockerfile @@ -0,0 +1,33 @@ +# HMPPS Node.js Runtime Base Image (Distroless) +FROM node:24-bookworm-slim AS prep + +ARG APP_UID=2000 +ARG APP_GID=2000 + +RUN groupadd --gid ${APP_GID} appgroup && \ + useradd --uid ${APP_UID} --gid ${APP_GID} --create-home 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/nodejs22-debian12:nonroot AS runtime + +ARG APP_UID=2000 +ARG APP_GID=2000 + +LABEL org.opencontainers.image.title="HMPPS Node.js Runtime Base Image (Distroless)" +LABEL org.opencontainers.image.description="Distroless runtime image for Node.js 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.node.base_variant="distroless" +LABEL org.opencontainers.image.base.name="gcr.io/distroless/nodejs22-debian12:nonroot" + +WORKDIR /app + +ENV TZ="Europe/London" +ENV NODE_ENV="production" + +COPY --from=prep --chown=${APP_UID}:${APP_GID} /home/appuser/.postgresql/root.crt /home/appuser/.postgresql/root.crt + +USER ${APP_UID}:${APP_GID} diff --git a/images/python/README.md b/images/python/README.md index 0da0130..6b530b6 100644 --- a/images/python/README.md +++ b/images/python/README.md @@ -1,40 +1,43 @@ -# Docker Images for Python 3.13 +# Python Base Image -This repository provides lightweight and optimized Docker images for Python 3.13, based on two different base images: Alpine Linux. These images are hosted on GitHub Container Registry (GHCR). +Lean, standardized Python base image for HMPPS apps. ## Supported Variants -| Variant Tag | OS | Arch (multi-platform) | Notes | -|---------------------------|-------------|-----------------------|--------------------------------| -| python3.13-alpine | Alpine | amd64, arm64 | Lightweight and minimal image | +| Image | Variant Tag | OS | Arch (multi-platform) | Notes | +|-------|-------------|----|-----------------------|-------| +| hmpps-python | python3.13-alpine | Alpine | amd64, arm64 | Lightweight runtime with uv preinstalled | ## Features - Non‑root user `appuser` (UID/GID 2000) and `WORKDIR /app` +- Timezone: `Europe/London` +- Security upgrades stage (`apk upgrade --no-cache`) +- OCI labels: `hmpps.python.base_image`, `hmpps.python.base_variant`, `org.opencontainers.image.base.name` Registry: `ghcr.io/ministryofjustice/hmpps-python` Common tags: `python3.13-alpine`, date tags (YYYYMMDD), `latest` +Current latest mapping in CI: + +- `ghcr.io/ministryofjustice/hmpps-python:latest` -> `python3.13-alpine` + ## Usage (simple) ```dockerfile -FROM ghcr.io/astral-sh/uv:python3.13-alpine +FROM ghcr.io/ministryofjustice/hmpps-python:python3.13-alpine WORKDIR /app -RUN addgroup -g 2000 appgroup && \ - adduser -u 2000 -G appgroup -h /home/appuser -D appuser -RUN chown -R appuser:appgroup /app - -# update PATH environment variable -ENV PATH=/home/appuser/.local:/app:$PATH +COPY --chown=appuser:appgroup . . USER 2000 +CMD ["python", "-m", "your_app"] ``` ## Notes -- Add dependencies hmpps-sre-python-lib and any other like veracode-api-signing, azure-identity etc in your pyproject.toml file and then run `uv sync` +- Add dependencies in `pyproject.toml` and run `uv sync` during your build stage. ## Usage (multi-stage) @@ -52,17 +55,15 @@ ENV BUILD_NUMBER=${BUILD_NUMBER} \ WORKDIR /app -# initialise uv +# Initialize dependencies COPY pyproject.toml . RUN uv sync -# create the /app/trivy directory f -# copy the dependencies from builder stage -RUN chown -R appuser:appgroup /app -COPY classes classes -COPY processes processes -RUN chown -R appuser:appgroup /app/classes /app/processes -COPY --chown=appuser:appgroup ./sharepoint_discovery.py /app/sharepoint_discovery.py +COPY --chown=appuser:appgroup classes classes +COPY --chown=appuser:appgroup processes processes +COPY --chown=appuser:appgroup sharepoint_discovery.py /app/sharepoint_discovery.py + +USER 2000 CMD [ "uv", "run", "python", "-u", "/app/sharepoint_discovery.py" ] ``` \ No newline at end of file diff --git a/images/python/alpine/Dockerfile b/images/python/alpine/Dockerfile index fdd5ce2..864fe0c 100644 --- a/images/python/alpine/Dockerfile +++ b/images/python/alpine/Dockerfile @@ -1,21 +1,14 @@ -# HMPPS Node.js Base Image - -ARG BASE_IMAGE=ghcr.io/astral-sh/uv -ARG BASE_TAG=python3.13-alpine -FROM ${BASE_IMAGE}:${BASE_TAG} AS base - -# Re-declare build args in-stage for label use -ARG BASE_IMAGE -ARG BASE_TAG +# HMPPS Python Base Image +FROM ghcr.io/astral-sh/uv:python3.13-alpine AS base # Metadata -LABEL org.opencontainers.image.title="HMPPS python Base Image" -LABEL org.opencontainers.image.description="Base container image for python applications used within HMPPS" +LABEL org.opencontainers.image.title="HMPPS Python Base Image" +LABEL org.opencontainers.image.description="Base container image for Python applications used within HMPPS" LABEL org.opencontainers.image.source="https://github.com/ministryofjustice/hmpps-base-container-images" LABEL org.opencontainers.image.vendor="Ministry of Justice" -LABEL hmpps.python.base_variant="${BASE_TAG}" -LABEL hmpps.python.base_image="${BASE_IMAGE}" -LABEL org.opencontainers.image.base.name="${BASE_IMAGE}:${BASE_TAG}" +LABEL hmpps.python.base_variant="python3.13-alpine" +LABEL hmpps.python.base_image="ghcr.io/astral-sh/uv" +LABEL org.opencontainers.image.base.name="ghcr.io/astral-sh/uv:python3.13-alpine" WORKDIR /app ARG APP_UID=2000