Skip to content
Latchkey

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.

.github/workflows/ci.yml
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.

Frequently asked questions

CI/CD for a Quarkus Native Image with GitHub Actions?
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.
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.
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.

Related guides

References

Run this faster and cheaper on Latchkey managed runners - self-healing included. Start free → 30-day trial · No credit card