Skip to content

Commit 9bb3aad

Browse files
authored
Merge pull request #295 from graalvm/tiny-containers-multi-stage
Tiny containers multi stage
2 parents 228695d + 2b69cb8 commit 9bb3aad

25 files changed

+247
-253
lines changed

.github/workflows/tiny-java-containers.yml

Lines changed: 31 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -23,24 +23,12 @@ jobs:
2323
github-token: ${{ secrets.GITHUB_TOKEN }}
2424
- name: Run 'tiny-java-containers'
2525
run: |
26-
cd tiny-java-containers
2726
sleep_period=5
28-
#
29-
# Setup musl toolchain
30-
#
31-
./setup-musl.sh
32-
export PATH="$PWD/musl-toolchain/bin:$PATH"
33-
#
34-
# Download upx
35-
#
36-
./setup-upx.sh
37-
#
27+
cd tiny-java-containers
3828
# Hello World
3929
#
4030
cd helloworld
4131
./build.sh
42-
./hello
43-
./hello.upx
4432
docker run --rm hello:upx
4533
cd ..
4634
#
@@ -64,9 +52,12 @@ jobs:
6452
curl "http://localhost:8000"
6553
docker stop $container_id
6654
#
67-
# Static Scratch
55+
# Static
6856
#
6957
./build-static.sh
58+
#
59+
# Static Scratch
60+
#
7061
container_id=$(docker run -d --rm -p8000:8000 jwebserver:scratch.static)
7162
sleep $sleep_period
7263
curl "http://localhost:8000"
@@ -100,3 +91,29 @@ jobs:
10091
sleep $sleep_period
10192
curl "http://localhost:8000"
10293
docker stop $container_id
94+
#
95+
# JDK Build
96+
#
97+
./build-jvm.sh
98+
#
99+
# Debian JDK
100+
#
101+
container_id=$(docker run -d --rm -p8000:8000 jwebserver:debian)
102+
sleep $sleep_period
103+
curl "http://localhost:8000"
104+
docker stop $container_id
105+
#
106+
# Eclipse Temurin Static
107+
#
108+
container_id=$(docker run -d --rm -p8000:8000 jwebserver:temurin)
109+
sleep $sleep_period
110+
curl "http://localhost:8000"
111+
docker stop $container_id
112+
#
113+
# Distroless Java
114+
#
115+
./build-jlink.sh
116+
container_id=$(docker run -d --rm -p8000:8000 jwebserver:distroless-java)
117+
sleep $sleep_period
118+
curl "http://localhost:8000"
119+
docker stop $container_id

tiny-java-containers/README.md

Lines changed: 76 additions & 121 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# Tiny Java Containers
22

3-
This example shows how a simple Java application and a simple web
3+
This demo shows how a simple Java application and a simple web
44
server can be compiled to produce very small Docker container images.
55

66
The smallest container images contains just an executable. But since there's
@@ -13,128 +13,80 @@ To support static linking of `libc`, GraalVM Native Image supports using the
1313
implementation.
1414

