DEV Community

Cover image for From Bloat to Bare Metal: How to Run Java on a scratch Container (and Why You Should)
unni mana
unni mana

Posted on

From Bloat to Bare Metal: How to Run Java on a scratch Container (and Why You Should)

Ditch the bloated OS layers. Here’s how to build a Java container that is smaller, faster, and more secure by starting with literally nothing.

In the world of cloud computing, the "thinner" your container, the better. Most developers are used to starting their Dockerfiles with a full operating system like Ubuntu or a language-specific runtime like openjdk:latest. This is easy, but it’s also incredibly wasteful. The video "How Scratch Images Run Java With Zero OS" breaks down a revolutionary approach to containerization: using the scratch image to run a Java application.

What Exactly is a scratch Image?

First, let's clear up a common misconception. scratch isn't a minimal operating system; it's the absence of one. In a Dockerfile, FROM scratch is a reserved instruction that means "start with an empty filesystem" .

A scratch container has:

No shell (no /bin/bash or /bin/sh)

No package manager (no apt, yum, or apk)

No OS kernel or standard libraries (like glibc)

Literally nothing except the files you specifically copy into it 
Enter fullscreen mode Exit fullscreen mode

Think of it as a blank slate. This radical minimalism is what makes scratch images the most secure and lightweight option available . According to one analysis, a standard Linux distribution layer can be over 70 MB, whereas a statically built container can be just a few megabytes.

The Challenge: Java Needs a JVM

Java applications typically rely on the Java Virtual Machine (JVM), which is a piece of software that expects certain libraries (like libc) to be present on the operating system . This is why you can't just drop a standard .jar file into a scratch container and expect it to run. The JVM needs an OS to interact with.

So how does the video and the broader container community get around this? The answer lies in GraalVM Native Image.

The Solution: Turning Java into a Static Binary

To run Java on scratch, you need to eliminate the JVM's dependency on the host OS. This is achieved by compiling your Java code into a native, statically linked executable .

The Magic of GraalVM Native Image

GraalVM is a high-performance JDK designed to compile Java applications ahead-of-time (AOT). The native-image tool takes your Java bytecode and compiles it into a platform-specific binary. This binary is self-contained and can run without a pre-installed JVM .

Build a Static Native Executable

To make this work with scratch, the binary must be statically linked. A dynamically linked binary would look for external libraries on the system (which don't exist in a scratch container). With static linking, all the necessary code (including parts of the JVM and standard libraries like libc) is bundled directly into the final binary .

GraalVM provides a --static option to build this type of executable. Often, it's done using musl-libc, a lightweight alternative to the standard glibc, to ensure the resulting binary has no dependencies on the host system .

`# This is a simplified Dockerfile using a multi-stage build

1. Build stage: Use a full JDK to compile the app and create a native image

FROM oracle/graalvm-ce:latest as builder
WORKDIR /app
COPY . .
RUN native-image --static --libc=musl -jar my-app.jar my-app

2. Final stage: Copy the static binary into a scratch image

FROM scratch

Copy the statically compiled binary from the builder stage

COPY --from=builder /app/my-app /my-app

Define the entrypoint to run the binary

ENTRYPOINT ["/my-app"]`

This multi-stage build pattern is crucial. It allows you to use a massive build image to compile the application, then only copy the resulting binary into the empty scratch image .

Why Bother? The Benefits of Going to scratch

Use scratch for: Go, Rust, and applications compiled with GraalVM Native Image into a fully static binary. It's ideal for simple microservices and CLI tools that are self-contained .

Avoid scratch for: Traditional Java applications that you run with a standard java -jar command, as they still need the JVM. Also, avoid it for any application that needs to shell out, run scripts, or expects a standard filesystem like /etc or /lib . For those cases, you can consider distroless images, which include a runtime (like a JRE) but no shell, offering a middle ground .

Conclusion

Running Java on a scratch container is a perfect example of modern DevOps optimization. It proves that with the right tools (like GraalVM), we can cut away decades of OS bloat and run applications on their absolute bare essentials. This isn't just a technical novelty; it's a practical path towards building a more secure, efficient, and cost-effective cloud-native infrastructure.

To understand it quickly, check the following video

Order Practical Quarkus: Zero to native

Top comments (0)