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
14 changes: 13 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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.
Expand Down
36 changes: 36 additions & 0 deletions dockerfiles/java/.devcontainer/devcontainer.json
Original file line number Diff line number Diff line change
@@ -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"
}
42 changes: 42 additions & 0 deletions dockerfiles/java/.dockerignore
Original file line number Diff line number Diff line change
@@ -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
33 changes: 33 additions & 0 deletions dockerfiles/java/Dockerfile.devcontainer
Original file line number Diff line number Diff line change
@@ -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
77 changes: 77 additions & 0 deletions dockerfiles/java/Dockerfile.java
Original file line number Diff line number Diff line change
@@ -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"]
36 changes: 36 additions & 0 deletions dockerfiles/java/Dockerfile.java.chainguard
Original file line number Diff line number Diff line change
@@ -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"]
49 changes: 49 additions & 0 deletions dockerfiles/java/Dockerfile.java.native
Original file line number Diff line number Diff line change
@@ -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"]
40 changes: 40 additions & 0 deletions dockerfiles/java/Dockerfile.lambda
Original file line number Diff line number Diff line change
@@ -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"]
Loading
Loading