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.0Using ${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: 10As 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.jarOption 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: trueMicronaut runs migrations at startup with a lock, so multiple replicas won't conflict.
Deploying on PandaStack
- 1Provision a managed PostgreSQL (14.x or 16.x). PandaStack injects
DATABASE_URL; convert to JDBC form or set the JDBC/user/password vars. - 2Connect the Git repo as a container app. The Dockerfile is auto-detected — pick the JAR or native Dockerfile.
- 3The platform provides
PORT;application.ymlreads it via${PORT:8080}. - 4Flyway migrates at startup (or run a one-off step).
- 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, readPORTvia${PORT}. - [ ]
DATABASE_URLconverted 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-certificatesin 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).