Micronaut #
Micronaut adalah framework JVM modern yang dirancang sejak awal untuk cold-start time secepat native image dan memory footprint sekecil mungkin — tanpa mengorbankan produktivitas developer yang sudah terbiasa dengan ekosistem Java. Berbeda dengan Spring Boot yang menggunakan reflection dan classpath scanning saat runtime, Micronaut melakukan dependency injection, AOP, dan konfigurasi bean di compile-time lewat annotation processor. Hasilnya: startup dalam hitungan milidetik, tidak ada proxy runtime, dan tidak ada lazy loading yang tidak perlu.
Docker Compose melengkapi kekuatan ini dengan cara yang elegan — kamu bisa menjalankan seluruh stack (Micronaut app, Postgres, Redis) dengan satu perintah, tanpa harus install Java, Maven, atau service backend secara manual di host. Artikel ini membahas setup lengkap Micronaut untuk local development: dari Dockerfile multi-stage, konfigurasi docker-compose.yml dengan dependency service, hot reload pakai mn:run, sampai integrasi Micronaut Data, Flyway, dan @MicronautTest untuk testing.
Prasyarat #
Pastikan environment lokal kamu sudah punya:
- Docker dan Docker Compose versi terbaru (Compose V2 sudah built-in di Docker Desktop)
- Java 21+ (LTS) — Micronaut 4.x butuh minimal Java 17, tapi Java 21 sudah mainstream dan lebih future-proof
- Maven 3.8+ atau Gradle 8+ (Gradle lebih cepat untuk build incremental, tapi Maven lebih familiar di banyak tim enterprise)
- Micronaut CLI (
mn) — untuk scaffolding project dan task management
Project Micronaut standar yang dibuat lewat mn create-app punya struktur seperti ini:
my-micronaut-app/
├── src/
│ ├── main/
│ │ ├── java/
│ │ │ └── com/example/
│ │ │ ├── Application.java
│ │ │ ├── controller/
│ │ │ ├── service/
│ │ │ └── repository/
│ │ └── resources/
│ │ ├── application.yml
│ │ ├── logback.xml
│ │ └── db/migration/
│ └── test/
│ └── java/
├── pom.xml
├── mvnw
├── mvnw.cmd
├── .mvn/
├── Dockerfile
├── docker-compose.yml
├── .env
└── .dockerignore
Filosofi Docker untuk Micronaut #
Sebelum masuk ke konfigurasi, penting untuk dipahami: local development mengutamakan feedback loop, bukan optimasi runtime. Artinya:
- Boleh pakai image besar — JDK 300MB tidak masalah untuk laptop developer. Image sekecil-kecilnya adalah target production.
- Boleh pakai build lebih lambat — yang penting iterasi cepat (edit → reload → cek).
- Boleh mount source code langsung — supaya perubahan di IDE langsung terlihat di container.
- Boleh skip multi-stage — Dockerfile dev cukup single stage, JRE base, command langsung
mvn mn:run.
Tujuan utama Docker di local development Micronaut:
- Konsistensi environment — semua developer pakai versi Java dan library yang sama
- Onboarding cepat — developer baru tinggal
docker compose uplalu jalan - Iterasi cepat — hot reload dengan
mn:runrestart dalam hitungan detik - Setup dependency otomatis — Postgres, Redis, dan service lain start bersamaan dengan app
Arsitektur Stack #
Setup Micronaut untuk local development biasanya melibatkan:
- Micronaut app — service utama yang kamu develop
- PostgreSQL — database paling umum untuk Micronaut Data
- Redis — untuk cache, session, atau rate limiting
- (Opsional) Kafka — untuk event-driven architecture
- (Opsional) pgAdmin atau Adminer — UI untuk inspect database
flowchart TB
subgraph Dev["Local Machine"]
IDE[IDE - IntelliJ / VS Code]
end
subgraph Compose["Docker Compose Stack"]
APP[Micronaut App<br/>mn:run :8080]
DB[(PostgreSQL<br/>:5432)]
CACHE[(Redis<br/>:6379)]
end
IDE -- edit & save --> APP
APP -- JDBC --> DB
APP -- Lettuce / Redis client --> CACHE
Semua service berjalan di network internal Compose. Micronaut app bisa resolve hostname db dan cache langsung tanpa tahu IP address.
Dockerfile Multi-Stage #
Micronaut tidak punya CLI Docker bawaan sekaya Quarkus, jadi kamu perlu tulis Dockerfile sendiri. Untuk local development, single stage dengan JDK base sudah cukup — kamu butuh JDK karena Micronaut pakai annotation processor dan mn:run menjalankan Maven goal. Tapi karena artikel ini juga membahas production, kita buat multi-stage dengan dev override.
# syntax=docker/dockerfile:1.6
# ============ Stage 1: Builder ============
FROM eclipse-temurin:21-jdk-alpine AS builder
WORKDIR /build
# Tool tambahan untuk debugging dan healthcheck
RUN apk add --no-cache bash curl
# Copy descriptor dulu untuk cache layer dependency
COPY pom.xml mvnw ./
COPY .mvn .mvn
RUN chmod +x mvnw && ./mvnw -B dependency:go-offline
# Copy source code dan build JAR
COPY src ./src
RUN ./mvnw -B package -DskipTests
# Extract layered JAR (Micronaut support sejak 3.x)
RUN mkdir -p target/extracted && \
cd target/extracted && \
unzip -q ../*.jar
# ============ Stage 2: Runtime Production ============
FROM eclipse-temurin:21-jre-alpine AS runtime
WORKDIR /app
# Copy layered JAR
COPY --from=builder /build/target/extracted/BOOT-INF/lib /app/lib
COPY --from=builder /build/target/extracted/BOOT-INF/classes /app/classes
COPY --from=builder /build/target/extracted/META-INF /app/META-INF
# Non-root user
RUN addgroup -S micronaut && adduser -S micronaut -G micronaut
USER micronaut:micronaut
EXPOSE 8080
ENV JAVA_OPTS="-XX:MaxRAMPercentage=75.0"
ENTRYPOINT ["sh", "-c", "exec java $JAVA_OPTS -cp 'classes:lib/*' com.example.Application"]
# ============ Stage 3: Development ============
FROM eclipse-temurin:21-jdk-alpine AS dev
WORKDIR /app
RUN apk add --no-cache bash curl
# Copy semua yang dibutuhkan untuk run via Maven
COPY pom.xml mvnw ./
COPY .mvn .mvn
RUN chmod +x mvnw
# Source code di-mount lewat volume di Compose, jadi tidak perlu COPY src
# TAPI: pre-warm dependency cache agar first build cepat
RUN ./mvnw -B dependency:go-offline || true
EXPOSE 8080
# Default command: jalankan mn:run (override via compose command)
CMD ["./mvnw", "mn:run"]
Penjelasan Tiap Stage #
Builder stage — berisi JDK dan Maven untuk compile. Layer dependency:go-offline meng-cache semua dependency, sehingga perubahan source code tidak invalidate layer ini. Build berikutnya hanya butuh compile dan package, biasanya 10–30 detik.
Runtime stage — image final untuk production. Cuma JRE, tanpa Maven. Extract JAR ke layer terpisah (lib, classes, META-INF) supaya Docker cache bisa reuse layer dependency saat source code berubah. Ini yang disebut layered JAR optimization — Spring Boot mengenalkan pola ini di 2.3, dan Micronaut support sejak 3.x.
Dev stage — JDK lengkap dengan Maven wrapper. Image ini yang dipakai untuk docker compose up di local. Berbeda dengan Spring Boot DevTools yang punya auto-restart di JVM, Micronaut andalkan mn:run yang melakukan restart pada proses Maven (bukan JVM — lebih lambat dari Spring DevTools, tapi cukup cepat untuk iterasi).
Tiga stage, bukan satu. Pemisahandevdanruntimememastikan image development tetap berisi JDK dan Maven (untukmn:run), sementara image production sekecil mungkin. Build dengan target stage spesifik:docker build --target dev -t myapp:dev .ataudocker build --target runtime -t myapp:prod ..
docker-compose.yml untuk Development #
Berikut setup lengkap dengan Postgres, Redis, healthcheck, dan bind mount untuk hot reload:
# docker-compose.yml
services:
app:
build:
context: .
dockerfile: Dockerfile
target: dev # pakai stage dev (bukan runtime)
image: my-micronaut-app:dev
container_name: micronaut-app
command: ./mvnw mn:run
volumes:
- ./src:/app/src
- ./pom.xml:/app/pom.xml
- maven_cache:/root/.m2
- ./target:/app/target
ports:
- "8080:8080"
- "5005:5005" # JDWP debug port
environment:
MICRONAUT_ENVIRONMENTS: dev
MICRONAUT_DATASOURCES_DEFAULT_URL: jdbc:postgresql://db:5432/micronaut
MICRONAUT_DATASOURCES_DEFAULT_USERNAME: app
MICRONAUT_DATASOURCES_DEFAULT_PASSWORD: dev
MICRONAUT_DATASOURCES_DEFAULT_DRIVER_CLASS_NAME: org.postgresql.Driver
MICRONAUT_REDIS_URI: redis://cache:6379
MICRONAUT_SERVER_PORT: 8080
JAVA_TOOL_OPTIONS: "-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=*:5005"
depends_on:
db:
condition: service_healthy
cache:
condition: service_healthy
db:
image: postgres:16-alpine
container_name: micronaut-db
environment:
POSTGRES_USER: app
POSTGRES_PASSWORD: dev
POSTGRES_DB: micronaut
volumes:
- db-data:/var/lib/postgresql/data
- ./db/init:/docker-entrypoint-initdb.d:ro
healthcheck:
test: ["CMD-SHELL", "pg_isready -U app -d micronaut"]
interval: 10s
timeout: 5s
retries: 5
ports:
- "5432:5432"
cache:
image: redis:7-alpine
container_name: micronaut-cache
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 10s
timeout: 3s
retries: 3
ports:
- "6379:6379"
adminer:
image: adminer:4.8.1
container_name: micronaut-adminer
ports:
- "8081:8080"
environment:
ADMINER_DEFAULT_SERVER: db
depends_on:
- db
volumes:
db-data:
maven_cache:
Penjelasan Service #
app — service Micronaut utama. target: dev memilih stage dev dari Dockerfile (JDK + Maven). Command mn:run menjalankan aplikasi via Maven plugin dengan auto-restart saat source code berubah. Volume bind mount ./src ke /app/src membuat perubahan di IDE langsung sinkron ke container. Volume maven_cache menyimpan .m2 repository di named volume — kalau tidak, dependency akan di-download ulang setiap container restart.
db — PostgreSQL 16. Healthcheck pg_isready memastikan database benar-benar siap menerima koneksi sebelum app mencoba connect. Init script di ./db/init (jika ada) akan dijalankan saat container pertama kali start (bukan saat restart dengan data existing).
cache — Redis 7. Healthcheck pakai redis-cli ping yang return PONG saat Redis ready. Micronaut punya module micronaut-redis-lettuce untuk koneksi Redis reactive.
adminer — UI berbasis web untuk inspect database. Akses di http://localhost:8081, login pakai credential Postgres. Bisa diganti dengan pgAdmin atau DBeaver (berbasis desktop).
Healthcheck dan depends_on #
depends_on tanpa condition: service_healthy hanya menunggu container start, bukan service siap. Postgres butuh 2–5 detik untuk initialize cluster, jadi app bisa saja crash dengan “Connection refused” kalau start sebelum Postgres siap. Selalu pakai healthcheck untuk service yang punya initialization time.
Hot Reload dengan mn:run #
Berbeda dengan Spring Boot DevTools atau Quarkus Dev Mode, Micronaut tidak punya hot reload seketika. Restart dilakukan oleh mn:run dengan cara:
- Deteksi perubahan di source code (polling setiap beberapa detik)
- Kill proses lama (SIGTERM)
- Compile ulang dengan annotation processor
- Start proses baru dengan classpath yang sama
Total waktu restart: 3–8 detik untuk project kecil-menengah. Lebih lambat dari Quarkus (~1 detik) atau JRebel, tapi cukup cepat untuk kebanyakan workflow development.
pom.xml — dependency untuk development #
<dependencies>
<!-- Core Micronaut -->
<dependency>
<groupId>io.micronaut</groupId>
<artifactId>micronaut-http-server-netty</artifactId>
</dependency>
<!-- Validation -->
<dependency>
<groupId>io.micronaut.validation</groupId>
<artifactId>micronaut-validation</artifactId>
</dependency>
<!-- Data access -->
<dependency>
<groupId>io.micronaut.data</groupId>
<artifactId>micronaut-data-jdbc</artifactId>
</dependency>
<dependency>
<groupId>io.micronaut.sql</groupId>
<artifactId>micronaut-jdbc-hikari</artifactId>
</dependency>
<dependency>
<groupId>org.postgresql</groupId>
<artifactId>postgresql</artifactId>
<scope>runtime</scope>
</dependency>
<!-- Flyway untuk schema migration -->
<dependency>
<groupId>io.micronaut.flyway</groupId>
<artifactId>micronaut-flyway</artifactId>
</dependency>
<dependency>
<groupId>org.flywaydb</groupId>
<artifactId>flyway-database-postgresql</artifactId>
<scope>runtime</scope>
</dependency>
<!-- Redis -->
<dependency>
<groupId>io.micronaut.redis</groupId>
<artifactId>micronaut-redis-lettuce</artifactId>
</dependency>
<!-- Management & health -->
<dependency>
<groupId>io.micronaut</groupId>
<artifactId>micronaut-management</artifactId>
</dependency>
<!-- Logging -->
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
<scope>runtime</scope>
</dependency>
<!-- Test -->
<dependency>
<groupId>io.micronaut.test</groupId>
<artifactId>micronaut-test-junit5</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>junit-jupiter</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
application.yml — konfigurasi #
micronaut:
application:
name: my-micronaut-app
server:
port: 8080
router:
static-resources:
default:
enabled: true
paths: classpath:public
http:
services:
# default http client config
datasources:
default:
url: jdbc:postgresql://localhost:5432/micronaut
username: app
password: dev
driver-class-name: org.postgresql.Driver
schema-generate: NONE # pakai Flyway
dialect: POSTGRES
maximum-pool-size: 10
flyway:
datasources:
default:
enabled: true
locations: classpath:db/migration
baseline-on-migrate: true
redis:
uri: redis://localhost:6379
endpoints:
health:
enabled: true
sensitive: false
details-visible: ANONYMOUS
all:
sensitive: false
jackson:
serialization:
indent-output: true
Override konfigurasi via environment variable. Micronaut baca environment variable dengan patternMICRONAUT_DATASOURCES_DEFAULT_URL,REDIS_URI, dll. Format Compose di atas pakai pendekatan ini supaya tidak perlu rebuild image saat konfigurasi berubah. Untuk production, simpan di secrets manager atau config file yang di-mount, bukan environment variable di Dockerfile.
Micronaut Data untuk Database Access #
Micronaut Data adalah ORM ringan yang terinspirasi dari GORM (Groovy) dan Spring Data. Berbeda dengan Hibernate yang pakai reflection, Micronaut Data compile repository implementation di build-time lewat annotation processor. Hasilnya: tidak ada startup overhead, tidak ada proxy runtime, dan query dieksekusi langsung ke JDBC.
Entity #
package com.example.entity;
import io.micronaut.data.annotation.GeneratedValue;
import io.micronaut.data.annotation.Id;
import io.micronaut.data.annotation.MappedEntity;
import io.micronaut.data.annotation.MappedProperty;
import io.micronaut.data.annotation.DateCreated;
import io.micronaut.data.annotation.DateUpdated;
import io.micronaut.serde.annotation.Serdeable;
import java.time.Instant;
@Serdeable
@MappedEntity("users")
public class User {
@Id
@GeneratedValue(GeneratedValue.Type.IDENTITY)
private Long id;
@MappedProperty("email")
private String email;
@MappedProperty("name")
private String name;
@DateCreated
@MappedProperty("created_at")
private Instant createdAt;
@DateUpdated
@MappedProperty("updated_at")
private Instant updatedAt;
// constructors, getters, setters
public User() {}
public User(String email, String name) {
this.email = email;
this.name = name;
}
public Long getId() { return id; }
public void setId(Long id) { this.id = id; }
public String getEmail() { return email; }
public void setEmail(String email) { this.email = email; }
public String getName() { return name; }
public void setName(String name) { this.name = name; }
public Instant getCreatedAt() { return createdAt; }
public Instant getUpdatedAt() { return updatedAt; }
}
Repository #
package com.example.repository;
import com.example.entity.User;
import io.micronaut.data.jdbc.annotation.JdbcRepository;
import io.micronaut.data.model.query.builder.sql.Dialect;
import io.micronaut.data.repository.CrudRepository;
import java.util.List;
import java.util.Optional;
@JdbcRepository(dialect = Dialect.POSTGRES)
public interface UserRepository extends CrudRepository<User, Long> {
// Method-name query (compile-time)
Optional<User> findByEmail(String email);
List<User> findByNameContaining(String nameFragment);
long countByEmail(String email);
// Custom query dengan @Query
@io.micronaut.data.annotation.Query("SELECT * FROM users WHERE created_at > :since")
List<User> findRecentUsers(java.time.Instant since);
}
Controller #
package com.example.controller;
import com.example.entity.User;
import com.example.repository.UserRepository;
import io.micronaut.http.HttpResponse;
import io.micronaut.http.MediaType;
import io.micronaut.http.annotation.*;
import io.micronaut.validation.Validated;
import jakarta.validation.Valid;
import jakarta.validation.constraints.Email;
import jakarta.validation.constraints.NotBlank;
import java.util.List;
@Controller("/users")
@Validated
@Produces(MediaType.APPLICATION_JSON)
public class UserController {
private final UserRepository userRepository;
public UserController(UserRepository userRepository) {
this.userRepository = userRepository;
}
@Get
public List<User> list() {
return (List<User>) userRepository.findAll();
}
@Get("/{id}")
public HttpResponse<User> get(Long id) {
return userRepository.findById(id)
.map(HttpResponse::ok)
.orElse(HttpResponse.notFound());
}
@Post
@Consumes(MediaType.APPLICATION_JSON)
public HttpResponse<User> create(@Body @Valid UserRequest request) {
User user = new User(request.email(), request.name());
User saved = userRepository.save(user);
return HttpResponse.created(saved);
}
public record UserRequest(
@NotBlank @Email String email,
@NotBlank String name
) {}
}
Pattern ini sangat berbeda dari Spring Data JPA: tidak ada JpaRepository, tidak ada @Entity dari JPA, dan tidak ada EntityManager injection. Micronaut Data generate implementasi JDBC langsung di compile-time.
Schema Migration dengan Flyway #
Flyway adalah tool schema migration yang paling mature di ekosistem JVM. Micronaut punya module micronaut-flyway yang otomatis jalankan migration saat startup.
Struktur migration files #
src/main/resources/db/migration/
├── V1__create_users.sql
├── V2__create_orders.sql
└── V3__add_user_index.sql
V1__create_users.sql #
CREATE TABLE users (
id BIGSERIAL PRIMARY KEY,
email VARCHAR(255) NOT NULL UNIQUE,
name VARCHAR(255) NOT NULL,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
);
CREATE INDEX idx_users_email ON users(email);
V2__create_orders.sql #
CREATE TABLE orders (
id BIGSERIAL PRIMARY KEY,
user_id BIGINT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
total_amount DECIMAL(10, 2) NOT NULL,
status VARCHAR(50) NOT NULL DEFAULT 'pending',
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
);
CREATE INDEX idx_orders_user_id ON orders(user_id);
CREATE INDEX idx_orders_status ON orders(status);
Flyway otomatis baca file SQL di db/migration sesuai prefix versi (V1__, V2__, dst.) dan run saat aplikasi start. Tracking table flyway_schema_history menyimpan migration yang sudah dijalankan. Untuk reset, hapus volume database:
docker compose down -v # hapus db-data
docker compose up -d
Jangan edit migration yang sudah di-apply di production. Migration harus immutable setelah di-deploy. Untuk fix bug, buat migration baru (V4__fix_users_table.sql) yang melakukan ALTER atau data fix. Ini menjamin konsistensi antara environment dan mencegah “works on my machine”.
Health Endpoints Built-in #
Micronaut punya built-in health checks via module micronaut-management. Tidak perlu dependency tambahan — endpoint langsung tersedia di /health (atau path lain sesuai konfigurasi).
endpoints:
health:
enabled: true
sensitive: false
details-visible: ANONYMOUS
Endpoint yang tersedia #
| Endpoint | Fungsi |
|---|---|
GET /health |
Status keseluruhan (UP/DOWN) |
GET /health/liveness |
Liveness probe — apakah proses hidup |
GET /health/readiness |
Readiness probe — apakah app siap terima traffic |
GET /health/diskSpace |
Detail penggunaan disk |
GET /health/db |
Status koneksi database |
GET /health/redis |
Status koneksi Redis |
Untuk Kubernetes, biasanya mapping liveness dan readiness ke path terpisah:
endpoints:
health:
probes:
enabled: true
liveness:
enabled: true
readiness:
enabled: true
Custom health check #
Tambah HealthIndicator untuk check dependency custom:
package com.example.health;
import io.micronaut.health.HealthStatus;
import io.micronaut.management.endpoint.health.HealthEndpoint;
import io.micronaut.management.health.indicator.HealthIndicator;
import io.micronaut.management.health.indicator.HealthResult;
import jakarta.inject.Singleton;
import org.reactivestreams.Publisher;
import reactor.core.publisher.Mono;
import java.util.Map;
@Singleton
public class ExternalApiHealthIndicator implements HealthIndicator {
@Override
public Publisher<HealthResult> getResult() {
// call external API, return result
boolean isUp = checkExternalApi();
HealthResult result = isUp
? HealthResult.builder("external-api", HealthStatus.UP).build()
: HealthResult.builder("external-api", HealthStatus.DOWN)
.details(Map.of("error", "timeout")).build();
return Mono.just(result);
}
private boolean checkExternalApi() {
// implementation
return true;
}
}
Testing dengan @MicronautTest #
Micronaut Test adalah ekstensi JUnit 5 yang menyederhanakan integration test. Annotation @MicronautTest boot application context, dan kamu bisa inject bean apa pun yang kamu butuhkan.
Unit test service #
package com.example.service;
import com.example.entity.User;
import com.example.repository.UserRepository;
import io.micronaut.test.annotation.MockBean;
import io.micronaut.test.extensions.junit5.annotation.MicronautTest;
import jakarta.inject.Inject;
import org.junit.jupiter.api.Test;
import java.util.Optional;
import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.Mockito.*;
@MicronautTest
class UserServiceTest {
@Inject
UserService userService;
@Inject
UserRepository userRepository;
@MockBean(UserRepository.class)
UserRepository mockRepository() {
return mock(UserRepository.class);
}
@Test
void shouldFindUserByEmail() {
User user = new User("[email protected]", "Test");
when(userRepository.findByEmail("[email protected]"))
.thenReturn(Optional.of(user));
Optional<User> found = userService.findByEmail("[email protected]");
assertTrue(found.isPresent());
assertEquals("[email protected]", found.get().getEmail());
}
}
Integration test dengan Testcontainers #
package com.example;
import com.example.repository.UserRepository;
import io.micronaut.test.extensions.junit5.annotation.MicronautTest;
import io.micronaut.test.support.TestPropertyProvider;
import jakarta.inject.Inject;
import org.junit.jupiter.api.Test;
import org.testcontainers.containers.PostgreSQLContainer;
import org.testcontainers.junit.jupiter.Container;
import org.testcontainers.junit.jupiter.Testcontainers;
import static org.junit.jupiter.api.Assertions.*;
@MicronautTest
@Testcontainers
class UserRepositoryIntegrationTest implements TestPropertyProvider {
@Container
static PostgreSQLContainer<?> postgres = new PostgreSQLContainer<>("postgres:16-alpine")
.withDatabaseName("test")
.withUsername("test")
.withPassword("test");
@Inject
UserRepository userRepository;
@Override
public Map<String, String> getProperties() {
return Map.of(
"datasources.default.url", postgres.getJdbcUrl(),
"datasources.default.username", postgres.getUsername(),
"datasources.default.password", postgres.getPassword()
);
}
@Test
void shouldSaveAndRetrieveUser() {
var user = userRepository.save(new User("[email protected]", "Test"));
var found = userRepository.findById(user.getId());
assertTrue(found.isPresent());
assertEquals("[email protected]", found.get().getEmail());
}
}
Testcontainers otomatis pull image Postgres, jalankan container, dan inject konfigurasi lewat TestPropertyProvider. Container di-shutdown setelah test class selesai.
Debugging dari IDE #
Micronaut app di container bisa di-debug dari IntelliJ atau VS Code. Set JAVA_TOOL_OPTIONS di Compose untuk enable JDWP:
environment:
JAVA_TOOL_OPTIONS: "-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=*:5005"
ports:
- "5005:5005"
Di IntelliJ:
- Run → Edit Configurations → Add New → Remote JVM Debug
- Host:
localhost, Port:5005 - Command line arguments:
-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=5005 - Set breakpoint di kode
- Run → Debug
Suspend=n artinya JVM start langsung, tidak menunggu debugger attach. Cocok untuk development yang tidak selalu butuh debugger. Kalau mau menunggu attach (untuk debug saat startup), set suspend=y.
Build dan Run #
# Build image
docker compose build
# Jalankan semua service di background
docker compose up -d
# Lihat log aplikasi (real-time)
docker compose logs -f app
# Restart hanya service app (tanpa rebuild)
docker compose restart app
# Stop semua service (data Postgres tetap)
docker compose down
# Stop dan hapus volume (reset database)
docker compose down -v
# Masuk ke container untuk debugging
docker compose exec app sh
Setelah docker compose up -d, akses:
- App: http://localhost:8080
- Health check: http://localhost:8080/health
- Adminer (DB UI): http://localhost:8081
- Postgres:
localhost:5432(userapp, passworddev) - Redis:
localhost:6379
Profile Environment #
Micronaut pakai konsep environment untuk konfigurasi per-stage. Defaultnya ada dev, test, prod. Set via environment variable MICRONAUT_ENVIRONMENTS=dev,custom.
application.yml (default, production-like):
micronaut:
application:
name: my-micronaut-app
datasources:
default:
url: ${DATABASE_URL:`jdbc:postgresql://localhost:5432/micronaut`}
username: ${DATABASE_USER:app}
password: ${DATABASE_PASSWORD:dev}
application-dev.yml (development overrides):
datasources:
default:
url: jdbc:postgresql://db:5432/micronaut
username: app
password: dev
flyway:
datasources:
default:
clean-disabled: false # boleh drop & re-create saat dev
logger:
levels:
com.example: DEBUG
io.micronaut.data.query: DEBUG
application-prod.yml (production):
datasources:
default:
url: ${DATABASE_URL}
username: ${DATABASE_USER}
password: ${DATABASE_PASSWORD}
maximum-pool-size: 20
logger:
levels:
root: WARN
com.example: INFO
Aktifkan profile dengan MICRONAUT_ENVIRONMENTS=prod atau --micronaut.environments=prod.
Best Practice #
Pisahkan Dockerfile Dev dan Prod #
Pakai multi-stage dengan dev dan runtime stage. Image dev berisi JDK dan Maven, image prod cuma JRE + JAR. Hindari satu image yang dipakai untuk keduanya — JDK di production adalah attack surface tambahan.
Cache Maven Repository Selalu #
volumes:
- maven_cache:/root/.m2
Named volume untuk .m2 membuat dependency cache persist antar container restart. Tanpa ini, Maven download ratusan MB setiap kali container start.
Pakai Healthcheck untuk Service Dependency #
Database, Redis, dan message broker butuh waktu untuk siap. depends_on: condition: service_healthy membuat app menunggu healthcheck pass, bukan hanya container start.
Resource Limits untuk JVM #
services:
app:
deploy:
resources:
limits:
memory: 1G
cpus: "1.0"
JVM tidak auto-detect Docker memory limit kecuali pakai MaxRAMPercentage. Set di Dockerfile:
ENV JAVA_OPTS="-XX:MaxRAMPercentage=75.0 -XX:+UseContainerSupport"
Jangan Mount target/ secara Utuh #
volumes:
- ./target:/app/target
Mount target memungkinkan reuse compiled classes antar container restart, tapi bisa konflik dengan annotation processor yang menulis ke target/classes. Jika muncul error aneh saat mn:run, hapus target volume dan biarkan container compile ulang.
Logging Configuration #
Default log Micronaut ke stdout/stderr. Untuk development, set level DEBUG atau TRACE untuk package tertentu:
logger:
levels:
com.example: DEBUG
io.micronaut.http.server: DEBUG
Di production, pakai structured JSON log:
<dependency>
<groupId>net.logstash.logback</groupId>
<artifactId>logstash-logback-encoder</artifactId>
</dependency>
Pakai Layered JAR untuk Production #
Extract target/*.jar ke layer lib, classes, META-INF di Dockerfile. Layer dependency (lib) jarang berubah, layer classes sering berubah. Docker cache membuat rebuild sangat cepat.
Schema Migration di Production #
Jangan pakai schema-generate: CREATE_DROP atau UPDATE di production. Selalu pakai Flyway atau Liquibase dengan versioned migration files.
Troubleshooting #
Connection Refused ke Database #
app start sebelum Postgres siap. Pastikan:
- Healthcheck
dbbenar depends_on: db: condition: service_healthydiapp- Log Postgres menunjukkan “ready to accept connections”
Hot Reload Tidak Jalan #
Periksa:
- Volume
./src:/app/srcterpasang mn:runbenar-benar running (bukanmvn package)- File berubah di host (cek
docker compose exec app ls -la /app/src) - Annotation processor tidak error (cek log untuk “annotation processing”)
Maven Build Lambat #
Biasanya karena dependency cache hilang. Pastikan volume maven_cache terpasang dan tidak ke-flush:
docker compose down --remove-orphans # JANGAN pakai -v
docker compose up -d
OutOfMemoryError #
Tiga penyebab umum:
- Heap terlalu kecil — naikkan
JAVA_OPTS="-Xmx..."atau pakaiMaxRAMPercentage - Connection pool terlalu besar — turunkan
maximum-pool-sizedi datasource config - Memory limit container terlalu kecil — naikkan di
deploy.resources.limits.memory
Cek memory usage container:
docker stats micronaut-app
Health Check DOWN #
Lihat detail di /health/details-visible: ANONYMOUS (dev only). Biasanya:
- Database connection gagal — cek network antar container
- Redis tidak ready — cek
redis-cli pingdi container - Disk space habis — cek
df -hdi host
Micronaut dengan GraalVM Native Image #
Micronaut dirancang untuk native image compilation. Berbeda dengan Spring Boot yang baru support native di versi 3, Micronaut sudah native-first sejak awal.
pom.xml:
<plugin>
<groupId>org.graalvm.buildtools</groupId>
<artifactId>native-maven-plugin</artifactId>
</plugin>
Dockerfile untuk native build:
FROM ghcr.io/graalvm/graalvm-ce:java21-22.3.0 AS builder
WORKDIR /build
COPY pom.xml mvnw ./
COPY .mvn .mvn
RUN chmod +x mvnw && ./mvnw dependency:go-offline
COPY src ./src
RUN ./mvnw package -DskipTests -Pnative
FROM alpine:3.19
RUN apk add --no-cache libc6-compat
WORKDIR /app
COPY --from=builder /build/target/my-micronaut-app /app/app
EXPOSE 8080
ENTRYPOINT ["/app/app"]
Native image Micronaut startup dalam ~30ms (vs ~1–2 detik untuk JVM) dan memory ~50MB (vs ~200MB untuk JVM). Build lebih lama (menit), tapi runtime sangat efisien — cocok untuk serverless dan Kubernetes.
Micronaut Launch dan CRI #
Untuk multi-profile build, pakai Micronaut Launch API atau CLI mn:
# Create app dengan dependency spesifik
mn create-app my-app \
--features=data-jdbc,flyway,redis-lettuce,management
# Tambahkan dependency
mn add-dependency micronaut-security-jwt
CLI ini generate project dari template resmi, lalu tambahkan module yang dipilih. Lebih cepat dari manual edit pom.xml.
Distributed Configuration dengan Consul #
Untuk konfigurasi terpusat, Micronaut support Consul, Eureka, atau Kubernetes ConfigMap:
application.yml:
micronaut:
config-client:
enabled: true
consul:
client:
url: http://consul:8500
config:
enabled: true
format: yaml
path: config/my-micronaut-app
# docker-compose.yml
services:
consul:
image: hashicorp/consul:1.16
ports:
- "8500:8500"
app:
depends_on:
- consul
Config di Consul lebih di-prioritaskan dari file lokal — cocok untuk konfigurasi yang sama di banyak service.
Service Discovery dengan Consul #
micronaut:
discovery-client:
enabled: true
Setiap Micronaut app otomatis register ke Consul saat start, dan lookup service lain via Consul. Cocok untuk arsitektur microservice tanpa perlu hardcode URL.
Recap Cheatsheet #
| Pattern | Micronaut Module |
|---|---|
| HTTP server | micronaut-http-server-netty |
| Validation | micronaut-validation |
| Database | micronaut-data-jdbc, micronaut-jdbc-hikari |
| Schema migration | micronaut-flyway |
| Cache | micronaut-redis-lettuce |
| Health & metrics | micronaut-management |
| Security | micronaut-security-jwt |
| Reactive | micronaut-rxjava2/3 |
| Native | GraalVM native image |
| Service discovery | micronaut-discovery-client |
| Distributed config | micronaut-config-client |
| Tracing | micronaut-tracing-opentelemetry |
| Testing | micronaut-test-junit5 |
| CLI | mn (Micronaut CLI) |
Ringkasan #
- Micronaut ideal untuk local development JVM yang ringan dan cepat, dengan startup time milidetik dan memory footprint kecil.
- Dockerfile multi-stage dengan
devstage (JDK + Maven) untuk development danruntimestage (JRE + layered JAR) untuk production. Build pakaitarget: devdi Compose,target: runtimeuntuk production image.- Hot reload dengan
mn:runmelakukan restart proses Maven saat source code berubah, total 3–8 detik. Lebih lambat dari Quarkus tapi cukup untuk iterasi.- docker-compose.yml dengan Postgres, Redis, dan Adminer. Pakai healthcheck +
depends_on: condition: service_healthyuntuk startup yang benar.- Maven cache pakai named volume
maven_cache:/root/.m2agar dependency tidak di-download ulang setiap container restart.- Micronaut Data adalah ORM ringan dengan compile-time repository generation, tanpa reflection dan proxy runtime seperti Hibernate.
- Flyway untuk schema migration dengan versioned SQL files (
V1__,V2__, dst.) disrc/main/resources/db/migration/. Auto-run saat aplikasi start.- Health endpoints built-in via
micronaut-management:/health,/health/liveness,/health/readiness,/health/db,/health/redis. Cocok untuk Kubernetes probes.- Testing dengan
@MicronautTest+ Testcontainers untuk integration test.TestPropertyProviderinject konfigurasi container ke test context.- Debug dari IDE via JDWP port 5005 dengan
JAVA_TOOL_OPTIONS=-agentlib:jdwp=.... IntelliJ/VS Code attach sebagai Remote JVM Debug.- Environment profiles dengan
application-{env}.ymldanMICRONAUT_ENVIRONMENTS=dev,produntuk konfigurasi per-stage.- Resource limits penting untuk JVM: set
JAVA_OPTS=-XX:MaxRAMPercentage=75.0dandeploy.resources.limits.memorydi Compose.- Best practice: pisahkan Dockerfile dev/prod, cache
.m2, healthcheck untuk dependency, layered JAR untuk production, Flyway untuk migration, resource limits.- Troubleshooting: connection refused (healthcheck), hot reload (cek volume mount), slow build (cache Maven), OOM (resource limits).
- Layered JAR optimization — extract
target/*.jarkelib,classes,META-INFdi Dockerfile untuk Docker cache optimal.- Custom health check implement
HealthIndicatoruntuk check dependency eksternal. Built-in untuk database, Redis, disk space.- Production readiness dengan profile
prod, image multi-stage, health probes, resource limits, dan structured JSON logging.