Back to Blog
Tutorial11 min read2026-07-02

How to Deploy a Micronaut Java App

Micronaut's compile-time dependency injection gives fast startup and low memory, ideal for containers. This guide covers building a JAR or GraalVM native image, configuring datasources, running Flyway, and deploying.

Ajay Kumar
Ajay Kumar
Founder & DevOps, PandaStack

Micronaut is a JVM framework designed for the cloud-native era. Its standout trait is compile-time dependency injection and AOT (ahead-of-time) processing — it does at build time what frameworks like Spring do via runtime reflection. The payoff is fast startup and low memory, which matters enormously for containers and scale-to-zero. You can deploy it as a normal JAR or compile to a GraalVM native image for near-instant cold starts.

Why Micronaut suits containers

Because DI and configuration are resolved at compile time, a Micronaut app:

  • Starts in a fraction of the time of a reflection-heavy framework.
  • Uses less memory (no large runtime metadata).
  • Is GraalVM-native-image friendly out of the box.

That profile is ideal for autoscaling and scale-to-zero, where startup latency is a direct user-facing cost.

A minimal controller

// src/main/java/com/example/HealthController.java
import io.micronaut.http.annotation.Controller;
import io.micronaut.http.annotation.Get;
import java.util.Map;

@Controller("/health")
public class HealthController {
    @Get
    public Map<String, String> health() {
        return Map.of("status", "ok");
    }
}

Micronaut's HTTP server binds based on configuration; set host and port in application.yml:

micronaut:
  application:
    name: myapp
  server:
    port: ${PORT:8080}
    host: 0.0.0.0

Using ${PORT:8080} reads the platform's PORT env var with a sensible default — and binding to 0.0.0.0 ensures the platform proxy can reach it.

Configuring a datasource

Micronaut Data with JDBC/JPA needs a datasource. Configure it in application.yml, reading the URL from the environment:

datasources:
  default:
    url: ${JDBC_DATABASE_URL}
    driver-class-name: org.postgresql.Driver
    username: ${DB_USER}
    password: ${DB_PASSWORD}
    maximum-pool-size: 10

As with most JVM stacks, managed platforms hand you a DATABASE_URL in postgres://user:pass@host/db form, while JDBC wants jdbc:postgresql://host/db. Either convert it at startup or set JDBC_DATABASE_URL, DB_USER, and DB_PASSWORD explicitly. This URL-format mismatch is the single most common JVM deploy snag.

Building: JAR vs native image

Option A — standard JAR (simplest, works everywhere):

./gradlew shadowJar    # or ./mvnw package
# Produces build/libs/myapp-all.jar
java -jar build/libs/myapp-all.jar

Option B — GraalVM native image (tiny binary, millisecond startup):

./gradlew nativeCompile
# Produces a native executable in build/native/nativeCompile/

Native images start in milliseconds and use very little memory — the best fit for scale-to-zero. The trade-off is longer build times and occasional reflection-config work for libraries that aren't native-ready. Micronaut's AOT minimizes that pain, but test thoroughly.

Dockerfiles for each path

JAR:

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

FROM eclipse-temurin:21-jre
WORKDIR /app
COPY --from=build /app/build/libs/*-all.jar app.jar
ENV PORT=8080
EXPOSE 8080
CMD ["java", "-jar", "app.jar"]

Native image:

FROM ghcr.io/graalvm/native-image-community:21 AS build
WORKDIR /app
COPY . .
RUN ./gradlew nativeCompile --no-daemon

FROM debian:bookworm-slim
RUN apt-get update && apt-get install -y ca-certificates && rm -rf /var/lib/apt/lists/*
COPY --from=build /app/build/native/nativeCompile/myapp /usr/local/bin/myapp
EXPOSE 8080
CMD ["myapp"]

The native path produces a far smaller, faster-starting container at the cost of a heavier build.

Migrations with Flyway

Micronaut has first-class Flyway integration. Add the dependency and put migrations in src/main/resources/db/migration, then enable:

flyway:
  datasources:
    default:
      enabled: true

Micronaut runs migrations at startup with a lock, so multiple replicas won't conflict.

Deploying on PandaStack

  1. 1Provision a managed PostgreSQL (14.x or 16.x). PandaStack injects DATABASE_URL; convert to JDBC form or set the JDBC/user/password vars.
  2. 2Connect the Git repo as a container app. The Dockerfile is auto-detected — pick the JAR or native Dockerfile.
  3. 3The platform provides PORT; application.yml reads it via ${PORT:8080}.
  4. 4Flyway migrates at startup (or run a one-off step).
  5. 5Add a custom domain; SSL is automatic.

If you run on a scale-to-zero tier, the native image is worth the extra build effort — cold starts drop from JVM-boot seconds to milliseconds. For steady traffic, the JAR on always-on compute is perfectly fine and simpler.

Production checklist

  • [ ] Bind 0.0.0.0, read PORT via ${PORT}.
  • [ ] DATABASE_URL converted to JDBC format.
  • [ ] HikariCP pool under the DB tier's connection limit.
  • [ ] Flyway enabled for migrations.
  • [ ] Native image for scale-to-zero, JAR for simplicity.
  • [ ] ca-certificates in the native runtime image for DB TLS.

Verifying

curl -s https://api.example.com/health
# {"status":"ok"}

A healthy response confirms binding, build, and datasource conversion are correct.

References

  • [Micronaut deployment guide](https://docs.micronaut.io/latest/guide/#deployment)
  • [Micronaut GraalVM native images](https://docs.micronaut.io/latest/guide/#graal)
  • [Micronaut Data configuration](https://micronaut-projects.github.io/micronaut-data/latest/guide/)
  • [Micronaut Flyway support](https://micronaut-projects.github.io/micronaut-flyway/latest/guide/)

---

Micronaut's fast startup (especially as a native image) is a natural fit for scale-to-zero, and PandaStack auto-detects your Dockerfile while injecting DATABASE_URL from a managed PostgreSQL. Try it free at [dashboard.pandastack.io](https://dashboard.pandastack.io).

Ready to deploy?

Start free on PandaStack.

Start free on PandaStack

More in Tutorial

Browse all Tutorial articles →

See also