Run Micronaut on Kubernetes

Khimananda Oli 8 min read Programming and Languages
Run Micronaut on Kubernetes

By Khimananda Oli | Last reviewed: August 2026

To successfully run Micronaut on Kubernetes, you must move beyond basic deployments and configure the platform-specific integration that makes this framework unique. Unlike traditional Spring Boot applications, Micronaut’s compile-time dependency injection and low-memory footprint offer distinct advantages in containerized environments, but only if you correctly configure liveness probes, resource requests, and environment variable mapping. This guide provides the exact configuration patterns I use in production to ensure your Kubernetes resource limits and requests align with Micronaut’s actual runtime behavior.

Micronaut AppHealth EndpointsConfig Map BindingService DiscoveryKubernetes ClusterAPI ServerCoreDNS / ServiceConfigMaps & SecretsIngress ControllerExternal TrafficHTTPS RequestsgRPC / TCP
Micronaut integrates directly with Kubernetes APIs for health, configuration, and service discovery without sidecars

How do you configure Micronaut health checks for Kubernetes probes?

Kubernetes relies on liveness and readiness probes to manage pod lifecycle, and Micronaut exposes dedicated endpoints specifically designed for this purpose. A common mistake is using the generic /health endpoint for both probes, which can cause cascading restarts when a single downstream dependency fails. You should always separate these concerns to maintain cluster stability.

Distinguishing liveness from readiness

Liveness determines if the application process is functional, while readiness determines if it can accept traffic. In Micronaut, enable the management endpoints and expose them on a separate port to prevent external access to internal operational data. Add the following to your application.yml:

endpoints:
  health:
    enabled: true
    sensitive: false
    details-visible: ANONYMOUS
  all:
    port: 8081

micronaut:
  application:
    name: my-service
  server:
    port: 8080

This configuration binds management endpoints to port 8081 while your business logic remains on 8080. Your Kubernetes deployment should then reference these specific paths:

  • Liveness: httpGet on /health/liveness port 8081
  • Readiness: httpGet on /health/readiness port 8081
  • Startup probe: Use /health/started for native images to avoid premature kills during initialization

If you are integrating with databases or message brokers, consider reading about PostgreSQL administration essentials to understand how connection pool exhaustion can falsely trigger readiness failures. Configure custom health indicators in Micronaut to report degraded status rather than down status for non-critical dependencies.

What are the best practices for setting Kubernetes resource limits with Micronaut?

Micronaut’s predictable memory usage makes right-sizing containers easier than with reflection-heavy frameworks, but you still need to account for JVM overhead, metaspace, and direct buffers. Setting requests too low causes OOMKilled errors; setting them too high wastes cluster capacity and increases cloud bills.

Calculating accurate memory requests

For JVM-based Micronaut applications, a reliable baseline formula is: Heap Size + Metaspace (256MB) + Thread Stacks (1MB × thread count) + Direct Memory (256MB). If your application uses 512MB heap with 200 threads, your minimum request should be approximately 1.2GB. Always set limits equal to or slightly above requests to avoid burstable QoS penalties in production namespaces.

Deployment ModeMemory RequestCPU RequestStartup TimeBest For
JVM (Standard)1Gi – 2Gi500m – 1000m3s – 8sComplex apps, dynamic proxies
Native Image (GraalVM)128Mi – 256Mi100m – 250m0.05s – 0.3sServerless, high-density, scale-to-zero
JVM + CDS768Mi – 1.5Gi400m – 800m1.5s – 4sBalance of compatibility and speed

Monitor actual usage with Prometheus before finalizing values. My article on Prometheus metrics monitoring fundamentals covers the specific JVM exporters you need to track heap utilization and GC pressure accurately. Adjust requests based on P95 memory usage over a 7-day period, not peak spikes during load tests.

How does Micronaut service discovery work natively in Kubernetes?

You do not need Consul, Eureka, or a service mesh just to discover other services within the same cluster. Micronaut includes a native Kubernetes service discovery module that queries the Kubernetes API directly, eliminating the operational overhead of maintaining additional infrastructure. This approach reduces latency and simplifies compliance audits by removing extra network hops.

Enabling Kubernetes-native discovery

