diff --git a/README.md b/README.md index 1f19c51..f35d2b8 100644 --- a/README.md +++ b/README.md @@ -18,7 +18,7 @@ [![Hadolint](https://img.shields.io/badge/Hadolint-clean-success)](https://github.com/hadolint/hadolint) [![Prettier](https://img.shields.io/badge/Code_Style-Prettier-F7B93E?logo=prettier&logoColor=black)](https://prettier.io/) -Reference [Dockerfiles](https://docs.docker.com/engine/reference/builder/) and best-practice guides for building **secure container images** that ship to production: small, signed, scanner-friendly, [OCI](https://opencontainers.org/)-compliant. [Python](https://www.python.org/), [Go](https://go.dev/), [JAX](https://jax.readthedocs.io/), [Node.js](https://nodejs.org/), [TypeScript](https://www.typescriptlang.org/), and [Rust](https://www.rust-lang.org/) are covered. +Reference [Dockerfiles](https://docs.docker.com/engine/reference/builder/) and best-practice guides for building **secure container images** that ship to production: small, signed, scanner-friendly, [OCI](https://opencontainers.org/)-compliant. [Python](https://www.python.org/), [Go](https://go.dev/), [JAX](https://jax.readthedocs.io/), [Node.js](https://nodejs.org/), [TypeScript](https://www.typescriptlang.org/), [Rust](https://www.rust-lang.org/), and [Java](https://openjdk.org/) are covered. ## Why these templates @@ -135,6 +135,18 @@ Multi-stage [Rust](https://www.rust-lang.org/) images using [cargo-chef](https:/ Full documentation: [`dockerfiles/rust/README.md`](dockerfiles/rust/README.md). +### Java — Maven/Gradle dep caching + Distroless JRE + +Multi-stage [Java](https://openjdk.org/) images supporting both Maven and Gradle via `--build-arg BUILD_TOOL`. Deps are cached in a dedicated layer before source is copied. Ships in [**Google Distroless java21**](https://github.com/GoogleContainerTools/distroless/blob/main/java/README.md) — JRE only, no shell, no package manager. Includes a GraalVM Native Image variant for fast cold starts. + +- Distroless JVM image (Maven/Gradle) → [`dockerfiles/java/Dockerfile.java`](dockerfiles/java/Dockerfile.java) +- GraalVM Native Image → [`dockerfiles/java/Dockerfile.java.native`](dockerfiles/java/Dockerfile.java.native) +- Chainguard production image → [`dockerfiles/java/Dockerfile.java.chainguard`](dockerfiles/java/Dockerfile.java.chainguard) +- AWS Lambda container image → [`dockerfiles/java/Dockerfile.lambda`](dockerfiles/java/Dockerfile.lambda) +- VS Code devcontainer (Java 21 + Maven + Gradle) → [`dockerfiles/java/Dockerfile.devcontainer`](dockerfiles/java/Dockerfile.devcontainer) + +Full documentation: [`dockerfiles/java/README.md`](dockerfiles/java/README.md). + ### Coming soon Tracked as issues — comment or 👍 to bump priority. diff --git a/dockerfiles/java/.devcontainer/devcontainer.json b/dockerfiles/java/.devcontainer/devcontainer.json new file mode 100644 index 0000000..178552f --- /dev/null +++ b/dockerfiles/java/.devcontainer/devcontainer.json @@ -0,0 +1,36 @@ +{ + "name": "Java", + "build": { + "dockerfile": "../Dockerfile.devcontainer", + "context": "..", + "args": { + "VARIANT": "1-21-bookworm" + } + }, + "remoteUser": "vscode", + "workspaceMount": "source=${localWorkspaceFolder},target=/workspace,type=bind,consistency=cached", + "workspaceFolder": "/workspace", + "customizations": { + "vscode": { + "extensions": [ + "redhat.java", + "vscjava.vscode-maven", + "vscjava.vscode-gradle", + "vscjava.vscode-java-test", + "vscjava.vscode-java-debug", + "vscjava.vscode-spring-initializr", + "vmware.vscode-spring-boot", + "usernamehw.errorlens" + ], + "settings": { + "java.configuration.updateBuildConfiguration": "automatic", + "java.compile.nullAnalysis.mode": "automatic", + "editor.formatOnSave": true, + "[java]": { + "editor.defaultFormatter": "redhat.java" + } + } + } + }, + "postCreateCommand": "mvn dependency:go-offline -q 2>/dev/null || gradle dependencies -q 2>/dev/null || true" +} diff --git a/dockerfiles/java/.dockerignore b/dockerfiles/java/.dockerignore new file mode 100644 index 0000000..40e18b3 --- /dev/null +++ b/dockerfiles/java/.dockerignore @@ -0,0 +1,42 @@ +# Maven build output +target/ + +# Gradle build output and cache +build/ +.gradle/ + +# Gradle wrapper jar (optional -- include if offline builds are needed) +gradle/wrapper/gradle-wrapper.jar + +# Compiled class files +*.class + +# Stale fat JARs in the source root +*.jar + +# Editor and IDE +.vscode/ +.idea/ +*.iml +*.swp +*.swo + +# Devcontainer +.devcontainer/ + +# Credentials and secrets +.env +*.pem +*.key +*.cert + +# CI +.github/ + +# Git +.git/ +.gitignore + +# Documentation +*.md +LICENSE diff --git a/dockerfiles/java/Dockerfile.devcontainer b/dockerfiles/java/Dockerfile.devcontainer new file mode 100644 index 0000000..24468a1 --- /dev/null +++ b/dockerfiles/java/Dockerfile.devcontainer @@ -0,0 +1,33 @@ +# Java devcontainer image (Microsoft devcontainers/java base). +# +# Extends the official VS Code Java devcontainer with additional tooling: +# - Gradle (pinned) for projects not using the Gradle wrapper +# - jq and curl for JSON processing and HTTP requests +# +# The base image ships: Java JDK, Maven, git, common dev utilities, and +# the VS Code remote-containers server. +# +# Build context: dockerfiles/java/ +# docker build -f Dockerfile.devcontainer . +# +# Typical usage: open in VS Code with the Dev Containers extension; +# the .devcontainer/devcontainer.json references this file. +ARG VARIANT=1-21-bookworm + +FROM mcr.microsoft.com/devcontainers/java:${VARIANT} + +USER root + +RUN apt-get update && apt-get install -y --no-install-recommends \ + jq=1.6-2.1 \ + curl=7.88.1-10+deb12u12 \ + && rm -rf /var/lib/apt/lists/* + +ARG GRADLE_VERSION=8.11 +RUN curl -fsSL "https://services.gradle.org/distributions/gradle-${GRADLE_VERSION}-bin.zip" \ + -o /tmp/gradle.zip && \ + unzip -q /tmp/gradle.zip -d /opt && \ + ln -s "/opt/gradle-${GRADLE_VERSION}/bin/gradle" /usr/local/bin/gradle && \ + rm /tmp/gradle.zip + +USER vscode diff --git a/dockerfiles/java/Dockerfile.java b/dockerfiles/java/Dockerfile.java new file mode 100644 index 0000000..6634ec9 --- /dev/null +++ b/dockerfiles/java/Dockerfile.java @@ -0,0 +1,77 @@ +# Java application image (Eclipse Temurin + Google Distroless java21, multi-stage). +# +# Supports both Maven and Gradle via BUILD_TOOL build-arg (default: maven). +# Dependency manifests are copied before source, so a source-only change does +# not invalidate the dependency-download layer. +# +# BUILD_TOOL stages: +# maven-deps — copies pom.xml and resolves deps offline (Maven projects). +# gradle-deps — copies Gradle wrappers and resolves deps (Gradle projects). +# builder — FROM ${BUILD_TOOL}-deps; compiles and packages the fat JAR. +# +# Only the stage matching BUILD_TOOL is executed by BuildKit — the other is +# skipped entirely, so the Gradle stage never runs in a Maven project and +# vice versa. +# +# Runtime: gcr.io/distroless/java21-debian12:nonroot — ships the JRE but no +# shell, no package manager. The process cannot drop into a shell or install +# tools. Incompatible with GraalVM native image; use Dockerfile.java.native +# for native compilation. +# +# Spring Boot layered JARs: +# For Spring Boot 2.3+ apps, unpack the fat JAR into layers to get better +# Docker cache utilisation — each layer (dependencies, spring-boot-loader, +# snapshot-dependencies, application) is a separate COPY. See README.md. +# +# Multi-arch builds: +# docker buildx build --platform=linux/amd64,linux/arm64 \ +# --build-arg BUILD_TOOL=maven -t myapp -f Dockerfile.java . +# +# Build: +# docker build --build-arg BUILD_TOOL=maven -t myapp -f Dockerfile.java . +# docker build --build-arg BUILD_TOOL=gradle -t myapp -f Dockerfile.java . +# +# Run (hardened): +# docker run --rm \ +# --read-only \ +# --cap-drop=ALL \ +# --security-opt=no-new-privileges \ +# myapp +ARG BUILD_TOOL=maven +ARG JAVA_VERSION=21 +ARG DISTROLESS_TAG=debian12 + +# -- Maven: cache pom.xml before source +FROM eclipse-temurin:${JAVA_VERSION}-jdk-jammy AS maven-deps +WORKDIR /app +COPY pom.xml ./ +RUN mvn dependency:go-offline -q + +# -- Gradle: cache wrapper + build files before source +FROM eclipse-temurin:${JAVA_VERSION}-jdk-jammy AS gradle-deps +WORKDIR /app +COPY gradlew build.gradle* settings.gradle* ./ +COPY gradle/ ./gradle/ +RUN chmod +x gradlew && ./gradlew dependencies --no-daemon -q + +# -- Build: compile and package (inherits deps layer from chosen tool) +# hadolint ignore=DL3006 +FROM ${BUILD_TOOL}-deps AS builder +ARG BUILD_TOOL +WORKDIR /app +COPY src ./src +SHELL ["/bin/bash", "-o", "pipefail", "-c"] +RUN if [ "${BUILD_TOOL}" = "maven" ]; then \ + mvn package -DskipTests -q && cp target/*.jar /app/app.jar; \ + else \ + ./gradlew build --no-daemon -x test -q && cp build/libs/*.jar /app/app.jar; \ + fi + +# -- Runtime +FROM gcr.io/distroless/java${JAVA_VERSION}-${DISTROLESS_TAG}:nonroot +WORKDIR /app +COPY --from=builder --chown=nonroot:nonroot /app/app.jar /app/app.jar + +USER nonroot + +ENTRYPOINT ["java", "-jar", "/app/app.jar"] diff --git a/dockerfiles/java/Dockerfile.java.chainguard b/dockerfiles/java/Dockerfile.java.chainguard new file mode 100644 index 0000000..6327c74 --- /dev/null +++ b/dockerfiles/java/Dockerfile.java.chainguard @@ -0,0 +1,36 @@ +# Java application image (Chainguard maven builder + Chainguard jre runtime). +# +# Chainguard sibling of Dockerfile.java. Builds with cgr.dev/chainguard/maven +# and ships in cgr.dev/chainguard/jre — daily-rebuilt, Sigstore-signed, with +# SLSA provenance and SBOMs attached. +# +# Free vs. paid Chainguard tags: +# The free Developer Edition only publishes :latest. Versioned tags +# require a paid Chainguard subscription. For free-tier reproducibility, +# pin by digest instead of relying on :latest: +# +# docker pull cgr.dev/chainguard/maven:latest +# docker inspect --format='{{index .RepoDigests 0}}' cgr.dev/chainguard/maven:latest +# docker pull cgr.dev/chainguard/jre:latest +# docker inspect --format='{{index .RepoDigests 0}}' cgr.dev/chainguard/jre:latest +# # bake the digests into the FROM lines. +# +# Refresh digests deliberately (e.g. weekly) and re-scan. +# +# Build (free tier -- pin by digest in production): +# docker build -t myapp -f Dockerfile.java.chainguard . +ARG BASE_TAG=latest + +FROM cgr.dev/chainguard/maven:${BASE_TAG} AS builder +WORKDIR /app +COPY pom.xml ./ +RUN mvn dependency:go-offline -q +COPY src ./src +RUN mvn package -DskipTests -q && cp target/*.jar /app/app.jar + +FROM cgr.dev/chainguard/jre:${BASE_TAG} +COPY --from=builder /app/app.jar /app/app.jar + +USER java + +ENTRYPOINT ["java", "-jar", "/app/app.jar"] diff --git a/dockerfiles/java/Dockerfile.java.native b/dockerfiles/java/Dockerfile.java.native new file mode 100644 index 0000000..dc8bdb2 --- /dev/null +++ b/dockerfiles/java/Dockerfile.java.native @@ -0,0 +1,49 @@ +# Java GraalVM Native Image (native-image-community + Distroless cc, multi-stage). +# +# Compiles a Java application to a native binary via GraalVM Native Image. +# The resulting binary starts in milliseconds (no JVM warm-up) and uses a +# fraction of the heap compared to the JVM variant. +# +# Trade-offs vs. the JVM variant (Dockerfile.java): +# + Cold start: ~10ms vs. 100-500ms for a typical Spring Boot app. +# + Memory: native binary uses 2-5x less heap at steady state. +# - Build time: native-image compilation takes minutes (vs. seconds for javac). +# - Reflection: dynamic reflection, JNI, and serialization require AOT +# configuration (reflect-config.json). Spring Boot 3 Native handles this +# automatically via the GraalVM reachability metadata. +# - Build platform: the native binary targets the build host OS/arch; cross- +# compilation requires separate builds per platform. +# +# Runtime: gcr.io/distroless/cc-debian12:nonroot — includes glibc and +# libstdc++ for a dynamically-linked native binary. For a fully-static binary +# (requires musl toolchain; see README.md), switch to distroless/static. +# +# APP_NAME must match the artifact ID / binary name produced by native-image. +# +# Build: +# docker build --build-arg APP_NAME=myapp -t myapp -f Dockerfile.java.native . +# +# Run: +# docker run --rm \ +# --read-only \ +# --cap-drop=ALL \ +# --security-opt=no-new-privileges \ +# myapp +ARG GRAALVM_TAG=21-ol9 +ARG DISTROLESS_TAG=debian12 + +FROM ghcr.io/graalvm/native-image-community:${GRAALVM_TAG} AS builder +ARG APP_NAME=app +WORKDIR /app +COPY pom.xml ./ +RUN mvn dependency:go-offline -q +COPY src ./src +RUN mvn -Pnative native:compile -DskipTests -q && \ + cp "target/${APP_NAME}" /native-binary + +FROM gcr.io/distroless/cc-${DISTROLESS_TAG}:nonroot +COPY --from=builder --chown=nonroot:nonroot /native-binary /app/server + +USER nonroot + +ENTRYPOINT ["/app/server"] diff --git a/dockerfiles/java/Dockerfile.lambda b/dockerfiles/java/Dockerfile.lambda new file mode 100644 index 0000000..58ab248 --- /dev/null +++ b/dockerfiles/java/Dockerfile.lambda @@ -0,0 +1,40 @@ +# Java AWS Lambda image (Eclipse Temurin builder + Lambda java runtime). +# +# Uses Eclipse Temurin to build a fat JAR, then copies it into the AWS Lambda +# Java base image which ships the Lambda Runtime Interface Client (RIC) and +# the Lambda Runtime Interface Emulator (RIE) for local testing. +# +# Handler class: +# Set CMD to "com.example.MyHandler::handleRequest" (fully qualified class +# name :: method name). The Lambda Java runtime invokes this method for +# each event. +# +# SnapStart: +# Enable AWS Lambda SnapStart (Java 21 Managed Runtime) to take a snapshot +# after initialization and restore it on cold starts — cuts cold start from +# ~400ms to ~10ms for typical Spring Boot apps. Requires: +# - Lambda function runtime: Java 21 (Managed Runtime, not container image) +# - OR: configure CRaC (Coordinated Restore at Checkpoint) in your app for +# container-based Lambda SnapStart. +# See: https://docs.aws.amazon.com/lambda/latest/dg/snapstart.html +# +# Build: +# docker build --platform=linux/amd64 -t myapp-lambda -f Dockerfile.lambda . +# +# Local invocation (Lambda Runtime Interface Emulator): +# docker run --rm -p 9000:8080 myapp-lambda +# curl -XPOST "http://localhost:9000/2015-03-31/functions/function/invocations" \ +# -d '{"key":"value"}' +ARG JAVA_VERSION=21 + +FROM eclipse-temurin:${JAVA_VERSION}-jdk-jammy AS builder +WORKDIR /app +COPY pom.xml ./ +RUN mvn dependency:go-offline -q +COPY src ./src +RUN mvn package -DskipTests -q + +FROM public.ecr.aws/lambda/java:${JAVA_VERSION} +COPY --from=builder /app/target/*.jar ${LAMBDA_TASK_ROOT}/ + +CMD ["com.example.Handler::handleRequest"] diff --git a/dockerfiles/java/README.md b/dockerfiles/java/README.md new file mode 100644 index 0000000..776cce5 --- /dev/null +++ b/dockerfiles/java/README.md @@ -0,0 +1,291 @@ +# Secure Java Docker Image Templates + +Reference Dockerfiles for building **secure Java Docker images** in production — including a **Distroless JRE** variant with Maven/Gradle dep caching, a **GraalVM Native Image** variant, a **Chainguard** variant for signed/attested images, an **AWS Lambda** variant, and a **devcontainer** for VS Code. + +**Default runtime is [`gcr.io/distroless/java21-debian12:nonroot`](https://github.com/GoogleContainerTools/distroless/blob/main/java/README.md).** This image ships the JRE but no shell, no package manager. Chainguard variants are provided as siblings for users who prefer daily-rebuilt, Sigstore-signed images. + +| File | Builder | Runtime base | Use when | +| ---------------------------- | ----------------------------------------------- | --------------------------------------------- | ------------------------------------------------------------------- | +| `Dockerfile.java` | `eclipse-temurin:21-jdk-jammy` | `gcr.io/distroless/java21-debian12:nonroot` | Default. Maven or Gradle fat JAR on the JVM. | +| `Dockerfile.java.native` | `ghcr.io/graalvm/native-image-community:21-ol9` | `gcr.io/distroless/cc-debian12:nonroot` | GraalVM Native Image for fast cold starts and lower memory. | +| `Dockerfile.java.chainguard` | `cgr.dev/chainguard/maven` | `cgr.dev/chainguard/jre` | Chainguard daily CVE patches, Sigstore signatures, SLSA provenance. | +| `Dockerfile.lambda` | `eclipse-temurin:21-jdk-jammy` | `public.ecr.aws/lambda/java:21` | AWS Lambda container image with Lambda RIC pre-installed. | +| `Dockerfile.devcontainer` | — | `mcr.microsoft.com/devcontainers/java:1-21-*` | VS Code Remote-Containers / Dev Containers development environment. | + +## Why these images are efficient + +### Maven dep-cache layer + +Copy `pom.xml` before source so the dependency-download layer is only invalidated when dependencies change: + +```dockerfile +COPY pom.xml ./ +RUN mvn dependency:go-offline -q # cached layer +COPY src ./src +RUN mvn package -DskipTests -q +``` + +Changing only `src/` reuses the `mvn dependency:go-offline` layer. Re-builds for code-only changes take seconds instead of minutes. + +### Gradle dep-cache layer + +Copy the Gradle wrapper and build files before source: + +```dockerfile +COPY gradlew build.gradle* settings.gradle* ./ +COPY gradle/ ./gradle/ +RUN chmod +x gradlew && ./gradlew dependencies --no-daemon -q # cached layer +COPY src ./src +RUN ./gradlew build --no-daemon -x test -q +``` + +### Maven vs Gradle via ARG BUILD_TOOL + +`Dockerfile.java` supports both build tools via `--build-arg BUILD_TOOL=maven` (default) or `--build-arg BUILD_TOOL=gradle`. BuildKit builds only the stage that matches — the unused tool's dep-cache stage is never executed. + +```bash +# Maven project (default) +docker build --build-arg BUILD_TOOL=maven -t myapp -f Dockerfile.java . + +# Gradle project +docker build --build-arg BUILD_TOOL=gradle -t myapp -f Dockerfile.java . +``` + +### Multi-stage build + +The builder stage (Temurin JDK + Maven/Gradle) stays out of the final image. Only the compiled JAR is copied into the distroless JRE runtime. Result: the final image contains the JRE and your JAR — no build toolchain. + +### Spring Boot layered JARs + +Spring Boot 2.3+ supports layered JARs that split the fat JAR into distinct layers. Use this to get much better Docker cache utilisation for framework applications: + +```dockerfile +FROM eclipse-temurin:21-jdk-jammy AS builder +# ... build fat JAR ... +RUN java -Djarmode=layertools -jar target/app.jar extract + +FROM gcr.io/distroless/java21-debian12:nonroot +COPY --from=builder /app/dependencies / +COPY --from=builder /app/spring-boot-loader / +COPY --from=builder /app/snapshot-dependencies / +COPY --from=builder /app/application / +ENTRYPOINT ["java", "org.springframework.boot.loader.launch.JarLauncher"] +``` + +The `dependencies` layer (all stable library JARs) is rarely invalidated. Only the `application` layer (your compiled classes) changes on code edits. + +## JVM vs GraalVM Native Image + +| Metric | JVM (`Dockerfile.java`) | Native Image (`Dockerfile.java.native`) | +| ------------------- | ----------------------- | --------------------------------------- | +| Cold start | 100–500 ms | 10–50 ms | +| Steady-state RSS | 200–500 MB | 50–150 MB | +| Build time | Seconds | Minutes | +| Reflection | Dynamic (no config) | Requires AOT config or Spring Native | +| JNI / Proxies | Transparent | Requires reachability metadata | +| Profile-guided opts | Yes (JIT) | Requires PGO instrumentation build | + +### GraalVM Native Image (`Dockerfile.java.native`) + +The `Dockerfile.java.native` uses `ghcr.io/graalvm/native-image-community:21-ol9` to compile to a native binary via `mvn -Pnative native:compile`. The runtime is `gcr.io/distroless/cc-debian12:nonroot` (glibc-linked). + +For **Spring Boot 3** applications, add the Spring AOT plugin: + +```xml + + org.graalvm.buildtools + native-maven-plugin + +``` + +The plugin generates the AOT configuration (reflection, serialization, proxies) automatically at build time. + +#### Fully-static native binary (musl) + +To produce a fully-static binary and ship in `distroless/static-debian12:nonroot`: + +1. Install the musl toolchain in the builder stage: + +```dockerfile +RUN dnf install -y gcc musl-gcc && \ + curl -fsSL https://musl.libc.org/releases/musl-1.2.5.tar.gz | tar xz && \ + cd musl-1.2.5 && ./configure --prefix=/usr/local/musl && make install +``` + +1. Compile with `--static --libc=musl`: + +```dockerfile +RUN native-image --static --libc=musl -jar target/app.jar app +``` + +1. Switch the runtime to `gcr.io/distroless/static-debian12:nonroot`. + +## Why these images are secure + +### No shell in the JVM runtime + +`distroless/java21-debian12:nonroot` ships the JRE, CA certificates, and tzdata. There is no `/bin/sh`, no package manager, no curl. A compromised JVM process cannot drop into a shell. + +### Non-root by default + +All production final stages run as a non-root user (`nonroot` UID 65532 for distroless, `java` for Chainguard JRE). The JAR is `--chown=nonroot:nonroot` so the process cannot overwrite it. + +## Build and run + +```bash +# Distroless JVM (Maven, default) +docker build --build-arg BUILD_TOOL=maven -t myapp -f Dockerfile.java . + +# Distroless JVM (Gradle) +docker build --build-arg BUILD_TOOL=gradle -t myapp -f Dockerfile.java . + +# GraalVM Native Image +docker build --build-arg APP_NAME=myapp -t myapp-native -f Dockerfile.java.native . + +# Chainguard (free tier -- pin by digest in production) +docker build -t myapp -f Dockerfile.java.chainguard . + +# Lambda (match your function's architecture) +docker build --platform=linux/amd64 -t myapp-lambda -f Dockerfile.lambda . + +# Multi-arch JVM +docker buildx build --platform=linux/amd64,linux/arm64 \ + --build-arg BUILD_TOOL=maven -t myapp -f Dockerfile.java . + +# Run (hardened) +docker run --rm \ + --read-only \ + --cap-drop=ALL \ + --security-opt=no-new-privileges \ + myapp +``` + +## Expected build-context layout + +```text +. +├── Dockerfile.java # or .native / .chainguard / .lambda +├── .dockerignore # provided in this directory +├── pom.xml # Maven: required for dep-cache layer +├── src/ +│ └── main/java/ +│ └── com/example/ +│ └── Main.java +``` + +For Gradle projects, replace `pom.xml` with `build.gradle` / `settings.gradle` / `gradlew` / `gradle/`. + +## Lambda variant + +The Lambda Java base image (`public.ecr.aws/lambda/java:21`) ships the AWS Lambda Runtime Interface Client. The fat JAR is copied to `${LAMBDA_TASK_ROOT}`. Set `CMD` to the fully-qualified handler class and method: + +```dockerfile +CMD ["com.example.Handler::handleRequest"] +``` + +### Local invocation + +```bash +docker run --rm -p 9000:8080 myapp-lambda +curl -XPOST "http://localhost:9000/2015-03-31/functions/function/invocations" \ + -d '{"key":"value"}' +``` + +### AWS Lambda SnapStart + +SnapStart (available on the Java 21 Managed Runtime) snapshots the initialized Lambda execution environment and restores it on cold starts — reducing cold start latency from ~400 ms to ~10 ms. To use SnapStart with a container image, configure [CRaC (Coordinated Restore at Checkpoint)](https://docs.aws.amazon.com/lambda/latest/dg/snapstart-supported-states.html) in your application and implement the `CracResource` interface to clean up and restore resources across checkpoints. + +## Chainguard digest-pinning + +The Chainguard free tier publishes only `:latest`. For reproducible builds, pin by digest: + +```bash +docker pull cgr.dev/chainguard/maven:latest +docker inspect --format='{{index .RepoDigests 0}}' cgr.dev/chainguard/maven:latest + +docker pull cgr.dev/chainguard/jre:latest +docker inspect --format='{{index .RepoDigests 0}}' cgr.dev/chainguard/jre:latest +``` + +Then replace the `FROM` lines with the digest form: + +```dockerfile +FROM cgr.dev/chainguard/maven@sha256: AS builder +# ... +FROM cgr.dev/chainguard/jre@sha256: +``` + +## Devcontainer variant + +`Dockerfile.devcontainer` is based on `mcr.microsoft.com/devcontainers/java:1-21-bookworm` and adds: + +- Gradle (pinned) for projects not using the Gradle wrapper +- `jq` and `curl` for common dev tasks + +The base image ships Maven, git, and common dev utilities. + +The companion `.devcontainer/devcontainer.json` wires up: + +- `redhat.java` Language Support for Java +- `vscjava.vscode-maven` and `vscjava.vscode-gradle` for build tool integration +- `vscjava.vscode-java-test` Test Runner UI +- `vscjava.vscode-spring-initializr` and `vmware.vscode-spring-boot` for Spring apps +- `postCreateCommand: mvn dependency:go-offline` to warm the dep cache on start + +### Reopen in Container + +1. Open the `dockerfiles/java/` folder in VS Code. +2. When prompted "Reopen in Container", click yes — or use `Ctrl+Shift+P` → **Dev Containers: Reopen in Container**. +3. VS Code builds `Dockerfile.devcontainer`, mounts the workspace, and installs extensions. + +## JLink custom JRE + +For a middle ground between the full JDK and distroless, `jlink` can produce a minimal custom JRE that includes only the modules your application uses: + +```dockerfile +FROM eclipse-temurin:21-jdk-jammy AS jlink +RUN jlink \ + --add-modules java.base,java.logging,java.sql,java.net.http \ + --strip-debug \ + --no-man-pages \ + --no-header-files \ + --compress=zip-6 \ + --output /jre-custom + +FROM debian:bookworm-slim +COPY --from=jlink /jre-custom /opt/java +ENV PATH="/opt/java/bin:${PATH}" +# ... +``` + +This is not provided as a separate Dockerfile but is a useful technique documented here. The modules list must be tuned per application via `jdeps --print-module-deps`. + +## Multi-arch builds + +GraalVM Native Image does **not** support cross-compilation — you must build natively on each target platform. Use separate `docker buildx build --platform=linux/amd64` and `--platform=linux/arm64` commands (or separate CI jobs) for the native variant. + +The JVM variant (`Dockerfile.java`) supports multi-arch: + +```bash +docker buildx create --use + +docker buildx build \ + --platform=linux/amd64,linux/arm64 \ + --push \ + --build-arg BUILD_TOOL=maven \ + -t myregistry/myapp:latest \ + -f Dockerfile.java . +``` + +## Hardening checklist + +- [ ] Pin `JAVA_VERSION` to `21` (or your target LTS) — never use `latest`. +- [ ] Pin the base image by digest in production (`gcr.io/distroless/java21-debian12@sha256:…`). +- [ ] Commit `pom.xml` / `build.gradle` with exact dependency versions (no SNAPSHOT ranges in production). +- [ ] Pass `-DskipTests` only in the Docker build; run tests in CI before building the image. +- [ ] For Spring Boot: enable layered JARs to improve cache utilisation (see above). +- [ ] For GraalVM: run `-DskipTests=false` in native profile in CI to catch AOT reflection issues. +- [ ] Run with `--read-only`, `--cap-drop=ALL`, `--security-opt=no-new-privileges`. +- [ ] Set resource limits (`--memory`, `--cpus`) — the JVM will size its heap to the container limit. +- [ ] Scan the built image (`grype`, `trivy`) before publishing. +- [ ] Sign the image and SLSA provenance with Cosign — see [`docs/supply-chain.md`](../../docs/supply-chain.md).