Skip to content

Quarkus Deployment Guide

Status: Final
Version: 1.0


Purpose

This guide provides comprehensive instructions for deploying the ESG Reporting Platform using Quarkus in both JVM and native modes.


Table of Contents

  1. Packaging Options
  2. Docker Images
  3. Kubernetes Deployment
  4. Health Checks
  5. Resource Requirements
  6. Native Image Compilation
  7. Development Features
  8. Production Checklist

Packaging Options

Quarkus provides multiple packaging formats, each optimized for different deployment scenarios.

Fast-Jar (Default)

The default packaging mode for Quarkus, optimized for fast startup and low memory usage.

./mvnw clean package

Output: target/quarkus-app/

Structure:

quarkus-app/
├── app/
│   └── esg-platform-1.0.0.jar        # Application code
├── lib/
│   └── *.jar                          # Dependencies
├── quarkus/
│   └── generated-bytecode.jar         # Quarkus-generated code
└── quarkus-run.jar                    # Main JAR

Run:

java -jar target/quarkus-app/quarkus-run.jar

Advantages: - Fast startup (~1 second) - Low memory usage (~70-100 MB) - Efficient for containers (layered for caching)

Uber-Jar

Single JAR containing all dependencies, useful for traditional deployment.

./mvnw clean package -Dquarkus.package.type=uber-jar

Output: target/esg-platform-1.0.0-runner.jar

Run:

java -jar target/esg-platform-1.0.0-runner.jar

Advantages: - Single file deployment - Traditional deployment model - Easier to distribute

Disadvantages: - Slower startup vs fast-jar - Larger file size - No Docker layer caching benefits

Native Executable

Compiled to native code using GraalVM, providing the fastest startup and lowest memory footprint.

./mvnw clean package -Pnative

Output: target/esg-platform-1.0.0-runner

Run:

./target/esg-platform-1.0.0-runner

Advantages: - Extremely fast startup (< 100ms) - Minimal memory usage (< 50 MB) - Instant scale-up for serverless/auto-scaling - Reduced attack surface

Disadvantages: - Longer build time (3-5 minutes) - Requires GraalVM or Docker - Some libraries require additional configuration


Docker Images

JVM Mode Dockerfile

Multi-stage build for optimal JVM container image:

# Build stage
FROM maven:3.9-eclipse-temurin-21 AS build
WORKDIR /build
COPY pom.xml .
COPY src ./src
RUN mvn clean package -DskipTests

# Runtime stage
FROM eclipse-temurin:21-jre-alpine
WORKDIR /app

# Create non-root user
RUN addgroup -g 1001 quarkus && \
    adduser -u 1001 -G quarkus -D -h /app quarkus

# Copy application
COPY --from=build --chown=quarkus:quarkus /build/target/quarkus-app /app

# Switch to non-root user
USER quarkus

# Health check
HEALTHCHECK --interval=30s --timeout=3s --start-period=60s --retries=3 \
  CMD wget --no-verbose --tries=1 --spider http://localhost:8080/q/health/live || exit 1

# Expose port
EXPOSE 8080

# Run application
CMD ["java", "-jar", "/app/quarkus-run.jar"]

Build and run:

docker build -f Dockerfile.jvm -t esg-platform:jvm .
docker run -p 8080:8080 esg-platform:jvm

Image size: ~200-250 MB (with Alpine JRE)

Native Mode Dockerfile

Multi-stage build for native executable:

# Build stage with GraalVM
FROM quay.io/quarkus/ubi-quarkus-mandrel-builder-image:23.1-java21 AS build
USER quarkus
WORKDIR /build

COPY --chown=quarkus:quarkus pom.xml .
COPY --chown=quarkus:quarkus src ./src

RUN mvn clean package -Pnative -DskipTests

# Runtime stage - minimal image
FROM quay.io/quarkus/quarkus-micro-image:2.0
WORKDIR /app