Add the micronaut-kubernetes-discovery-client dependency to your project. Micronaut will automatically resolve service names like http://payment-service to the corresponding Kubernetes Service DNS entry. No annotation changes are required in your declarative HTTP clients:

@Client("payment-service")
public interface PaymentClient {
    @Get("/api/v1/payments/{id}")
    Payment getPayment(String id);
}

Ensure your service account has RBAC permissions to list services and endpoints in the namespace. Without this, discovery fails silently and falls back to DNS-only resolution, losing load-balancing awareness. For cross-namespace communication, specify the fully qualified domain name (service.namespace.svc.cluster.local) or configure namespace watching in your Micronaut configuration.

Micronaut PodK8s API ServerTarget Service@Client InitGET /api/v1/endpointsList EndpointsIP:Port ListCache EndpointsHTTP/gRPC CallProcess RequestResponse
Micronaut queries the Kubernetes API once at startup and caches endpoints, avoiding per-request API calls

Should you use GraalVM native images when you run Micronaut on Kubernetes?

Native images transform your Micronaut application into a standalone executable with near-instant startup and minimal memory footprint. This is particularly valuable in Kubernetes environments where node density, scaling responsiveness, and cost efficiency directly impact operational budgets. However, native compilation introduces build-time complexity and restricts certain dynamic features.

When native images deliver measurable ROI

Choose native images when your workload scales frequently, runs in serverless-like patterns (Knative, KEDA), or operates under strict memory constraints. The reduction from 1.5GB to 150MB per pod allows 10x higher density on the same nodes. Startup times drop from seconds to milliseconds, making horizontal pod autoscaling genuinely responsive to traffic spikes rather than lagging behind demand.

When to stay on the JVM

Stick with standard JVM deployments if your application relies heavily on reflection-based libraries incompatible with GraalVM, requires dynamic class loading, or has long-lived processes where JIT optimization outperforms AOT compilation after warmup. Debugging native images in production is also significantly harder; stack traces lack line numbers and tooling support is limited compared to mature JVM profilers.

For teams managing secrets across both deployment modes, review Kubernetes secrets management done right to ensure environment variable injection works identically whether you run JVM or native binaries. Native images read environment variables at runtime, but some Micronaut configuration binding behaves differently during AOT compilation.

How do you optimize Micronaut startup performance in Kubernetes clusters?

Even on the JVM, Micronaut starts faster than most Java frameworks because it avoids runtime reflection scanning. You can push this advantage further with Class Data Sharing (CDS), parallel bean initialization, and lazy loading of non-critical components. These optimizations reduce readiness probe failures during rolling updates and improve deployment velocity.

  1. Enable AppCDS: Generate a shared archive during your Docker build using -XX:ArchiveClassesAtExit, then reference it at runtime with -XX:SharedArchiveFile. This cuts classloading time by 30–40%.
  2. Parallel initialization: Set micronaut.bean-creation.parallel=true to initialize independent beans concurrently. Measure the impact; some applications see no benefit if beans have sequential dependencies.
  3. Lazy beans: Annotate expensive, infrequently-used beans with @Context scope removed or use @Factory with lazy providers. Defer database migration runners to post-startup hooks if they block readiness unnecessarily.
  4. Tune GC for containers: Use G1GC with -XX:MaxRAMPercentage=75.0 instead of fixed heap sizes. This respects container memory limits dynamically and avoids OOMKilled events when Kubernetes adjusts resources.
Micronaut Startup Performance Comparison0ms2s4s6s8sJVM Standard7.0s / 1.4GiJVM + CDS3.2s / 1.1GiNative Image0.08s / 180Mi
Native images achieve 87x faster startup and 87% less memory versus standard JVM Micronaut deployments

Production checklist for running Micronaut on Kubernetes

Deploying Micronaut successfully requires attention to details that generic Kubernetes tutorials overlook. Verify each item before promoting to production:

  • Management endpoints isolated on a separate port with network policies restricting access
  • Liveness and readiness probes targeting correct paths with appropriate initial delays
  • Resource requests derived from observed P95 memory usage, not guesses
  • Service account RBAC scoped to minimum required permissions for discovery
  • Configuration externalized via ConfigMaps with immutable flags for cache safety
  • Graceful shutdown enabled with preStop hook delay matching termination grace period
  • Structured logging configured for container log aggregation (JSON format preferred)

