Skip to content
Open
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
18 changes: 18 additions & 0 deletions httpserver-java17-springboot-distroless/DemoApplication.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package com.example.demo;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

@SpringBootApplication
@RestController
public class DemoApplication {
public static void main(String[] args) {
SpringApplication.run(DemoApplication.class, args);
}
@GetMapping("/hello")
public String hello(@RequestParam(value = "name", defaultValue = "World") String name) {
return String.format("Hello, World!");
}
}
41 changes: 41 additions & 0 deletions httpserver-java17-springboot-distroless/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
FROM --platform=linux/x86_64 debian:bookworm AS build

RUN set -xe ; \
apt -yqq update ; \
apt -yqq install default-jre ; \
apt -yqq install default-jdk; \
apt -yqq install curl zip \
;

RUN ldconfig /usr/lib/jvm/java-17-openjdk-amd64/lib/

WORKDIR /src

ARG BOOT_VERSION=4.1.0

RUN set -xe ; \
curl -G https://start.spring.io/starter.zip \
-d applicationName=DemoApplication \
-d artifactId=demo \
-d bootVersion=${BOOT_VERSION} \
-d dependencies=web \
-d description=com.example \
-d javaVersion=17 \
-d language=java \
-d name=demo \
-d packageName=com.example.demo \
-d packaging=jar \
-d type=maven-project \
-d version=0.0.1-SNAPSHOT \
-o demo.zip ; \
unzip demo.zip \
;

COPY DemoApplication.java src/main/java/com/example/demo/

RUN set -xe ; \
./mvnw compile package install

FROM gcr.io/distroless/java17-debian13

COPY --from=build /src/target/demo-0.0.1-SNAPSHOT.jar /usr/src/demo-0.0.1-SNAPSHOT.jar
12 changes: 12 additions & 0 deletions httpserver-java17-springboot-distroless/Kraftfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
spec: v0.7

runtime: base-compat:latest

targets:
- kraftcloud/x86_64

rootfs:
source: ./Dockerfile
format: erofs

cmd: ["/usr/bin/java", "-jar", "/usr/src/demo-0.0.1-SNAPSHOT.jar"]
238 changes: 238 additions & 0 deletions httpserver-java17-springboot-distroless/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,238 @@
# Spring Boot HTTP Server (Distroless)

