Posted on ::

OpenJDK Container Images

# Distroless Java Images
# https://console.cloud.google.com/gcr/images/distroless/GLOBAL
# https://github.com/GoogleContainerTools/distroless#what-images-are-available
$ docker pull gcr.io/distroless/java-base-debian12:latest
$ docker pull gcr.io/distroless/java21-debian12:latest

# Distroless Static & Base
$ docker pull gcr.io/distroless/static-debian12:latest
$ docker pull gcr.io/distroless/base-debian12:latest

# GraalVM native-image base
$ docker pull cgr.dev/chainguard/graalvm-native:latest
$ docker pull cgr.dev/chainguard/static:latest-glibc
$ docker pull cgr.dev/chainguard/jdk:latest

# Openjdk
# https://github.com/docker-library/openjdk
$ docker pull openjdk:25-slim

# Eclipse Temurin
# https://github.com/adoptium/containers#supported-images
$ docker pull eclipse-temurin:24-jdk
$ docker pull eclipse-temurin:24-alpine

# Oracle OpenJDK
$ docker pull container-registry.oracle.com/java/openjdk:latest

# Oracle Java (NFTC)
# https://github.com/oracle/docker-images/tree/main/OracleJava
$ docker pull container-registry.oracle.com/java/jdk:latest

# Azul Zulu
# https://github.com/zulu-openjdk/zulu-openjdk
$ docker pull azul/zulu-openjdk-debian:24-jre
$ docker pull azul/zulu-openjdk-alpine:24-jre
$ docker pull azul/prime-debian:latest

# Amazon Corretto
# https://github.com/corretto/corretto-docker
$ docker pull amazoncorretto:23
$ docker pull amazoncorretto:23-alpine

# Microsoft OpenJDK
# https://learn.microsoft.com/en-us/java/openjdk/containers
$ docker pull mcr.microsoft.com/openjdk/jdk:21-ubuntu
$ docker pull mcr.microsoft.com/openjdk/jdk:21-distroless

# Liberica
# https://github.com/bell-sw/Liberica/tree/master/docker/repos/liberica-openjre-debian
$ docker pull bellsoft/liberica-openjdk-debian:latest
$ docker pull bellsoft/liberica-openjdk-alpine-musl:latest
$ docker pull bellsoft/liberica-openjdk-alpine:latest (libc)

# GraalVM CE & EE
# https://github.com/graalvm/container
$ docker pull ghcr.io/graalvm/graalvm-community:latest
$ docker pull ghcr.io/graalvm/native-image-community:muslib
$ docker pull container-registry.oracle.com/graalvm/native-image:latest

# GraalVM CE Dev Builds (No docker images available)
https://github.com/graalvm/graalvm-ce-dev-builds/releases/

# Jetbrains Runtime (No docker images available)
https://github.com/JetBrains/JetBrainsRuntime/releases

# Azul Zulu OpenJDK & Mission Control (Homebrew on MacOS)
# https://github.com/mdogan/homebrew-zulu
$ brew tap mdogan/zulu
$ brew install <name>
  • https://hub.docker.com/_/openjdk
  • https://github.com/docker-library/docs/tree/master/openjdk
  • https://container-registry.oracle.com/java/openjdk
  • https://www.graalvm.org/docs/getting-started/container-images/

Docker Commands

  • Docker Run

    # Remove all unused images, not just dangling ones
    $ docker system prune -af
    
    # Docker Run
    $ docker run \
            -it \
            --rm \
            --pull always \
            --workdir /app \
            --publish 8080:8080 \
            --name app \
            --mount type=bind,source=$PWD,destination=/app,readonly \
            openjdk:25-slim       
  • Docker Image Size

    # Compressed Size
    $ docker image save ghcr.io/sureshg/containers:openjdk-latest | gzip | wc -c | numfmt --to=si
    $ docker image save ghcr.io/sureshg/containers:nativeimage-latest | gzip | wc -c | numfmt --to=si
    # OR
    $ docker manifest inspect ghcr.io/sureshg/containers:openjdk-latest -v
    
    # Uncompressed Size
    $ docker inspect -f "{{ .Size }}" sureshg/jvm | numfmt --to=si
    $ docker history sureshg/jvm

Debug Container

