Deploy Groovy & Micronaut to Europe — EU Hosting for JVM Microservices in 2026
Groovy is not a language that happened by accident in a North American university lab. It grew out of the Java ecosystem as a practical response to a real problem: Java's verbosity made it difficult to write expressive scripts, domain-specific languages, and glue code inside enterprise applications. The solution came from a French developer.
Guillaume Laforge is a software engineer from France who became the Groovy project lead in 2003, the year Groovy was created, and held that role for well over a decade. Laforge is based in Paris and has worked at companies including SpringSource, Google (as Developer Advocate), and Canva. He shaped Groovy's design philosophy — the idea that a JVM language should feel natural, should interoperate seamlessly with Java libraries, and should reduce the ceremony that makes enterprise Java painful. Under his leadership Groovy became one of the most widely used JVM scripting languages in the world.
Cédric Champeau, another French developer, joined the Groovy core team and became one of its most influential contributors. Champeau is particularly known for his work on Groovy's static type-checking system — a significant addition that made Groovy viable for large production codebases where runtime type surprises are unacceptable. He later moved to Gradle (the build tool that uses Groovy as its DSL), where he continues to influence how the JVM ecosystem builds software. Both Laforge and Champeau have spoken at Devoxx — the European developer conference that originated in Antwerp, Belgium, and is itself a product of the EU's strong JVM developer community.
Jochen Theodorou, a German developer, has been the technical lead of Groovy's runtime for many years. His work on the meta-object protocol — the system that makes Groovy's dynamic dispatch possible — is what allows Groovy code to call Java libraries transparently while adding features like closures, builders, and runtime mixins on top.
This European core team built a language that is deeply embedded in EU enterprise infrastructure. Groovy is the scripting language behind Jenkins pipelines, which run the CI/CD of thousands of European companies. It is the DSL inside Gradle builds, which the majority of Android and JVM projects use. It powers Grails applications that run in German insurance companies, Dutch banks, and French e-commerce platforms. For European JVM teams, Groovy is often the first DSL they write without even noticing it — because it is already inside the tools they use.
Micronaut is the framework that takes the JVM ecosystem into the cloud-native era. Unlike Spring Boot, which performs heavy reflection and classpath scanning at startup, Micronaut processes annotations at compile time using AST transformations. This means a Micronaut application starts in milliseconds rather than seconds, uses significantly less memory, and produces GraalVM native images that start even faster. For EU teams deploying microservices or serverless workloads, the operational difference is meaningful — a Micronaut API that starts in 200ms costs fundamentally less to run than one that takes 8 seconds.
For EU development teams whose data must remain in European infrastructure — whether for GDPR compliance, data residency requirements, or simply because their users are in Frankfurt and latency matters — this guide shows how to deploy a Groovy Micronaut application to sota.io.
Why Groovy and Micronaut Together
Micronaut was designed from the ground up to work well with Groovy. The framework's annotation-processing pipeline handles Groovy's AST correctly, and Micronaut's dependency injection, HTTP client, and data access layers all work with Groovy idioms — closures, builders, and dynamic typing where you want it, static typing where you need it.
The combination is particularly strong for teams that already have Groovy expertise from Jenkins or Gradle scripting. Moving from Groovy build scripts to a Groovy Micronaut service requires no language context switch. The same functional patterns — method closures, list comprehension, named parameters — work in both environments.
The startup advantage is real in production. A Groovy Micronaut application with database connectivity typically starts in under 500ms on the JVM, compared to 4-8 seconds for a comparable Spring Boot application. Compiled to GraalVM native image, startup times drop to under 50ms. For EU companies running autoscaling deployments on platforms like sota.io, this translates directly to faster scale-out under load and lower cold-start latency for users.
Building a Groovy Micronaut Application
A minimal Micronaut application with Groovy uses Micronaut Launch to generate the project structure. The following example builds a REST API with PostgreSQL via Micronaut Data JDBC.
Project Structure
src/
main/
groovy/
com/example/
Application.groovy
controller/
ProductController.groovy
domain/
Product.groovy
repository/
ProductRepository.groovy
resources/
application.yml
build.gradle
Dockerfile
sota.yaml
Application Entry Point
// src/main/groovy/com/example/Application.groovy
package com.example
import io.micronaut.runtime.Micronaut
class Application {
static void main(String[] args) {
Micronaut.run(Application, args)
}
}
Domain and Repository
// src/main/groovy/com/example/domain/Product.groovy
package com.example.domain
import io.micronaut.data.annotation.GeneratedValue
import io.micronaut.data.annotation.Id
import io.micronaut.data.annotation.MappedEntity
import groovy.transform.CompileStatic
@CompileStatic
@MappedEntity
class Product {
@Id
@GeneratedValue
Long id
String name
String description
BigDecimal price
String currency = "EUR"
}
// src/main/groovy/com/example/repository/ProductRepository.groovy
package com.example.repository
import com.example.domain.Product
import io.micronaut.data.jdbc.annotation.JdbcRepository
import io.micronaut.data.model.query.builder.sql.Dialect
import io.micronaut.data.repository.CrudRepository
@JdbcRepository(dialect = Dialect.POSTGRES)
interface ProductRepository extends CrudRepository<Product, Long> {
List<Product> findByCurrencyOrderByPriceAsc(String currency)
}
HTTP Controller
// src/main/groovy/com/example/controller/ProductController.groovy
package com.example.controller
import com.example.domain.Product
import com.example.repository.ProductRepository
import groovy.transform.CompileStatic
import io.micronaut.http.HttpResponse
import io.micronaut.http.annotation.*
@CompileStatic
@Controller("/products")
class ProductController {
private final ProductRepository repository
ProductController(ProductRepository repository) {
this.repository = repository
}
@Get("/")
List<Product> list() {
repository.findAll().toList()
}
@Get("/{id}")
HttpResponse<Product> get(Long id) {
repository.findById(id)
.map { HttpResponse.ok(it) }
.orElse(HttpResponse.notFound())
}
@Post("/")
HttpResponse<Product> create(@Body Product product) {
def saved = repository.save(product)
HttpResponse.created(saved)
}
@Delete("/{id}")
HttpResponse delete(Long id) {
repository.deleteById(id)
HttpResponse.noContent()
}
}
Database Configuration
# src/main/resources/application.yml
micronaut:
application:
name: groovy-micronaut-api
server:
port: 8080
datasources:
default:
url: ${DATABASE_URL:`jdbc:postgresql://localhost:5432/mydb`}
username: ${DATABASE_USER:postgres}
password: ${DATABASE_PASSWORD:}
driver-class-name: org.postgresql.Driver
db-type: postgres
schema-generate: CREATE_DROP
dialect: POSTGRES
flyway:
datasources:
default:
enabled: true
locations: classpath:db/migration
Gradle Build File
// build.gradle
plugins {
id("io.micronaut.application") version "4.4.0"
id("com.github.johnrengelman.shadow") version "8.1.1"
id("io.micronaut.graalvm") version "4.4.0"
}
version = "1.0"
group = "com.example"
repositories {
mavenCentral()
}
dependencies {
annotationProcessor("io.micronaut.data:micronaut-data-processor")
annotationProcessor("io.micronaut:micronaut-http-validation")
annotationProcessor("io.micronaut.serde:micronaut-serde-processor")
implementation("io.micronaut.groovy:micronaut-runtime-groovy")
implementation("io.micronaut.data:micronaut-data-jdbc")
implementation("io.micronaut.sql:micronaut-jdbc-hikari")
implementation("io.micronaut.serde:micronaut-serde-jackson")
runtimeOnly("org.postgresql:postgresql")
runtimeOnly("ch.qos.logback:logback-classic")
}
application {
mainClass.set("com.example.Application")
}
java {
sourceCompatibility = JavaVersion.toVersion("21")
targetCompatibility = JavaVersion.toVersion("21")
}
graalvmNative.toolchainDetection = false
micronaut {
runtime("netty")
testRuntime("junit5")
processing {
incremental(true)
annotations("com.example.*")
}
aot {
optimizeServiceLoading = false
convertYamlToJava = false
precomputeOperations = true
cacheEnvironment = true
optimizeClassLoading = true
deduceEnvironment = true
optimizeNetty = true
}
}
Dockerfile for Production
Micronaut works well with a multi-stage Docker build that compiles the fat JAR in a build stage and runs it in a minimal JRE image. For production deployments you can also build a GraalVM native image — though build times are longer, the result is a binary that starts in milliseconds with a fraction of the memory footprint.
# Stage 1 — Build
FROM gradle:8.5-jdk21 AS builder
WORKDIR /app
COPY build.gradle settings.gradle ./
COPY gradle ./gradle
RUN gradle dependencies --no-daemon
COPY src ./src
RUN gradle shadowJar --no-daemon
# Stage 2 — Runtime
FROM eclipse-temurin:21-jre-jammy
WORKDIR /app
RUN useradd -r -u 1001 appuser
USER appuser
COPY --from=builder /app/build/libs/*-all.jar app.jar
EXPOSE 8080
ENV JAVA_OPTS="-Xmx256m -Xms64m -XX:+UseContainerSupport"
ENTRYPOINT ["sh", "-c", "java $JAVA_OPTS -jar app.jar"]
For GraalVM native image builds, replace the runtime stage with:
# Stage 1 — Native Image Build
FROM ghcr.io/graalvm/native-image-community:21 AS builder
WORKDIR /app
COPY build.gradle settings.gradle ./
COPY gradle ./gradle
COPY src ./src
RUN gradle nativeCompile --no-daemon
# Stage 2 — Minimal Runtime
FROM debian:bookworm-slim
WORKDIR /app
RUN useradd -r -u 1001 appuser
USER appuser
COPY --from=builder /app/build/native/nativeCompile/groovy-micronaut-api .
EXPOSE 8080
ENTRYPOINT ["./groovy-micronaut-api"]
Native image builds typically take 5-10 minutes but produce a binary that starts in under 50ms and uses under 64MB of RSS memory — a significant advantage for EU microservice deployments where density and cold-start latency matter.
Deploying to Europe with sota.io
Add a sota.yaml to your project root:
# sota.yaml
build:
dockerfile: Dockerfile
deploy:
port: 8080
region: eu-central
health_check: /health
env:
- DATABASE_URL
- DATABASE_USER
- DATABASE_PASSWORD
Deploy with:
sota deploy
sota.io detects the Dockerfile, builds the image in EU infrastructure, provisions a managed PostgreSQL instance in the same region, and returns a live HTTPS URL — all without configuring Kubernetes, VPCs, or load balancers.
Environment Variables
Set your database credentials via the sota.io CLI or dashboard:
sota env set DATABASE_URL=jdbc:postgresql://your-db-host:5432/mydb
sota env set DATABASE_USER=myuser
sota env set DATABASE_PASSWORD=mypassword
sota.io's managed PostgreSQL databases are provisioned in Frankfurt (EU-Central) by default, meaning your application and database run in the same availability zone. For Micronaut Data JDBC, this co-location matters — even sub-millisecond network variance between application and database affects throughput at scale.
Health Checks and Observability
Micronaut's management module provides built-in health endpoints that sota.io uses for deployment health checks and traffic routing:
// Add to dependencies in build.gradle
implementation("io.micronaut:micronaut-management")
# application.yml additions
endpoints:
health:
enabled: true
sensitive: false
info:
enabled: true
sensitive: false
metrics:
enabled: true
sensitive: true
The /health endpoint returns a JSON summary of the application and its datasource connections. sota.io monitors this endpoint to determine whether the deployment is ready to receive traffic and automatically replaces unhealthy instances.
GDPR and EU Data Residency for JVM Teams
The GDPR's data residency requirements create a specific constraint for EU companies running JVM applications that process personal data: the data must stay in the EU unless appropriate safeguards exist for cross-border transfers. For most EU companies, the simplest safeguard is not transferring the data in the first place — which means running the JVM service and its database in EU infrastructure.
This is especially relevant for Groovy's enterprise strongholds. German automotive suppliers, Dutch logistics companies, and French insurance platforms that process customer personal data in Groovy services have a clear obligation to keep that data in EU servers. Running on AWS us-east-1 or a US-headquartered PaaS creates legal exposure that EU privacy officers are increasingly unwilling to accept after the Schrems II decision invalidated the Privacy Shield framework.
sota.io runs exclusively in EU-based data centres, holds a Data Processing Agreement available under Article 28 GDPR, and does not transfer customer data outside the EU. For EU enterprise teams, this means the compliance answer is straightforward: your Micronaut service and its PostgreSQL data stay in Europe.
Performance Profile
Micronaut's compile-time approach produces a consistent performance advantage over reflection-based frameworks at EU-scale workloads:
| Metric | Spring Boot 3 | Micronaut 4 (JVM) | Micronaut 4 (Native) |
|---|---|---|---|
| Startup time | 4-8s | 300-500ms | 30-50ms |
| RSS memory (idle) | 280-350MB | 90-130MB | 28-45MB |
| First request latency | 200-800ms | 5-15ms | 1-3ms |
| Throughput (req/s) | 8,000-12,000 | 15,000-22,000 | 18,000-25,000 |
| Docker image size | 280MB | 180MB | 22MB |
These figures are consistent with the benchmarks published by TechEmpower and the Micronaut documentation. The throughput difference reflects Micronaut's non-blocking Netty server and the absence of Spring's reflection overhead at request time.
Deployment Comparison
| Feature | sota.io | Railway | Render | Heroku |
|---|---|---|---|---|
| EU-native infrastructure | ✓ Frankfurt | ✗ US-default | ✓ Frankfurt | ✗ US-default |
| GDPR Article 28 DPA | ✓ | ✗ | Partial | ✗ |
| Managed PostgreSQL EU | ✓ same region | ✗ | ✓ | ✓ EU add-on |
| Groovy/JVM auto-detect | ✓ | ✓ | ✓ | ✓ |
| GraalVM native support | ✓ | ✓ | ✓ | ✗ |
| Free tier | ✓ | ✓ limited | ✓ limited | ✗ |
| Data stays in EU | ✓ always | ✗ | Partial | ✗ |
For EU JVM teams where data residency is a requirement rather than a preference, the operational choice simplifies: run in infrastructure that keeps your data in Europe by default, not by configuration.
Groovy was designed in Europe, refined by European developers, and has become embedded in the tools that European JVM teams use every day. Deploying a Groovy Micronaut service to European infrastructure is not just a technical choice — it is a natural continuation of where the language came from.