CI/CD for a Micronaut + GraalVM App with GitHub Actions
Test on the JVM, build a Micronaut native image with Gradle, and deploy.
Micronaut does dependency injection at compile time, which pairs well with GraalVM native images. This recipe runs JVM tests, then builds the native executable via the Gradle nativeCompile task.
What the pipeline does
- set up a GraalVM JDK
- run JVM tests with gradle test
- build the native binary with nativeCompile
- build the native Docker image
- push the image for deploy
The workflow
The Micronaut Gradle plugin wires the GraalVM nativeCompile and dockerBuildNative tasks. setup-graalvm provides native-image and caches Gradle artifacts.
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: gradle
- run: ./gradlew test
- run: ./gradlew nativeCompile
- run: ./gradlew dockerBuildNative
- name: Push image
if: github.ref == 'refs/heads/main'
run: |
echo "${{ secrets.GITHUB_TOKEN }}" | docker login ghcr.io -u ${{ github.actor }} --password-stdin
docker push ghcr.io/${{ github.repository }}:latestCaching and speed
cache: gradle restores ~/.gradle. nativeCompile is the expensive step; running it on cheaper managed runners such as Latchkey (around 69% cheaper than GitHub-hosted) keeps native CI affordable, and auto-retry covers transient toolchain download failures so a flaky build does not block the merge.
Deploying
dockerBuildNative produces a small native-image container. Push it and deploy where fast startup matters: AWS Lambda (with the custom runtime), Cloud Run, or Knative for scale-to-zero workloads.
Key takeaways
- Micronaut does DI at compile time, which suits GraalVM native images.
- gradle nativeCompile and dockerBuildNative come from the Micronaut plugin.
- Native containers are tiny and start fast, ideal for serverless.