Back to Blog
Tutorial10 min read2026-07-03

How to Deploy a Micronaut Application

A practical guide to containerizing and deploying a Micronaut JVM application to production, including health checks, graceful shutdown, GraalVM native images, and wiring a managed PostgreSQL database.

Ajay Kumar
Ajay Kumar
Founder & DevOps, PandaStack

Micronaut is one of the most pleasant JVM frameworks to run in production. Its ahead-of-time compilation model means dependency injection and AOP are resolved at build time, so startup is fast and memory footprint is low compared to reflection-heavy stacks. That makes it a natural fit for containers. This guide walks through getting a Micronaut app from your laptop to a public URL with a managed database attached.

What we're deploying

We'll use a standard Micronaut app built with Gradle. If you're starting fresh:

mn create-app com.example.api --features=postgres,jdbc-hikari,flyway --build=gradle

This gives you a JDBC HikariCP connection pool, Flyway migrations, and the Postgres driver wired in. The key thing to understand before deploying is how Micronaut reads configuration: environment variables map cleanly to application.yml keys, so DATASOURCE_DEFAULT_URL overrides datasources.default.url. That convention is what lets us inject a database URL at runtime without code changes.

Configure for the container

Make sure your application.yml binds to 0.0.0.0 and reads the port from the environment. Most hosting platforms set a PORT variable:

micronaut:
  server:
    host: 0.0.0.0
    port: ${PORT:8080}

datasources:
  default:
    url: ${DATABASE_URL:`jdbc:postgresql://localhost:5432/app`}
    driver-class-name: org.postgresql.Driver

A subtlety: managed databases often hand you a URL in postgresql://user:pass@host:5432/db form, but the JDBC driver wants jdbc:postgresql://.... You can either transform it at startup or set the discrete DATASOURCE_DEFAULT_USERNAME / PASSWORD / URL variables explicitly. I prefer the explicit form because it avoids surprises with special characters in passwords.

Health checks and graceful shutdown

Add the management dependency so you get a /health endpoint, which orchestrators use for liveness and readiness probes:

implementation("io.micronaut:micronaut-management")

Enable it:

endpoints:
  health:
    enabled: true
    sensitive: false

Micronaut handles SIGTERM gracefully by default, draining in-flight requests before exit. This matters during rolling deploys so you don't drop active connections.

Dockerfile: JIT vs native

You have two real choices. The JIT (regular JVM) image is simpler and rebuilds fast. A native image via GraalVM starts in tens of milliseconds and uses a fraction of the RAM, at the cost of longer build times.

Here's a solid multi-stage JIT Dockerfile:

FROM gradle:8.7-jdk21 AS build
WORKDIR /app
COPY . .
RUN gradle shadowJar --no-daemon

FROM eclipse-temurin:21-jre-jammy
WORKDIR /app
COPY --from=build /app/build/libs/*-all.jar app.jar
EXPOSE 8080
ENTRYPOINT ["java", "-XX:MaxRAMPercentage=75", "-jar", "app.jar"]

The MaxRAMPercentage flag is important in containers: it tells the JVM to size its heap relative to the container memory limit rather than the host's total RAM, preventing OOM kills.

For a native image, Micronaut's Gradle plugin does the heavy lifting:

./gradlew nativeCompile

Then package the resulting binary in a minimal base. Native is worth it if you're running many small instances or care about cold starts on scale-to-zero infrastructure.

Deploying on PandaStack

With a Dockerfile in your repo, deployment is a git connection. PandaStack runs your build in an ephemeral Kubernetes Job pod using rootless BuildKit, pushes the image to Google Artifact Registry, and rolls it out via Helm. There's no host Docker socket involved, which is a meaningful security property for multi-tenant infrastructure.

Steps:

  1. 1Push your repo to GitHub.
  2. 2In the PandaStack dashboard, create a new container app and connect the repo.
  3. 3PandaStack detects the Dockerfile automatically. If you skipped the Dockerfile, the buildpack path can build a JVM app directly, but for Micronaut I recommend the explicit Dockerfile for control over JVM flags.
  4. 4Provision a managed PostgreSQL database (14.x or 16.x). PandaStack auto-wires it and injects DATABASE_URL into your app environment.
  5. 5Add the discrete datasource env vars if you're using the explicit form.

Flyway runs your migrations on startup, so the schema is created on first boot. Watch the live build and app logs (streamed from self-hosted Elasticsearch) to confirm the migration applied and the health endpoint went green.

Resource sizing

WorkloadSuggested tierNotes
Hobby/dev APIFree (0.25 CPU / 512MB)Fine for JIT idle; native images shine here
Steady production APIc1 compute-optimizedCPU-bound request handling
In-memory caching/heavy heapm1 memory-optimizedLarger HikariCP pools, caches

Free-tier apps run in a gVisor sandbox on spot nodes with KEDA scale-to-zero, so expect a cold start after idle periods. For a native-compiled Micronaut binary, that cold start is dramatically smaller than a typical JVM app, which makes the free tier genuinely usable for low-traffic services.

Verifying the deploy

Once live, hit your endpoints over the auto-provisioned domain with SSL:

curl https://your-app.pandastack.app/health
# {"status":"UP"}

Attach a custom domain in the dashboard and SSL is issued automatically. Server-side metrics and analytics (captured via the ingress into ClickHouse, no client SDK) give you request rates and latency without instrumenting your code.

Common pitfalls

  • Forgetting MaxRAMPercentage: the JVM ignores container limits without it and can get OOM-killed.
  • JDBC URL format: don't pass a raw postgresql:// URL where a jdbc: URL is expected.
  • Migrations on multiple replicas: Flyway takes a lock, but be aware that all replicas attempt it on boot. This is fine, but worth knowing.
  • Native reflection config: if you use libraries that reflect, you may need @ReflectiveAccess or reachability metadata for native images.

References

  • Micronaut deployment docs: https://docs.micronaut.io/latest/guide/#deployingTheApplication
  • Micronaut GraalVM native images: https://guides.micronaut.io/latest/micronaut-creating-first-graal-app.html
  • Flyway documentation: https://documentation.red-gate.com/fd
  • JVM container awareness (MaxRAMPercentage): https://docs.oracle.com/en/java/javase/21/docs/specs/man/java.html
  • HikariCP configuration: https://github.com/brettwooldridge/HikariCP

Micronaut's build-time model and small footprint make it one of the best JVM frameworks to containerize. If you want to try the full flow with an auto-wired database and live logs, PandaStack's free tier covers a web service plus a managed database to start. Deploy yours at https://dashboard.pandastack.io

Ready to deploy?

Start free on PandaStack.

Start free on PandaStack

More in Tutorial

Browse all Tutorial articles →

See also