# On M1 mac
$ docker run \
         --platform linux/amd64 \
         --pull always \
         -it \
         --rm \
         -p 8080:80 \
         --name openjdk-playground \
         ghcr.io/sureshg/containers:openjdk-latest

# Default busybox image should work.
$ brew install cdebug
$ cdebug exec \
         --privileged \
         -it \
         --rm \
         --platform linux/amd64 \
         docker://openjdk-playground

# Extract a file from the Docker environment and store it locally
$ docker run \
         --rm \
         --entrypoint cat \
         busybox:latest \
         '/bin/ls' > ls

# OR copy files from a container
$ docker cp <CONTAINER>:/app/app/jar .

# OR
$ id=$(docker create ghcr.io/sureshg/containers:openjdk-latest)
$ docker cp $id:/app/app.jar - > app.tar
$ docker rm -v $id

JVM Ergonomics and Container logs

$ cat << EOF >App.java
import static java.lang.System.out;
public class App {
  void main() {
    var version = "• Java %s running on %s %s".formatted(
                   System.getProperty("java.version"),
                   System.getProperty("os.name"),
                   System.getProperty("os.arch"));
    out.println(version);
    long unit = 1024 * 1024L;
    long heapSize = Runtime.getRuntime().totalMemory();
    long heapFreeSize = Runtime.getRuntime().freeMemory();
    long heapUsedSize = heapSize-heapFreeSize;
    long heapMaxSize = Runtime.getRuntime().maxMemory();

    out.println("• [CPU] Active Processors: " + Runtime.getRuntime().availableProcessors());
    out.println("• [Mem] Current Heap Size (Committed) : " + heapSize/unit + " MiB");
    out.println("• [Mem] Current Free memeory in Heap  : " + heapFreeSize/unit + " MiB");
    out.println("• [Mem] Currently used memory         : " + heapUsedSize/unit + " MiB");
    out.println("• [Mem] Max Heap Size (-Xmx)          : " + heapMaxSize/unit + " MiB");
  }
}
EOF

$ docker run \
        -it \
        --rm \
        --cpus=2 \
        --memory=512m \
        --pull always \
        --mount type=bind,source=$(pwd),destination=/app,readonly \
        --mount type=bind,source=/,destination=/host,readonly \
        --name openjdk \
        openjdk:25-slim \
        java \
        --source 25 --enable-preview \
        -XX:+UnlockExperimentalVMOptions \
        -XX:+UnlockDiagnosticVMOptions \
        -XX:+PrintFlagsFinal \
        -XX:MaxRAMPercentage=0.8 \
        -XX:-MaxFDLimit \
        -Xlog:gc \
        /app/App.java \
        | grep -e "Use.*GC" -e "Active" -e "Using" -e "Max.*Limit" -e "Container" -e ""

Note: For container logs, add -Xlog:gc*,os=trace,os+container=trace option. Override the default container CPU quota detection -XX:ActiveProcessorCount=<n>

JVM default GC

# OpenJDK reverts to Serial GC when it detects < 2 CPUs or < 2GB RAM
$ docker run -it --rm --cpus=1 --memory=1G openjdk:25-slim java -Xlog:gc --version
  #[0.007s][info][gc] Using Serial

Container Sandbox

$ docker run \
         -it --rm \
         --cpus="0.5" \
         --memory="512m" \
         --memory-swap="640m" \
         --pids-limit 512 \
         --cap-drop=ALL \
         --cap-add=DAC_OVERRIDE \
         --security-opt=no-new-privileges \
         --ulimit nproc=20:20 \
         --ulimit fsize=1000000 \
         --user nobody \
         --network none \
         --read-only \
         --tmpfs /tmp \
         --name sandboxed \
         --workdir /app \
         --mount type=bind,source=$PWD,destination=/app,readonly \
         --mount type=bind,source=/var/run/docker.sock,destination=/var/run/docker.sock,readonly \
         --mount type=bind,source=/usr/bin/docker,destination=/usr/bin/docker,readonly \
         --mount type=bind,source=/proc/,target=/host/proc/,ro=true \
         --mount type=bind,source=/sys/fs/cgroup/,target=/host/sys/fs/cgroup,ro=true \
         openjdk:25-slim java --enable-preview src/App.java
         