1515
You can watch a [Devoxx 2022](https://devoxx.be/) session that walks through
16-
this example on YouTube.
16+
an earlier version of this example on YouTube.
1717

1818
[![A 1.5MB Java Container
1919
App](images/youtube.png)](https://youtu.be/6wYrAtngIVo)
2020

2121
## Prerequisites
2222

2323
* x86 Linux (but the few binary dependencies could easily be changed for aarch64)
24-
* Docker installed and running. It should work fine with [podman](https://podman.io/) but it has not been tested.
25-
* [GraalVM for JDK 21](https://www.graalvm.org/downloads/)
24+
* Docker installed and running. It should work fine with
25+
[podman](https://podman.io/) but it has not been tested.
2626

27-
> We recommend Oracle GraalVM for the best experience. It is licensed under the [GraalVM Free Terms and Conditions (GFTC)](https://www.oracle.com/downloads/licenses/graal-free-license.html) license, which permits use by any user including commercial and production use.
28-
GraalVM Community Edition for JDK 21 works too, but Native Image generated executables sizes will differ.
29-
30-
> These instructions have only been tested on Linux x64.
27+
> NOTE: These instructions have only been tested on Linux x64.
3128
3229
## Setup
3330

34-
You need the following zlib packages installed:
35-
* zlib.x86_64
36-
* zlib-devel.x86_64
37-
* zlib-static.x86_64
38-
39-
On Oracle Linux, you can install with:
40-
```sh
41-
sudo yum install -y zlib.x86_64
42-
sudo yum install -y zlib-devel.x86_64
43-
sudo yum install -y zlib-static.x86_64
44-
```
45-
46-
Clone this Git repo and in your Linux shell type the following to download and
47-
configure the `musl` toolchain.
48-
49-
![](images/keyboard.jpg) `./setup-musl.sh`
50-
51-
Download [upx](https://upx.github.io/):
52-
53-
![](images/keyboard.jpg) `./setup-upx.sh`
31+
Clone this Git repo. Everything runs in Docker so no need to install anything
32+
on your machine.
5433

5534
## Hello World
5635

57-
With the `musl` toolchain installed, cd in to the `helloworld` folder.
36+
Let's start with a simple Hello World example.
5837

59-
![](images/keyboard.jpg) `cd helloworld`
38+
Change the directory to `helloworld`.
6039

61-
Using the `build.sh` script, compile a simple single Java class Hello World
62-
application with `javac`, compile the generated .class file into a fully
63-
statically linked native Linux executable named `hello`, compress the executable
64-
with [upx](https://upx.github.io/) to create the executable `hello.upx`, and
65-
package the compressed static `hello.upx` executable into a `scratch`
66-
Docker container image:
40+
![](images/keyboard.jpg) `cd helloworld`
6741

68-
![](images/keyboard.jpg) `./build.sh`
42+
Use the `build.sh` script to run a Docker build that:
43+
1. compiles a simple single Java class Hello World application with `javac`
44+
2. compiles the generated .class file with GraalVM Native Image into a fully
45+
statically linked native Linux executable named `hello`
46+
3. compresses the executable with [upx](https://upx.github.io/) to create the
47+
executable `hello.upx`
48+
4. packages the compressed static `hello.upx` executable into a `scratch` Docker
49+
container image
6950

70-
You'll see two executables were built:
51+
In a terminal, run:
7152

72-
![](images/keyboard.jpg) `ls -lh hello*`
53+
![](images/keyboard.jpg) `./build.sh`
7354

7455
### Native Executables
7556

76-
Running either of the `hello` executables you can see they are functionally
77-
equivalent. They just print "Hello World". But there are a few points worth
78-
noting:
79-
80-
1. The executable generated by GraalVM Native Image using the
81-
`--static --libc=musl` options is a fully self-contained executable which can be
82-
confirmed by examining it with `ldd`:
83-
84-
![](images/keyboard.jpg) `ldd hello`
85-
86-
should result in:
87-
88-
```shell
89-
not a dynamic executable
90-
```
91-
92-
This means that it does not rely on any libraries in the host operating system
93-
environment making it easier to package in a variety of Docker container images.
94-
95-
Unfortunately `upx` compression renders `ldd` unable to list the shared
96-
libraries of an executable, but since you compressed the statically linked
97-
executable, you can be confident it is also statically linked.
98-
99-
2. Both executables are the result of compiling a Java bytecode application into
100-
native machine code. The uncompressed executable is only ~6.3MB! There's no
101-
JVM, no JARs, no JIT compiler and none of the overhead it imposes. Both
102-
start extremely fast as there is minimal startup cost.
103-
104-
3. The `upx` compressed executable is over 70% smaller, 1.7MB vs. 6.3MB! With
105-
`upx` the application self-extracts quickly but does incur a cost of about
106-
100ms for decompression. See this blog for a deep dive on [GraalVM Native
107-
Image and
57+
1. The `hello` executable generated by GraalVM Native Image in the Dockerfile
58+
using the `--static --libc=musl` options is a fully self-contained
59+
executable. This means that it does not rely on any libraries in the host
60+
operating system environment. This makes it easier to package in a variety of
61+
container images.
62+
63+
2. You can see in the output of the Dockerfile build that `ls -lh` reports the
64+
`hello` executable is ~4.9MB. There's no JVM, no JARs, no JIT compiler and
65+
none of the overhead it imposes. It starts extremely fast as there is minimal
66+
startup cost.
67+
68+
3. The `upx` compressed `hello.upx` executable is over 70% smaller, 1.3MB vs.
69+
4.9MB! A `upx` compressed application self-extracts quickly but does incur a
70+
cost of about 100ms for decompression. See this blog for a deep dive on
71+
[GraalVM Native Image and
10872
UPX](https://medium.com/graalvm/compressed-graalvm-native-images-4d233766a214).
10973

11074
### Container Images
11175

112-
The size of the `scratch`-based container image is slightly more than the `hello.upx`
113-
executable.
76+
The size of the `scratch`-based container image is about the same size as the
77+
`hello.upx` executable since it adds little overhead.
11478

11579
![](images/keyboard.jpg) `docker images hello`
11680

11781
```shell
11882
REPOSITORY TAG IMAGE ID CREATED SIZE
119-
hello upx 4d122bd39a8a About a minute ago 1.78 MB
83+
hello upx b69a5d79e8dc 1 second ago 1.3MB
12084
```
12185

12286
This is a tiny container image and yet it contains a fully functional and
123-
deployable (although fairly useless 😉) application. The Dockerfile that
124-
generated it simply copies the executable into the container image and sets the
125-
executable as the `ENTRYPOINT`.
126-
127-
A better way to build these images is with a multi-stage build, but to keep the
128-
focus on the final result, build on a host machine and copy the binary into
129-
the container image. E.g.,
130-
131-
```docker
132-
FROM scratch
133-
COPY hello.upx /
134-
ENTRYPOINT ["/hello.upx"]
135-
```
87+
deployable (although fairly useless 😉) application.
13688

137-
Running the container image is straight forward:
89+
Running the executable in the container image is straight forward:
13890

13991
![](images/keyboard.jpg) `docker run --rm hello:upx`
14092

@@ -153,7 +105,7 @@ introduced in JDK 18 and build a containerized executable that serves up web
153105
pages.
154106

155107
How small can a containerized Java web server be? Would you believe a measly
156-
5.5MB? Let's see.
108+
3.9 MB? Let's see.
157109

158110
Let's move from the `helloworld` folder over to the `jwebserver` folder.
159111

@@ -171,62 +123,65 @@ custome runtime image for comparison.
171123

172124
![](images/keyboard.jpg) `./build-all.sh`
173125

174-
The various Dockerfiles simply copy the executable or `jlink` generated custom
175-
runtime image folder into the container image along with an `index.html` file to
176-
serve, and set the `ENTRYPOINT`. E.g.,
126+
The various Dockerfiles simply copy the compiled executable or `jlink` generated
127+
custom runtime image folder into the deployment container image along with an
128+
`index.html` file to serve, and set the `ENTRYPOINT`.
177129

178-
```docker
179-
FROM scratch
180-
COPY jwebserver.static /
181-
COPY index.html /web/index.html
182-
ENTRYPOINT ["/jwebserver.static", "-b", "0.0.0.0", "-d", "/web"]
183-
```
130+
The Distroless Java, Eclipse Temurin, and Debian container images include a full
131+
JDK, which includes jwebserver.
184132

185-
When complete you can see the sizes of the various versions:
133+
When complete you can see the sizes of the various variants:
186134

187135
![](images/keyboard.jpg) `$ docker images jwebserver`
188136

189137
```shell
190138
REPOSITORY TAG IMAGE ID CREATED SIZE
191-
jwebserver distroless-java-base.jlink 414d84f8b7c7 22 minutes ago 132 MB
192-
jwebserver scratch.static-upx 47aabdd14c04 22 minutes ago 4.71 MB
193-
jwebserver alpine.static 783ab3a60248 22 minutes ago 23.4 MB
194-
jwebserver distroless-static.static c894f14d4068 22 minutes ago 18.7 MB
195-
jwebserver scratch.static 034cfbdf3577 22 minutes ago 15.7 MB
196-
jwebserver distroless-base.mostly e99811e574d3 22 minutes ago 37.6 MB
197-
jwebserver distroless-java-base.dynamic 72a210e3c705 23 minutes ago 50.6 MB
139+
jwebserver distroless-java de7f7efb6df4 4 minutes ago 192MB
140+
jwebserver temurin 643203bf8168 4 minutes ago 451MB
141+
jwebserver debian fa5bfa4b2e5e 4 minutes ago 932MB
142+
jwebserver distroless-java-base.jlink c3113c2400ea 5 minutes ago 122MB
143+
jwebserver scratch.static-upx 75b3bb3249f3 5 minutes ago 3.9MB
144+
jwebserver alpine.static 178081760470 6 minutes ago 21.6MB
145+
jwebserver distroless-static.static 84053f6323c1 6 minutes ago 15.8MB
146+
jwebserver scratch.static 98061f48037c 6 minutes ago 13.8MB
147+
jwebserver distroless-base.mostly b33fc99fbe2a 7 minutes ago 34.3MB
148+
jwebserver distroless-java-base.dynamic 1aceeabbb329 7 minutes ago 46.9MB
198149
```
199150

200151
Sorting by size, it's clear that the fully statically linked GraalVM Native
201152
Image generated executable that's compressed and packaged on `scratch`
202-
(`scratch.static-upx`) is the smallest at just 4.71MB, less than 4% of the size
153+
(`scratch.static-upx`) is the smallest at just 3.9 MB, less than 4% of the size
203154
of the `jlink` version (`distroless-java-base.jlink`) running on the JVM.
204155

205156
| Base Image | App Version | Size (MB) |
206157
| -------------------- | ---------------------------------- | --------- |
207-
| Distroless Java Base | jlink | 132.00 |
208-
| Distroless Java Base | native *dynamic* linked | 50.60 |
209-
| Distroless Base | native *mostly* static linked | 37.60 |
210-
| Alpine | native *fully* static linked | 23.40 |
211-
| Distroless Static | native *fully* static linked | 18.70 |
212-
| Scratch | native *fully* static linked | 15.70 |
213-
| Scratch | *compressed* native *fully* static | 4.71 |
214-
215-
Running a container image is straight forward, just remember to map the ports, e.g.:
216-
217-
![](images/keyboard.jpg) `docker run --rm -p8000:8000 jwebserver:scratch.static`
158+
| Debian Slim + JDK | jwebserver included in the JDK | 932.00 |
159+
| Eclipse Temurin | jwebserver included in the JDK | 451.00 |
160+
| Distroless Java | jwebserver included in the JDK | 192.00 |
161+
| Distroless Java Base | jlink | 122.00 |
162+
| Distroless Java Base | native *dynamic* linked | 46.90 |
163+
| Distroless Base | native *mostly* static linked | 34.30 |
164+
| Alpine | native *fully* static linked | 21.60 |
165+
| Distroless Static | native *fully* static linked | 15.80 |
166+
| Scratch | native *fully* static linked | 13.80 |
167+
| Scratch | *compressed* native *fully* static | 3.90 |
168+
169+
Running a container image is once again straight forward, just remember to map
170+
the server port, e.g.:
171+
172+
![](images/keyboard.jpg) `docker run --init --rm -p8000:8000 jwebserver:scratch.static`
218173

219174
or
220175

221-
![](images/keyboard.jpg) `docker run --rm -p8000:8000 jwebserver:scratch.static-upx`
176+
![](images/keyboard.jpg) `docker run --init --rm -p8000:8000 jwebserver:scratch.static-upx`
222177

223178
Using `curl` or your favourite tool you can hit `http://localhost:8000` to fetch
224179
the index.html file.
225180

226181
## Wrapping Up
227182

228-
A fully functional, albeit minimal, Java "microservice" was compiled
229-
into a native Linux executable and packaged into Distroless, Alpine, and
183+
A fully functional, albeit minimal, Java "microservice" was compiled into a
184+
native Linux executable and packaged into Distroless, Alpine, and
230185
`scratch`-based container images thanks to GraalVM Native Image's support for
231186
various linking options including fully static linking with the `musl` libc.
232187

tiny-java-containers/clean.sh

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@
22

33
set +e
44

5-
rm -rf x86_64-linux-musl-native zlib-*
65
cd helloworld
76
./clean.sh || true
87
cd ../jwebserver
Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,29 @@
1+
2+
# Build in a container with Oracle GraalVM Native Image and MUSL
3+
FROM container-registry.oracle.com/graalvm/native-image:23-muslib AS nativebuild
4+
WORKDIR /build
5+
# Install UPX
6+
ARG UPX_VERSION=4.2.2
7+
ARG UPX_ARCHIVE=upx-${UPX_VERSION}-amd64_linux.tar.xz
8+
RUN microdnf -y install wget xz && \
9+
wget -q https://github.com/upx/upx/releases/download/v${UPX_VERSION}/${UPX_ARCHIVE} && \
10+
tar -xJf ${UPX_ARCHIVE} && \
11+
rm -rf ${UPX_ARCHIVE} && \
12+
mv upx-${UPX_VERSION}-amd64_linux/upx . && \
13+
rm -rf upx-${UPX_VERSION}-amd64_linux
14+
15+
# Compile the Hello class to Java bytecode
16+
COPY Hello.java Hello.java
17+
RUN javac Hello.java
18+
# Build a native executable with native-image
19+
RUN native-image -Os --static --libc=musl Hello -o hello
20+
RUN ls -lh hello
21+
22+
# Compress the executable with UPX
23+
RUN ./upx --lzma --best -o hello.upx hello
24+
RUN ls -lh hello.upx
25+
26+
# Copy the compressed executable into a scratch container
127
FROM scratch
2-
COPY hello.upx /
28+
COPY --from=nativebuild /build/hello.upx /hello.upx
329
ENTRYPOINT ["/hello.upx"]

0 commit comments

Comments
 (0)