CI/CD for a Quarkus Native Image with GitHub Actions
Test on the JVM, compile a GraalVM native binary, and ship a tiny container.
Quarkus compiles to a GraalVM native image for fast startup and low memory. This recipe runs JVM tests first, then builds the native binary and a minimal container.
What the pipeline does
- set up a GraalVM JDK
- run JVM unit tests
- build the native image with -Pnative
- run native integration tests
- build and push a micro container
The workflow
graalvm/setup-graalvm provides native-image. The native profile compiles ahead of time; in-container native builds avoid a local GraalVM but are slower, so a native GraalVM build is used here.
name: CI
on:
push:
branches: [main]
pull_request:
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: graalvm/setup-graalvm@v1
with:
java-version: 21
distribution: graalvm
cache: maven
- run: ./mvnw -B test
- run: ./mvnw -B package -Pnative -DskipTests
- run: ./mvnw -B verify -Pnative
- name: Build native container
run: docker build -f src/main/docker/Dockerfile.native -t ghcr.io/${{ github.repository }}:${{ github.sha }} .Caching and speed
cache: maven restores ~/.m2. Native compilation is CPU and memory intensive and can take minutes, so this is exactly the step where cheaper managed runners such as Latchkey $0.0025/min at 2 vCPU against $0.006 GitHub-hosted pay off, and auto-retry covers transient GraalVM download or OOM blips on a retry.
Deploying
The native binary runs in a tiny distroless or UBI-micro image, so cold starts are milliseconds. Push to your registry and deploy to Kubernetes, Cloud Run, or Knative where fast scale-to-zero matters.
Making this reliable in CI
- Pin every tool version. An unpinned toolchain turns a runner image update into a build break on unchanged code.
- Cache what is expensive to produce, and key the cache to the tool version so a restore across a version boundary cannot poison the build.
- Assert on the produced artifact rather than on the command succeeding.
- Set an explicit timeout so a hung step fails fast instead of consuming the whole job budget.
Key takeaways
- Run JVM tests first; native builds are slow, so fail fast on the JVM.
- Use graalvm/setup-graalvm to get native-image in CI.
- Native binaries ship in micro images with millisecond cold starts.