# --security-opt seccomp=my_sandbox
# --cap-add=chown \
# --cap-add=setgid \
# --cap-add=setuid \
# --cap-add=sys_chroot \
# --cap-add=audit_write \
# --cap-add=kill \
# --cap-add=sys_tty_config \
# --cap-add=DAC_OVERRIDE - Needed to allow overwriting the file

# Create nobody user
# RUN useradd --shell /bin/sh --create-home --uid 65534 nobody        

App Running on K8S/Docker

# Check if running on docker container
$ docker run \
     -it \
     --rm \
     --pull always \
     openjdk:25-slim \
     sh -c "cat /proc/self/cgroup | grep -i '/docker'"

# Check if running on Kubernets
$  docker run \
     -it \
     --rm \
     --pull always \
     openjdk:25-slim \
     sh -c "printenv | grep SERVICE"

Access Docker desktop LinuxKit VM on MacOS

$ docker run -it --rm --memory=256m --cpus=1 --mount type=bind,source=/,destination=/host --name alpine alpine
 /# chroot /host
  # docker version

Multi Architecture Support

  • tonistiigi/binfmt

    $ docker run --rm --privileged tonistiigi/binfmt --install all
  • multiarch/qemu-user-static

    $ docker run --rm --privileged multiarch/qemu-user-static --reset -p yes
  • Supported Targets - qemu-binfmt-conf.sh

  • Dockerhub Supported Archs

  • Examples

    $ docker run -it --rm --platform=linux/aarch64  alpine uname -m
      aarch64
    $ docker run -it --rm --platform=linux/amd64 alpine uname -m
      x86_64
    
    $ docker run --rm arm64v8/alpine uname -a
    $ docker run --rm arm32v7/alpine uname -a
    $ docker run --rm ppc64le/alpine uname -a
    $ docker run --rm s390x/alpine uname -a
    $ docker run --rm tonistiigi/debian:riscv uname -a

Netcat Webserver

FROM alpine

ENTRYPOINT while :; do nc -k -l -p $PORT -e sh -c 'echo -e "HTTP/1.1 200 OK\n\n hello, world"'; done
# https://github.com/jamesward/hello-netcat
# docker build -t hello-netcat .
# docker run -p 8080:80 -e PORT=80 -it hello-netcat

Forwards Logs

# forward request and error logs to docker log collector
RUN ln -sf /dev/stdout /var/log/nginx/access.log \
 && ln -sf /dev/stderr /var/log/nginx/error.log

# OR output directly to
/proc/self/fd/1 (STDOUT)
/proc/self/fd/2 (STDERR)

# https://docs.docker.com/config/containers/logging/configure/

Shutdown signals and EnytryPoint

#!/bin/bash
## Entrypoint script for my-app. This script is to show how to write
## an entrypoint script that actually passes down signals from Docker.

## Load our DB Password into a runtime only Environment Variable
if [ -f /run/secrets/password ]
then
  echo "Loading DB password from secrets file"
  DB_PASS=$(cat /run/secrets/password)
  export DB_PASS
fi

## Run the Application
exec my-app
ENTRYPOINT ["../../myapp-entrypoint.sh"]

HTTP Proxy

FROM debian:stable-slim

ENV HTTP_PROXY="http://proxy.test.com:8080"
ENV HTTPS_PROXY="http://proxy.test.com:8080"
ENV NO_PROXY="*.test1.com,*.test2.com,127.0.0.1,localhost"

Container Tools

  • https://github.com/rancher-sandbox/rancher-desktop
  • https://github.com/beringresearch/macpine (Lightweight Linux VMs on MacOS)
  • https://github.com/containerd/containerd
  • https://github.com/lima-vm/lima (Linux on Mac)
  • https://github.com/containerd/nerdctl
  • https://github.com/k3s-io/k3s
  • https://github.com/rancher/k3d/
  • https://kind.sigs.k8s.io/
  • https://github.com/k0sproject/k0s
  • https://github.com/canonical/multipass (Running Ubuntu VM)
  • https://podman.io/blogs/2021/09/06/podman-on-macs.html
  • https://github.com/wagoodman/dive
  • https://github.com/google/cadvisor
  • https://github.com/google/go-containerregistry/tree/main/cmd/crane
  • CoSign - Container Signing
Table of Contents