# Copy native executable
COPY --from=build --chown=1001:1001 /build/target/*-runner /app/application

# Health check
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
  CMD wget --no-verbose --tries=1 --spider http://localhost:8080/q/health/live || exit 1

# Expose port
EXPOSE 8080

# Run application
CMD ["./application", "-Dquarkus.http.host=0.0.0.0"]

Build and run:

docker build -f Dockerfile.native -t esg-platform:native .
docker run -p 8080:8080 esg-platform:native

Image size: ~50-80 MB

Quarkus Docker Extension

Alternatively, use Quarkus to generate Dockerfiles automatically:

# Add Docker extension
./mvnw quarkus:add-extension -Dextensions="container-image-docker"

Configuration in application.properties:

# Docker image configuration
quarkus.container-image.build=true
quarkus.container-image.group=esg-platform
quarkus.container-image.name=api
quarkus.container-image.tag=latest

# Build JVM image
quarkus.container-image.builder=docker

# Or build native image
#quarkus.native.container-build=true

Build image:

./mvnw clean package -Dquarkus.container-image.build=true


Kubernetes Deployment

Deployment Manifest (JVM Mode)

apiVersion: apps/v1
kind: Deployment
metadata:
  name: esg-platform
  labels:
    app: esg-platform
    mode: jvm
spec:
  replicas: 3
  selector:
    matchLabels:
      app: esg-platform
  template:
    metadata:
      labels:
        app: esg-platform
        mode: jvm
    spec:
      securityContext:
        runAsNonRoot: true
        runAsUser: 1001
        fsGroup: 1001

      containers:
      - name: esg-platform
        image: esg-platform:jvm
        imagePullPolicy: IfNotPresent

        ports:
        - name: http
          containerPort: 8080
          protocol: TCP

        # Quarkus health checks
        livenessProbe:
          httpGet:
            path: /q/health/live
            port: 8080
            scheme: HTTP
          initialDelaySeconds: 30
          periodSeconds: 10
          timeoutSeconds: 3
          failureThreshold: 3

        readinessProbe:
          httpGet:
            path: /q/health/ready
            port: 8080
            scheme: HTTP
          initialDelaySeconds: 10
          periodSeconds: 5
          timeoutSeconds: 3
          failureThreshold: 3

        startupProbe:
          httpGet:
            path: /q/health/started
            port: 8080
            scheme: HTTP
          initialDelaySeconds: 5
          periodSeconds: 5
          timeoutSeconds: 3
          failureThreshold: 12

        # Resource limits for JVM mode
        resources:
          requests:
            memory: "256Mi"
            cpu: "250m"
          limits:
            memory: "512Mi"
            cpu: "500m"

        env:
        - name: QUARKUS_PROFILE
          value: "prod"
        - name: QUARKUS_DATASOURCE_JDBC_URL
          valueFrom:
            secretKeyRef:
              name: esg-database
              key: jdbc-url
        - name: QUARKUS_DATASOURCE_USERNAME
          valueFrom:
            secretKeyRef:
              name: esg-database
              key: username
        - name: QUARKUS_DATASOURCE_PASSWORD
          valueFrom:
            secretKeyRef:
              name: esg-database
              key: password

        securityContext:
          allowPrivilegeEscalation: false
          capabilities:
            drop:
            - ALL
          readOnlyRootFilesystem: true
---
apiVersion: v1
kind: Service
metadata:
  name: esg-platform
  labels:
    app: esg-platform
spec:
  type: ClusterIP
  ports:
  - port: 80
    targetPort: 8080
    protocol: TCP
    name: http
  selector:
    app: esg-platform

Deployment Manifest (Native Mode)

For native mode, adjust resource requirements:

# In Deployment spec.template.spec.containers[0].resources
resources:
  requests:
    memory: "64Mi"    # Much lower memory requirements
    cpu: "100m"
  limits:
    memory: "128Mi"
    cpu: "200m"

# Faster startup probe
startupProbe:
  httpGet:
    path: /q/health/started
    port: 8080
  initialDelaySeconds: 1   # Native starts almost instantly
  periodSeconds: 2
  timeoutSeconds: 1
  failureThreshold: 5

Horizontal Pod Autoscaler

apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
  name: esg-platform-hpa
spec:
  scaleTargetRef:
    apiVersion: apps/v1
    kind: Deployment
    name: esg-platform
  minReplicas: 3
  maxReplicas: 20
  metrics:
  - type: Resource
    resource:
      name: cpu
      target:
        type: Utilization
        averageUtilization: 70
  - type: Resource
    resource:
      name: memory
      target:
        type: Utilization
        averageUtilization: 80
  behavior:
    scaleDown:
      stabilizationWindowSeconds: 300
      policies:
      - type: Percent
        value: 50
        periodSeconds: 60
    scaleUp:
      stabilizationWindowSeconds: 0
      policies:
      - type: Percent
        value: 100
        periodSeconds: 30
      - type: Pods
        value: 4
        periodSeconds: 30
      selectPolicy: Max

Generate Kubernetes Resources with Quarkus

Quarkus can generate Kubernetes manifests automatically:

# Add Kubernetes extension
./mvnw quarkus:add-extension -Dextensions="kubernetes"

Configuration in application.properties:

# Kubernetes resource generation
quarkus.kubernetes.deployment-target=kubernetes
quarkus.kubernetes.replicas=3

# Health checks
quarkus.kubernetes.liveness-probe.http-action-path=/q/health/live
quarkus.kubernetes.readiness-probe.http-action-path=/q/health/ready

# Resources
quarkus.kubernetes.resources.requests.memory=256Mi
quarkus.kubernetes.resources.requests.cpu=250m
quarkus.kubernetes.resources.limits.memory=512Mi
quarkus.kubernetes.resources.limits.cpu=500m

# Labels
quarkus.kubernetes.labels."app.kubernetes.io/name"=esg-platform
quarkus.kubernetes.labels."app.kubernetes.io/version"=1.0.0

Generate manifests:

./mvnw clean package
# Manifests generated in target/kubernetes/


Health Checks

Quarkus provides MicroProfile Health endpoints for liveness, readiness, and startup checks.

Default Health Endpoints

Endpoint Purpose When to Use
/q/health Combined health status Overall health check
/q/health/live Liveness probe Detect broken application state
/q/health/ready Readiness probe Check if app can accept traffic
/q/health/started Startup probe Check if app has started

Add Health Extension

./mvnw quarkus:add-extension -Dextensions="smallrye-health"

Custom Health Checks

Liveness check (detects broken application state):

import org.eclipse.microprofile.health.HealthCheck
import org.eclipse.microprofile.health.HealthCheckResponse
import org.eclipse.microprofile.health.Liveness
import jakarta.enterprise.context.ApplicationScoped

@Liveness
@ApplicationScoped
class DatabaseConnectionHealthCheck : HealthCheck {

    @Inject
    lateinit var dataSource: DataSource

    override fun call(): HealthCheckResponse {
        return try {
            dataSource.connection.use { conn ->
                val isValid = conn.isValid(5)
                HealthCheckResponse.named("database-connection")
                    .status(isValid)
                    .withData("connection_pool_active", "true")
                    .build()
            }
        } catch (e: Exception) {
            HealthCheckResponse.named("database-connection")
                .down()
                .withData("error", e.message)
                .build()
        }
    }
}

Readiness check (checks dependencies):

import org.eclipse.microprofile.health.Readiness

@Readiness
@ApplicationScoped
class ExternalServicesHealthCheck : HealthCheck {

    @Inject
    @RestClient
    lateinit var reportingServiceClient: ReportingServiceClient

    @Inject
    lateinit var redisClient: RedisDataSource

    override fun call(): HealthCheckResponse {
        val builder = HealthCheckResponse.named("external-services")

        try {
            // Check Redis
            redisClient.value(String::class.java).ping()
            builder.withData("redis", "up")

            // Check reporting service
            reportingServiceClient.healthCheck()
            builder.withData("reporting-service", "up")

            builder.up()
        } catch (e: Exception) {
            builder.down().withData("error", e.message)
        }

        return builder.build()
    }
}

Startup check (warm-up tasks):

import org.eclipse.microprofile.health.Startup

@Startup
@ApplicationScoped
class CacheWarmupHealthCheck : HealthCheck {

    @Inject
    lateinit var cacheService: CacheWarmupService

    override fun call(): HealthCheckResponse {
        val isWarmedUp = cacheService.isInitialized()

        return HealthCheckResponse.named("cache-warmup")
            .status(isWarmedUp)
            .withData("cache_entries", cacheService.getEntryCount().toString())
            .build()
    }
}

Health Check Response Example

{
  "status": "UP",
  "checks": [
    {
      "name": "database-connection",
      "status": "UP",
      "data": {
        "connection_pool_active": "true"
      }
    },
    {
      "name": "external-services",
      "status": "UP",
      "data": {
        "redis": "up",
        "reporting-service": "up"
      }
    }
  ]
}

Resource Requirements

JVM Mode

Minimum resources:

resources:
  requests:
    memory: "256Mi"
    cpu: "250m"
  limits:
    memory: "512Mi"
    cpu: "500m"

Recommended for production:

resources:
  requests:
    memory: "512Mi"
    cpu: "500m"
  limits:
    memory: "1Gi"
    cpu: "1000m"

Characteristics: - Startup time: 1-3 seconds - Base memory: ~100-150 MB - Memory under load: 200-400 MB - Warm-up time: 5-10 seconds

Native Mode

Minimum resources:

resources:
  requests:
    memory: "64Mi"
    cpu: "100m"
  limits:
    memory: "128Mi"
    cpu: "200m"

Recommended for production:

resources:
  requests:
    memory: "128Mi"
    cpu: "200m"
  limits:
    memory: "256Mi"
    cpu: "400m"

Characteristics: - Startup time: < 100ms - Base memory: ~30-50 MB - Memory under load: 80-150 MB - Warm-up time: Instant (no JIT)

Comparison Table

Metric JVM Mode Native Mode Improvement
Startup Time 1-3 seconds < 100ms 30x faster
Memory (Base) 150 MB 40 MB 3.7x less
Memory (Load) 300 MB 100 MB 3x less
Image Size 250 MB 60 MB 4x smaller
Time to First Request 5 seconds Instant Immediate
Build Time 30 seconds 4 minutes 8x slower
Peak Throughput High Slightly lower JVM JIT optimizes

Cost Savings Example

Scenario: 50 pods running 24/7

Mode Memory per Pod Total Memory Monthly Cost (AWS EKS)*
JVM 512 MB 25.6 GB ~$80
Native 128 MB 6.4 GB ~$20

*Approximate costs, actual pricing varies

Annual savings with native mode: ~$720


Native Image Compilation

Prerequisites

Option 1: Install GraalVM locally

# Download GraalVM
wget https://github.com/graalvm/graalvm-ce-builds/releases/download/jdk-21.0.1/graalvm-community-jdk-21.0.1_linux-x64_bin.tar.gz
tar -xzf graalvm-community-jdk-21.0.1_linux-x64_bin.tar.gz
export GRAALVM_HOME=$PWD/graalvm-community-openjdk-21.0.1+12.1
export PATH=$GRAALVM_HOME/bin:$PATH

Option 2: Use Docker (recommended)

# Build in container
./mvnw clean package -Pnative -Dquarkus.native.container-build=true

Build Native Executable

Maven:

# Local build (requires GraalVM)
./mvnw clean package -Pnative

# Container build (uses Docker)
./mvnw clean package -Pnative -Dquarkus.native.container-build=true

# With additional native-image options
./mvnw clean package -Pnative \
  -Dquarkus.native.additional-build-args="--verbose,-H:+ReportExceptionStackTraces"

Gradle:

./gradlew build -Dquarkus.package.type=native

# Container build
./gradlew build -Dquarkus.package.type=native \
  -Dquarkus.native.container-build=true

Native Configuration

application.properties:

# Native image configuration
quarkus.native.resources.includes=META-INF/**,templates/**,reports/**
quarkus.native.enable-http-url-handler=true
quarkus.native.enable-https-url-handler=true

# Reflection configuration (auto-detected by Quarkus)
# Add manual entries if needed:
quarkus.native.additional-build-args=\
  --initialize-at-run-time=io.netty.handler.ssl.ReferenceCountedOpenSslEngine

# Memory settings for native-image build
quarkus.native.native-image-xmx=6g

Reflection Configuration

Quarkus automatically registers most reflection needs, but you can add custom entries:

src/main/resources/reflection-config.json:

[
  {
    "name": "com.example.esg.model.MetricSubmission",
    "allDeclaredConstructors": true,
    "allPublicConstructors": true,
    "allDeclaredMethods": true,
    "allPublicMethods": true,
    "allDeclaredFields": true,
    "allPublicFields": true
  }
]

Testing Native Build

# Build native image
./mvnw clean package -Pnative -Dquarkus.native.container-build=true

# Run native tests
./mvnw verify -Pnative

# Run native executable
./target/esg-platform-1.0.0-runner

Troubleshooting Native Builds

Issue: ClassNotFoundException at runtime

# Add reflection configuration
quarkus.native.additional-build-args=\
  -H:ReflectionConfigurationFiles=reflection-config.json

Issue: Resource not found

# Include resources
quarkus.native.resources.includes=config/*,templates/*

Issue: Out of memory during build

# Increase native-image heap
quarkus.native.native-image-xmx=8g

Issue: SSL errors

# Enable URL handlers
quarkus.native.enable-https-url-handler=true
quarkus.ssl.native=true


Development Features

Dev Mode

Quarkus provides continuous testing and live reload during development.

Start dev mode:

./mvnw quarkus:dev

Features: - Hot reload (automatic code recompilation) - Live reload (browser auto-refresh) - Dev UI at http://localhost:8080/q/dev - Continuous testing - Dev Services (automatic test containers)

Dev UI Features: - Configuration browser - Extension list - Health check viewer - OpenAPI/Swagger UI - Database console (H2) - Arc (CDI) bean inspector - Scheduler viewer

Continuous Testing

In dev mode, tests run automatically on code changes:

# Start dev mode with continuous testing
./mvnw quarkus:dev

# In dev mode console:
# Press 'r' - Re-run tests
# Press 'f' - Run failed tests only
# Press 'b' - Toggle breakpoints
# Press 'i' - Toggle instrumentation
# Press 's' - Open test report in browser

Enable continuous testing by default:

quarkus.test.continuous-testing=enabled

Dev Services

Quarkus automatically starts test containers for databases, message brokers, etc.

Automatic services: - PostgreSQL - MySQL - MongoDB - Kafka - Redis - Keycloak

Configuration:

# Dev Services PostgreSQL (automatic)
%dev.quarkus.datasource.db-kind=postgresql
# No URL needed - DevServices starts container automatically

# Customize DevServices
%dev.quarkus.datasource.devservices.image-name=postgres:16
%dev.quarkus.datasource.devservices.port=5432

View DevServices:

# In dev mode, check console output for:
# DevServices: PostgreSQL started on port 51234

Remote Dev Mode

Develop against a remote Quarkus application:

# Package for remote dev
./mvnw clean package -Dquarkus.package.type=mutable-jar

# Start app with remote dev enabled
java -Dquarkus.live-reload.password=secret \
     -jar target/quarkus-app/quarkus-run.jar

# Connect from local
./mvnw quarkus:remote-dev -Dquarkus.live-reload.url=http://remote:8080 \
                          -Dquarkus.live-reload.password=secret

Production Checklist

Pre-Deployment

  • Run full test suite: ./mvnw verify
  • Build optimized package: ./mvnw clean package -Dquarkus.profile=prod
  • Scan image for vulnerabilities: docker scan esg-platform:latest
  • Review resource limits in Kubernetes manifests
  • Configure production datasource URL and credentials
  • Set up secrets management (HashiCorp Vault, AWS Secrets Manager)
  • Enable TLS/SSL for external connections
  • Configure CORS policy for production domains
  • Review and enable security headers
  • Set up log aggregation (ELK, Loki, CloudWatch)
  • Configure metrics collection (Prometheus)
  • Set up distributed tracing (Jaeger, Tempo)
  • Define alerting rules (Prometheus Alertmanager)
  • Configure horizontal pod autoscaling
  • Set up network policies
  • Enable pod security standards
  • Configure backup and disaster recovery
  • Document runbook procedures

Production Configuration

application.properties (%prod profile):

# Production profile
%prod.quarkus.http.port=8080
%prod.quarkus.http.host=0.0.0.0

# Security
%prod.quarkus.http.cors=true
%prod.quarkus.http.cors.origins=https://esg-platform.example.com
%prod.quarkus.http.ssl.port=8443
%prod.quarkus.http.ssl.certificate.key-store-file=/etc/ssl/keystore.p12
%prod.quarkus.http.ssl.certificate.key-store-password=${SSL_KEYSTORE_PASSWORD}

# Database
%prod.quarkus.datasource.jdbc.url=${QUARKUS_DATASOURCE_JDBC_URL}
%prod.quarkus.datasource.username=${QUARKUS_DATASOURCE_USERNAME}
%prod.quarkus.datasource.password=${QUARKUS_DATASOURCE_PASSWORD}
%prod.quarkus.datasource.jdbc.max-size=20
%prod.quarkus.datasource.jdbc.min-size=5

# Hibernate
%prod.quarkus.hibernate-orm.database.generation=none
%prod.quarkus.hibernate-orm.sql-load-script=no-file

# Logging
%prod.quarkus.log.level=INFO
%prod.quarkus.log.category."com.example.esg".level=INFO
%prod.quarkus.log.console.json=true

# Metrics
%prod.quarkus.micrometer.enabled=true
%prod.quarkus.micrometer.export.prometheus.enabled=true
%prod.quarkus.micrometer.binder.http-server.enabled=true
%prod.quarkus.micrometer.binder.jvm.enabled=true

# Tracing
%prod.quarkus.opentelemetry.enabled=true
%prod.quarkus.opentelemetry.tracer.exporter.otlp.endpoint=http://jaeger:4317

# Health checks
%prod.quarkus.smallrye-health.ui.enable=false

Deployment Commands

JVM Mode:

# Build
./mvnw clean package -Dquarkus.profile=prod

# Build Docker image
docker build -f Dockerfile.jvm -t esg-platform:1.0.0-jvm .

# Push to registry
docker tag esg-platform:1.0.0-jvm your-registry/esg-platform:1.0.0-jvm
docker push your-registry/esg-platform:1.0.0-jvm

# Deploy to Kubernetes
kubectl apply -f k8s/deployment-jvm.yaml
kubectl rollout status deployment/esg-platform

Native Mode:

# Build native image
./mvnw clean package -Pnative -Dquarkus.native.container-build=true

# Build Docker image
docker build -f Dockerfile.native -t esg-platform:1.0.0-native .

# Push to registry
docker tag esg-platform:1.0.0-native your-registry/esg-platform:1.0.0-native
docker push your-registry/esg-platform:1.0.0-native

# Deploy to Kubernetes
kubectl apply -f k8s/deployment-native.yaml
kubectl rollout status deployment/esg-platform

Post-Deployment Verification

# Check pod status
kubectl get pods -l app=esg-platform

# Check health endpoints
kubectl port-forward svc/esg-platform 8080:80
curl http://localhost:8080/q/health
curl http://localhost:8080/q/health/live
curl http://localhost:8080/q/health/ready

# View logs
kubectl logs -l app=esg-platform --tail=100 -f

# Check metrics
curl http://localhost:8080/q/metrics

# View OpenAPI spec
curl http://localhost:8080/q/openapi

Monitoring Queries

Prometheus queries:

# Request rate
rate(http_server_requests_seconds_count[5m])

# Error rate
rate(http_server_requests_seconds_count{status=~"5.."}[5m])

# P95 latency
histogram_quantile(0.95, rate(http_server_requests_seconds_bucket[5m]))

# Memory usage
process_resident_memory_bytes / 1024 / 1024

# JVM heap (JVM mode only)
jvm_memory_used_bytes{area="heap"} / 1024 / 1024


Cross-References


Additional Resources


Change Log

Version Date Author Changes
1.0 2026-01-13 Ralph Agent Initial Quarkus deployment guide