Deployment

An external servlet container

Deploy to an external servlet container by skipping start and mapping AppServlet yourself:

ServletContextHandler context = new ServletContextHandler(ServletContextHandler.SESSIONS);
context.addServlet(new ServletHolder(new AppServlet(app)), "/*");

AppServlet is a standard servlet, so it runs on any container (Tomcat and others), not just Jetty. An embedded Tomcat that app.start(port) runs itself is a different thing, and lives in spider-silk-tomcat: see Tomcat, and Undertow for the third option. Deploying to a container you did not start, exclude the bundled Jetty:

implementation('io.github.benelog.spidersilk:spider-silk-core') {
    exclude group: 'org.eclipse.jetty.ee10'
}

Precompiled templates

jte compiles templates at runtime by default, which requires a JDK on the deployment machine. A production build precompiles instead: the jte Gradle plugin turns each template into a Java class at build time, so the jar renders compiled classes and a template that does not compile fails the build rather than the request. The plugin configuration and the matching constructor are in Templates, alongside the development-mode counterpart; a native image makes this mode a requirement rather than an optimization, since a native binary carries no compiler.

A container image, without Docker

Jib builds a container image from the Gradle model alone, so the build machine needs no Docker daemon: gradle :example-flashcard:jibBuildTar writes build/jib-image.tar, ready for any Docker host to load. The example’s configuration is the plugin, a JRE base, and the port:

plugins {
    id 'com.google.cloud.tools.jib' version '3.5.4'
}

jib {
    from {
        image = 'eclipse-temurin:21-jre'
    }
    to {
        image = 'ghcr.io/benelog/spider-silk-flashcard'
    }
    container {
        ports = ['8080']
    }
}

Jib layers the image the way the build is structured — dependencies, project modules, resources, classes — so an edit to the application code rewrites only the small layers on top, and it takes the entrypoint from the application block’s mainClass. A JRE base suffices because the templates ride along as the precompiled classes from the section above; nothing in the image compiles at runtime. One wrinkle: Jib checks the app’s targetCompatibility against the base image’s Java version, and a build that targets 21 through options.release alone must restate it — java { targetCompatibility = JavaVersion.VERSION_21 } — or the check reads the toolchain’s version and refuses the JRE 21 base.

gradle :example-flashcard:jibBuildTar
# then, on a machine that runs containers:
docker load < example-flashcard/build/jib-image.tar
docker run -p 8080:8080 ghcr.io/benelog/spider-silk-flashcard

The tar is only the daemonless route: gradle jib pushes the same image straight to the registry given credentials, and gradle jibDockerBuild loads it into a local daemon where one exists. H2 keeps its database file under the process home, /root/db in this image, so the data lives and dies with the container unless a volume is mounted there. The same build containerizes the GraalVM binary instead when asked: -Pnative swaps the base image and the layers, and the native image chapter shows what changes.