Choosing Java Base Images, JDK vs JRE, and Enabling Graceful Shutdown in Docker
This article explains how to select a suitable Java base image (Alpine or Debian), decide between JDK and JRE, choose Oracle or OpenJDK, pick an appropriate JVM, configure signal handling for graceful shutdown, manage container memory limits, control DNS caching, and optionally compile native binaries with GraalVM.
System Selection
For the most basic underlying image there are three common choices: Alpine, Debian, and CentOS. CentOS is familiar to many operators but is no longer maintained, so the practical options are Alpine and Debian. Alpine is smaller but uses musl libc, which can cause compatibility issues for applications that heavily depend on glibc. If your application has deep glibc dependencies (e.g., JNI code), Debian‑based images are safer; otherwise Alpine is fine for size‑critical deployments.
JDK or JRE
Most people do not distinguish JDK from JRE. The JDK (Java Development Kit) includes development tools such as javac, jps, jstack, and jmap, and it bundles the JRE. The JRE (Java Runtime Environment) contains only the runtime components needed to run Java programs, making it smaller and lighter.
If you only need to run a JAR, the JRE is sufficient; if you need debugging or compilation tools at runtime, use the JDK. The author prefers using the JDK as the base image for production troubleshooting, unless image size is a strict concern.
JDK Selection
Oracle JDK vs OpenJDK
The choice depends on whether your code uses Oracle‑specific private APIs (e.g., classes under com.sun.*). If such APIs are used, you must use Oracle JDK; otherwise OpenJDK works fine. Unused private imports can be removed or replaced with alternatives such as Apache Commons.
IDE shortcuts: Option+Command+L formats code, and Control+Option+O optimizes imports.
Oracle JDK Rebuild Issue
Oracle JDK does not provide historical versions, so keep the downloaded archive if you need to rebuild the image later.
OpenJDK Distributions
Popular OpenJDK‑based images include AdoptOpenJDK (now Eclipse Adoptium, image name eclipse-temurin), Amazon Corretto, IBM Semeru Runtime, Azul Zulu, and Liberica JDK. AdoptOpenJDK is community‑driven and often provides Alpine, Ubuntu, and CentOS variants.
JVM Selection
The JVM implementation must comply with the Java specification. Common JVMs are HotSpot, OpenJ9, TaobaoVM, LiquidVM, and Azul Zing. HotSpot offers the best overall performance and compatibility; OpenJ9 is more container‑friendly with faster startup and lower memory usage. For most users, HotSpot is recommended; for advanced tuning, consider OpenJ9.
Signal Transmission
When a container is stopped, the orchestrator sends a termination signal to PID 1. If the Java process receives the signal, frameworks like Spring Boot can perform graceful shutdown. Incorrect signal handling (e.g., using a bash entrypoint) can cause the container to be force‑killed, leaving resources unreleased.
Examples of Dockerfiles:
Dockerfile.direct runs java -jar … directly via CMD and correctly forwards signals.
Dockerfile.exec uses a script that performs exec java -jar … to preserve signal forwarding.
Dockerfile.bash‑c runs bash -c "java -jar …"; works for simple commands but may fail with pipelines or redirects.
Dockerfile.tini adds tini or dumb‑init, which handle zombie processes but do not guarantee graceful shutdown for the Java child process.
Best Practices
Include tini or dumb‑init to reap zombies.
Use a simple CMD with the Java command for reliable signal forwarding.
When using scripts, invoke exec to replace the shell with the Java process.
Use bash -c only for straightforward commands and test its behavior.
Memory Limits
Containerized Java applications need to detect the memory limit set by cgroups. Different OpenJDK versions behave differently:
OpenJDK 8u111: no container support.
OpenJDK 8u131: adds -XX:+UseCGroupMemoryLimitForHeap but not effective by default.
OpenJDK 8u222: adds -XX:+UseContainerSupport (back‑ported) but still ineffective without flags.
OpenJDK 11.0.15: enables -XX:+UseContainerSupport by default, yet does not adapt memory automatically.
OpenJDK 11.0.16 and later (including 17): automatically adapt heap size to the container limit.
With cgroups v2, automatic memory adaptation is only supported from OpenJDK 11.0.16 onward.
DNS Cache
JVM DNS caching is controlled by -Dsun.net.inetaddr.ttl. By default the TTL is 30 seconds; with a Security Manager it becomes “forever” (‑1). To avoid unexpected caching, set the TTL explicitly. Tools like Alibaba’s DCM can help debug DNS cache behavior.
Native Compilation
GraalVM can compile Java applications to native binaries, dramatically improving startup time. To build a native image, set JAVA_HOME and PATH to GraalVM, then run mvn clean package -Dmaven.test.skip=true -Pnative. The resulting binary runs much faster, but the approach is still immature for legacy projects.
References
eclipse‑temurin: https://hub.docker.com/_/eclipse-temurin
ibm‑semeru‑runtimes: https://hub.docker.com/_/ibm-semeru-runtimes
GitHub project: https://github.com/mritd/SpringBootGracefulShutdownExample
StackExchange discussion: https://unix.stackexchange.com/questions/466496/...
JDK‑8230305: https://bugs.openjdk.org/browse/JDK-8230305
DNS cache script source: https://gist.github.com/andystanton/958a9a87f5b5a4eae537f96f896a19bc
Alibaba DCM: https://github.com/alibaba/java-dns-cache-manipulator
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
Open Source Linux
Focused on sharing Linux/Unix content, covering fundamentals, system development, network programming, automation/operations, cloud computing, and related professional knowledge.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
