2026-04-24·8 min read·sota.io team

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:

MetricSpring Boot 3Micronaut 4 (JVM)Micronaut 4 (Native)
Startup time4-8s300-500ms30-50ms
RSS memory (idle)280-350MB90-130MB28-45MB
First request latency200-800ms5-15ms1-3ms
Throughput (req/s)8,000-12,00015,000-22,00018,000-25,000
Docker image size280MB180MB22MB

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

Featuresota.ioRailwayRenderHeroku
EU-native infrastructure✓ Frankfurt✗ US-default✓ Frankfurt✗ US-default
GDPR Article 28 DPAPartial
Managed PostgreSQL EU✓ same region✓ EU add-on
Groovy/JVM auto-detect
GraalVM native support
Free tier✓ limited✓ limited
Data stays in EU✓ alwaysPartial

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.

Deploy your Groovy Micronaut app to Europe with sota.io →