The recommended way to ship an Enkan application is as a self-contained fat JAR using the Maven Shade plugin.
Add the following to your pom.xml:
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-shade-plugin</artifactId>
<executions>
<execution>
<phase>package</phase>
<goals><goal>shade</goal></goals>
<configuration>
<transformers>
<transformer implementation=
"org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
<mainClass>com.example.MyMain</mainClass>
</transformer>
<transformer implementation=
"org.apache.maven.plugins.shade.resource.ServicesResourceTransformer"/>
</transformers>
</configuration>
</execution>
</executions>
</plugin>
Build and run:
mvn package
java -jar target/myapp.jar
The main class starts the REPL (or the system directly) and blocks:
public class MyMain {
public static void main(String[] args) {
PseudoRepl repl = new PseudoRepl(MySystemFactory.class.getName());
ReplBoot.start(repl,
new KotowariCommandRegister(),
new DevelCommandRegister());
}
}
For production, PseudoRepl listens on the console.
To skip the REPL and start the system directly:
public class MyMain {
public static void main(String[] args) {
EnkanSystem system = new MySystemFactory().create();
Runtime.getRuntime().addShutdownHook(new Thread(system::stop));
system.start();
}
}
Enkan follows 12-factor app conventions.
Configuration is read from three sources in priority order:
-Dkey=value)KEY=value)env.properties on the classpath (development defaults)Use Env to read values in your system factory:
import enkan.Env;
builder(new JettyComponent())
.set(JettyComponent::setPort, Env.getInt("PORT", 3000))
.set(JettyComponent::setHost, Env.get("HOST", "0.0.0.0"))
.build()
Env normalises key names: DATABASE_URL and database.url are treated as the same key.
env.properties examplePORT=3000
HOST=0.0.0.0
DATABASE_URL=jdbc:h2:mem:dev
In production, set environment variables instead of bundling an env.properties file.
Both JettyComponent and UndertowComponent inherit from WebServerComponent and share the same configuration API.
| Property | Setter | Default | Description |
|---|---|---|---|
| Port | setPort(int) | 80 | HTTP listen port |
| Host | setHost(String) | 0.0.0.0 | Bind address |
| SSL enabled | setSsl(boolean) | false | Enable HTTPS |
| SSL port | setSslPort(int) | 443 | HTTPS listen port |
| Keystore path | setKeystorePath(String) | — | Path to JKS keystore |
| Keystore password | setKeystorePassword(String) | — | Keystore password |
builder(new JettyComponent())
.set(JettyComponent::setPort, Env.getInt("PORT", 8080))
.set(JettyComponent::setSsl, true)
.set(JettyComponent::setSslPort, Env.getInt("SSL_PORT", 8443))
.set(JettyComponent::setKeystorePath, Env.get("KEYSTORE_PATH"))
.set(JettyComponent::setKeystorePassword, Env.get("KEYSTORE_PASSWORD"))
.build()
FlywayMigration runs migrations automatically when the component starts.
Add it to the system and declare it as a dependency of any component that needs a migrated schema:
EnkanSystem.of(
"datasource", new HikariCPComponent(OptionMap.of("uri", Env.get("DATABASE_URL"))),
"flyway", new FlywayMigration(),
"doma", new DomaProvider(),
...
).relationships(
component("flyway").using("datasource"),
component("doma").using("datasource", "flyway"), // waits for migrations
...
);
Migration SQL files go in src/main/resources/db/migration/ following Flyway’s naming convention (V1__description.sql).
Enkan supports deployment as a WAR via enkan-servlet.
war in pom.xml.jakarta.servlet.http.HttpServlet and delegate to ServletUtils:@WebServlet(urlPatterns = "/*")
public class MyServlet extends HttpServlet {
private EnkanSystem system;
@Override
public void init() {
system = new MySystemFactory().create();
system.start();
}
@Override
protected void service(HttpServletRequest req, HttpServletResponse res)
throws IOException {
ApplicationComponent<HttpRequest, HttpResponse> app =
system.getComponent("app", ApplicationComponent.class);
HttpRequest request = ServletUtils.buildRequest(req);
HttpResponse response = app.getApplication().handle(request);
ServletUtils.updateServletResponse(res, response);
}
@Override
public void destroy() {
system.stop();
}
}
A minimal Dockerfile for a fat JAR application:
FROM eclipse-temurin:21-jre
WORKDIR /app
COPY target/myapp.jar app.jar
EXPOSE 3000
ENV PORT=3000
ENTRYPOINT ["java", "-jar", "app.jar"]
Build and run:
docker build -t myapp .
docker run -p 3000:3000 -e DATABASE_URL=jdbc:postgresql://db/mydb myapp