If you are building observability into your Micronaut services from day one, start with instrumenting an app with OpenTelemetry to capture traces, metrics, and logs through a unified pipeline. Micronaut has first-class OpenTelemetry support that auto-instruments HTTP clients, database calls, and messaging without manual span creation.

Next steps for your Micronaut Kubernetes deployment

Running Micronaut on Kubernetes gives you a compelling combination of developer productivity and operational efficiency when configured correctly. Start with JVM deployments to validate functionality, profile resource usage under realistic load, then evaluate native images for workloads where density and startup latency justify the build complexity. Every configuration decision should be backed by measurement, not assumptions borrowed from other frameworks.

If your team needs help designing production-grade Micronaut deployments, optimizing existing clusters, or establishing compliance-ready infrastructure patterns, reach out to discuss your specific requirements. I work with engineering teams across Nepal and globally to build systems that scale safely and pass audits confidently.

Frequently Asked Questions

Use eclipse-temurin:21-jre-alpine for production deployments in 2026. It provides native health checks and minimal attack surface. Avoid full JDK images as they increase pod startup time and memory overhead significantly during horizontal scaling events on your cluster nodes.

Enable micronaut-management and expose /health/liveness and /health/readiness endpoints. Configure http.client.read-timeout appropriately in application.yml. Kubernetes uses these distinct paths to determine if a pod needs restarting or should simply stop receiving traffic during rolling updates.

Yes, typically under two seconds versus ten or more. This rapid startup reduces cold start penalties during autoscaling events and improves overall cluster resource efficiency when running many microservice replicas across available nodes.

Start with 256Mi requests and 512Mi limits for standard HTTP services. Monitor actual usage via Prometheus metrics since Micronaut consumes far less heap than traditional frameworks. Adjust based on observed garbage collection frequency and throughput requirements.

Yes, native images reduce startup to milliseconds and memory to under 100Mi. Build using the official Gradle plugin and test thoroughly since reflection-heavy libraries may require additional configuration hints during the compilation phase.

Mount secrets as files or environment variables and reference them using ${K8S_SECRET_NAME} syntax. Micronaut resolves these at startup without requiring custom code. Use sealed secrets or external secret operators for secure management across namespaces.

Istio and Linkerd both integrate well via sidecar proxies. Micronaut propagates trace headers automatically when configured correctly. Choose Linkerd for lower resource overhead or Istio for advanced traffic policies and multi-cluster federation capabilities.

Set terminationGracePeriodSeconds to thirty and enable micronaut.server.shutdown.timeout. This allows active requests to complete before SIGTERM forces termination. Without this configuration, in-flight requests fail during deployments causing user-visible errors and retry storms.

Check logs with kubectl logs and verify health endpoint accessibility. Common causes include missing database credentials, incorrect port bindings, or failed bean initialization. Ensure the container listens on 0.0.0.0 not localhost to accept probe connections from kubelet.

Use HorizontalPodAutoscaler targeting CPU or custom Prometheus metrics. Micronaut stateless design supports rapid scaling. Configure minReplicas to avoid cold starts during predictable traffic spikes and maxReplicas to prevent resource exhaustion on worker nodes.

Yes, mount ConfigMaps as volumes or environment variables. Micronaut reloads file-based configuration automatically when watch.enabled is true. For environment variable changes, pods must restart since JVM processes cannot dynamically update env vars at runtime.

Implement OAuth2 or JWT validation via micronaut-security. Never expose management endpoints publicly. Use network policies to restrict pod-to-pod communication and ingress controllers with TLS termination for external traffic entering the cluster boundary.

Output structured JSON logs using logback-json-classic. Include traceId, spanId, and pod metadata fields. This enables correlation across distributed traces and simplifies querying in observability platforms like Loki or Elasticsearch within your cluster stack.

Enable remote debugging by adding JDWP agent flags and forwarding ports via kubectl port-forward. Use ephemeral containers for production clusters to attach debuggers without modifying deployment manifests or exposing debug ports permanently through service definitions.

Use GitHub Actions or GitLab CI with kaniko or buildah for rootless container builds. Cache Gradle dependencies and layer JAR files separately to speed up builds. Push to registry only after passing integration tests against a temporary namespace.