This guide explains how to create and deploy a Spring Boot web server, using a [distroless](https://github.com/GoogleContainerTools/distroless) base image (`gcr.io/distroless/java17-debian13`) for the runtime stage instead of a manually assembled `scratch` image.

This example is a variant of [`httpserver-java17-springboot`](../httpserver-java17-springboot).

To run this example, follow these steps:

1. Install the CLI.
Use the [unikraft CLI](https://unikraft.com/docs/cli/unikraft) or the legacy [kraft CLI](https://unikraft.org/docs/cli/install).
You need a [BuildKit](https://github.com/moby/buildkit) builder. The easiest way to get one is via [Docker](https://docs.docker.com/engine/install/).
Alternatively, you can also directly set up and use BuildKit, see the [quick start](https://github.com/moby/buildkit#quick-start).

> **Note**:
> The unikraft CLI is the current standard, while kraft is the legacy version.
> Choose one of the CLIs below and only run the commands associated with it for the rest of this guide.

2. Clone the [`examples` repository](https://github.com/unikraft-cloud/examples) and `cd` into the `examples/httpserver-java17-springboot-distroless/` directory:

```bash
git clone https://github.com/unikraft-cloud/examples
cd examples/httpserver-java17-springboot-distroless/
```

Make sure to log into Unikraft Cloud and pick a [metro](https://unikraft.com/docs/platform/metros) close to you.
This guide uses `fra` (Frankfurt, 🇩🇪):

**Using the unikraft CLI (Recommended)**
```bash title="unikraft"
unikraft login
```

or

**Using the legacy kraft CLI**
```bash title="kraft"
# Set Unikraft Cloud access token
export UKC_TOKEN=token
# Set metro to Frankfurt, DE
export UKC_METRO=fra
```

When done, invoke the following command to deploy this app on Unikraft Cloud:

**Using the unikraft CLI (Recommended)**
```bash title="unikraft"
unikraft build . --output <my-org>/httpserver-java17-springboot-distroless:latest
unikraft run --metro fra \
-m 1G \
-p 443:8080/tls+http \
--scale-to-zero policy=idle,cooldown-time=1000,stateful=true \
--image <my-org>/httpserver-java17-springboot-distroless:latest
```

or

**Using the legacy kraft CLI**
```bash title="kraft"
kraft cloud deploy \
-M 1Gi \
-p 443:8080/tls+http \
--scale-to-zero idle \
--scale-to-zero-stateful \
--scale-to-zero-cooldown 1s \
.
```

The output shows the instance address and other details:

**Using the unikraft CLI (Recommended)**
```ansi title="unikraft"
metro: fra
name: httpserver-java17-springboot-distroless-qseeo
uuid: b081166d-a2a0-43af-982d-1aa17f06b5c4
state: starting
image: <my-org>/httpserver-java17-springboot-distroless
resources:
memory: 1024MiB
vcpus: 1
service:
uuid: 04f5b3ce-0aae-a50a-9d58-e6fa618b2cdc
name: long-dust-si7xsngk
domains:
- fqdn: long-dust-si7xsngk.fra.unikraft.app
networks:
- uuid: 4bde3abf-faf8-3a14-ceb9-bc05e646dfac
private-ip: 10.0.6.2
mac: 12:b0:eb:ad:60:a2
timestamps:
created: just now
```

or

**Using the legacy kraft CLI**
```ansi title="kraft"
[●] Deployed successfully!
├───────── name: httpserver-java17-springboot-distroless-qseeo
├───────── uuid: b081166d-a2a0-43af-982d-1aa17f06b5c4
├──────── metro: https://api.fra.unikraft.cloud/v1
├──────── state: starting
├─────── domain: https://long-dust-si7xsngk.fra.unikraft.app
├──────── image: oci://unikraft.io/<my-org>/httpserver-java17-springboot-distroless@sha256:cc2f2ad18ce8e36b8e8f4debee096fef7b0bb8b47762575a2ba5a9de8199c64a
├─────── memory: 1024 MiB
├────── service: long-dust-si7xsngk
├─ private fqdn: httpserver-java17-springboot-distroless-qseeo.internal
└─── private ip: 10.0.6.2
```

In this case, the instance name is `httpserver-java17-springboot-distroless-qseeo` and the address is `https://long-dust-si7xsngk.fra.unikraft.app`.
They're different for each run.

Use `curl` to query the Spring Boot server's `hello` endpoint:

```bash
curl https://long-dust-si7xsngk.fra.unikraft.app/hello
```

```text
Hello World!
```

When done, you can remove the instance:

**Using the unikraft CLI (Recommended)**
```bash title="unikraft"
unikraft instances delete httpserver-java17-springboot-distroless-qseeo
```

or

**Using the legacy kraft CLI**
```bash title="kraft"
kraft cloud instance remove httpserver-java17-springboot-distroless-qseeo
```

## Customize your app

To customize the app, update the files in the repository, listed below:

* `DemoApplication.java`: the server
* `Kraftfile`: the Unikraft Cloud specification
* `Dockerfile`: the Docker-specified app filesystem

Lines in the `Kraftfile` have the following roles:

* `spec: v0.7`: The current `Kraftfile` specification version is `0.7`.

* `runtime: base-compat:latest`: The runtime kernel to use is the base compatibility kernel.

* `rootfs`: Build the app root filesystem.
`source: ./Dockerfile` means the filesystem is built using the `Dockerfile`.
`format: erofs` means the filesystem type is [EROFS](https://erofs.docs.kernel.org/).

* `cmd: ["/usr/bin/java", "-jar", "/usr/src/demo-0.0.1-SNAPSHOT.jar"]`: Use the Java runtime to run the Spring Boot JAR as the starting command of the instance.

Lines in the `Dockerfile` have the following roles:

* `FROM --platform=linux/x86_64 debian:bookworm AS build`: Build the app using Debian Bookworm, installing the JDK and Maven tooling needed to generate and compile the Spring Boot project.

* `RUN curl -G https://start.spring.io/starter.zip ...`: Generate the Spring Boot project skeleton via Spring Initializr.

* `COPY DemoApplication.java src/main/java/com/example/demo/`: Copy the server implementation file into the generated project.

* `RUN ./mvnw compile package install`: Build the Spring Boot JAR.

* `FROM gcr.io/distroless/java17-debian13`: Build the runtime filesystem from Google's [distroless](https://github.com/GoogleContainerTools/distroless) Java 17 base image, which already contains a minimal JRE and its required system libraries, instead of manually copying individual files into a `scratch` image.

* `COPY --from=build /src/target/demo-0.0.1-SNAPSHOT.jar /usr/src/demo-0.0.1-SNAPSHOT.jar`: Copy the compiled JAR into the app filesystem.

The following options are available for customizing the app:

* If you only update the implementation in the `DemoApplication.java` source file, you don't need to make any other changes.

* If you create any new source files, copy them into the app filesystem by using the `COPY` command in the `Dockerfile`.

* More extensive changes may require extending the `Dockerfile` ([see `Dockerfile` syntax reference](https://docs.docker.com/engine/reference/builder/)).

You can list information about the instance by running:

**Using the unikraft CLI (Recommended)**
```bash title="unikraft"
unikraft instances list
```

```ansi title="unikraft"
METRO NAME STATE IMAGE ARGS MEMORY VCPUS FQDN CREATED
fra httpserver-java17-springboot-distroless-qseeo running <my-org>/httpserver-java17-springboot-distroless 1024MiB 1 long-dust-si7xsngk.fra.unikraft.app 2 minutes ago
```

or

**Using the legacy kraft CLI**
```bash title="kraft"
kraft cloud instance list
```

```ansi title="kraft"
NAME FQDN STATE STATUS IMAGE MEMORY VCPUS ARGS BOOT TIME
httpserver-java17-springboot-distroless-qseeo long-dust-si7xsngk.fra.unikraft.app running 1 minute ago oci://unikraft.io/<my-org>/httpserver-java17-springboot-distroless@sha256:... 1.0 GiB 1 421.30 ms
```

When done, you can remove the instance:

**Using the unikraft CLI (Recommended)**
```bash title="unikraft"
unikraft instances delete httpserver-java17-springboot-distroless-qseeo
```

or

**Using the legacy kraft CLI**
```bash title="kraft"
kraft cloud instance remove httpserver-java17-springboot-distroless-qseeo
```

## Learn more

- [Spring Boot Documentation](https://docs.spring.io/spring-boot/index.html)
- [Unikraft Cloud's Documentation](https://unikraft.cloud/docs/)
- [Distroless Images](https://github.com/GoogleContainerTools/distroless)

Use the `--help` option for detailed information on using Unikraft Cloud:

**Using the unikraft CLI (Recommended)**
```bash title="unikraft"
unikraft --help
```

or

**Using the legacy kraft CLI**
```bash title="kraft"
kraft cloud --help
```

Or visit the [CLI Reference](https://unikraft.com/docs/cli/unikraft) or the [legacy CLI Reference](https://unikraft.com/docs/cli/kraft/overview).
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
"""End-to-end test for the ``httpserver-java17-springboot`` example.

Mirrors the manual steps from ``httpserver-java17-springboot/README.md``:

1. ``unikraft build . --output <prefix>/httpserver-java17-springboot:<tag>``
2. ``unikraft run --metro <metro> -p 443:8080/tls+http -m 1G --image ...``
3. ``curl https://<instance-url>/hello`` and assert "Hello, World!".
"""

from __future__ import annotations

from _testlib.unikraft import extract_instance_name, extract_instance_url


def test_springboot_serves_hello(build_image, run_instance, http, wait_instance):
image = build_image("httpserver-java17-springboot-distroless", "httpserver-java17-springboot-distroless")

instance = run_instance(
image,
publish=["443:8080/tls+http"],
memory="1G",
)

url = extract_instance_url(instance)
assert url, f"could not determine instance URL from: {instance!r}"

wait_instance(extract_instance_name(instance), "running")

resp = http(f"{url}/hello")
assert resp.status_code == 200
assert "Hello, World!" in resp.text
Loading