grails prod run-app
26 Deployment
Version: 8.0.0-SNAPSHOT
Table of Contents
26 Deployment
Grails applications can be deployed in a number of ways, each of which has its pros and cons.
26.1 Standalone
"grails run-app"
You should be very familiar with this approach by now, since it is the most common method of running an application during the development phase. An embedded Tomcat server is launched that loads the web application from the development sources, thus allowing it to pick up any changes to application files.
You can run the application in the production environment using:
You can run the app using the bootRun Gradle task. The next command uses the Gradle Wrapper.
./gradlew bootRun
You can specify an environment supplying grails.env system property.
./gradlew -Dgrails.env=prod bootRun
Runnable WAR or JAR file
Another way to deploy in Grails 3.0 or above is to use the new support for runnable JAR or WAR files. To create runnable archives, run grails package:
grails package
Alternatively, you could use the assemble Gradle task.
./gradlew assemble
You can then run either the WAR file or the JAR using your Java installation:
java -Dgrails.env=prod -jar build/libs/mywar-0.1.war (or .jar)
A TAR/ZIP distribution
| Note: TAR/ZIP distribution assembly has been removed from Grails 3.1. |
"./gradlew bootRun"
You should be very familiar with this approach by now, since it is the most common method of running an application during the development phase. An embedded Tomcat server is launched that loads the web application from the development sources, thus allowing it to pick up any changes to application files.
You can run the application in the production environment using:
./gradlew bootRun -Dgrails.env=prod
You can run the app using the bootRun Gradle task. The next command uses the Gradle Wrapper.
./gradlew bootRun
You can specify an environment supplying grails.env system property.
./gradlew -Dgrails.env=prod bootRun
Runnable WAR or JAR file
Another way to deploy in Grails 3.0 or above is to use the new support for runnable JAR or WAR files. To create runnable archives, run grails package:
grails package
Alternatively, you could use the assemble Gradle task.
./gradlew assemble
You can then run either the WAR file or the JAR using your Java installation:
java -Dgrails.env=prod -jar build/libs/mywar-0.1.war (or .jar)
A TAR/ZIP distribution
| Note: TAR/ZIP distribution assembly has been removed from Grails 3.1. |
26.2 Container Deployment (e.g. Tomcat)
Grails apps can be deployed to a Servlet Container or Application Server.
WAR file
A common approach to Grails application deployment in production is to deploy to an existing Servlet container via a WAR file. Containers allow multiple applications to be deployed on the same port with different paths.
Creating a WAR file is as simple as executing the war command:
grails war
This will produce a WAR file that can be deployed to a container, in the build/libs directory.
Note that by default Grails will include an embeddable version of Tomcat inside the WAR file so that it is runnable (see the previous section), this can cause problems if you deploy to a different version of Tomcat. If you don’t intend to use the embedded container, replace spring-boot-starter-tomcat with spring-boot-starter-tomcat-runtime in build.gradle:
providedRuntime "org.springframework.boot:spring-boot-starter-tomcat-runtime"
The -runtime variant was introduced in Spring Boot 4.
It contributes only the integration classes Spring Boot needs to start in an external Servlet container, without bundling the embedded Tomcat distribution into the WAR.
Application servers
The Grails framework requires that runtime containers support Servlet 6.0.0 and above. By default, Grails framework applications are bundled with an embeddable Tomcat and testing is primarily done with Tomcat. Any servlet container meeting the minimum requirements should be able to run Grails framework applications, but some workarounds may be required for container-specific bugs or configurations.
26.3 Deployment Configuration Tasks
Setting up HTTPS and SSL certificates for standalone deployment
To configure an SSL certificate and to listen on an HTTPS port instead of HTTP, add properties like these to application.yml:
server:
port: 8443 # The port to listen on
ssl:
enabled: true # Activate HTTPS mode on the server port
key-store: <the-location-of-your-keystore> # e.g. /etc/tomcat7/keystore/tomcat.keystore
key-store-password: <your-key-store-password> # e.g. changeit
key-alias: <your-key-alias> # e.g. tomcat
key-password: <usually-the-same-as-your-key-store-password>
These settings control the embedded Tomcat container for a production deployment. Alternatively, the properties can be specified on the command-line. Example: -Dserver.ssl.enabled=true -Dserver.ssl.key-store=/path/to/keystore.
| Configuration of both an HTTP and HTTPS connector via application properties is not supported. If you want to have both, then you’ll need to configure one of them programmatically. (More information on how to do this can be found in the how-to guide below.) |
There are other relevant settings. Further reference:
26.4 Ahead-of-Time Processing
Spring’s ahead-of-time (AOT) processing moves bean wiring from startup to build time. Instead of scanning the classpath, reading annotations and evaluating conditions on every boot, the build generates Java source that registers the bean definitions directly, and the application runs that.
Two things follow from it: startup does less work, and the application becomes eligible for a GraalVM native image, which cannot perform the classpath scanning and reflection that normal startup relies on.
AOT is opt-in.
Enabling AOT
Apply the Spring Boot AOT plugin alongside the Grails plugins:
apply plugin: 'org.springframework.boot.aot'
This adds a processAot task, which bootJar then runs as part of the build. Nothing about a normal bootRun or bootJar changes until the application is started with AOT enabled:
java -Dspring.aot.enabled=true -jar build/libs/myapp.jar
An application started this way logs Starting AOT-processed Application rather than Starting Application.
The environment the definitions are generated for
processAot builds the bean definitions the packaged application will use, so it runs in the environment those definitions are meant for. The Grails Gradle plugin sets grails.env to production on the task; nothing needs to be configured for it.
It matters because an application declares different beans in different environments. Under development the URL mappings holder is a proxy whose type cannot be determined without creating it, and generation fails naming a bean unrelated to anything in the application:
Field urlMappings in org.grails.web.mapping.servlet.UrlMappingsErrorPageCustomizer
required a bean of type 'grails.web.mapping.UrlMappings' that could not be found.
Reload-mode beans have no meaning in a packaged artifact, so generating in production is correct as well as necessary. To generate for a different environment, set the property on the task yourself:
tasks.named('processAot') {
systemProperty 'grails.env', 'staging'
}
What AOT changes at runtime
An AOT-processed application registers the same beans as a normal one. The difference is the annotation-processing infrastructure it no longer needs, since the work those components do has already been done:
-
internalConfigurationAnnotationProcessor -
internalAutowiredAnnotationProcessor -
internalCommonAnnotationProcessor -
Spring Boot’s shared metadata reader factory
-
Grails' own configuration class post-processor
Application beans, plugin beans and auto-configuration beans are all present as usual.
Limitations
Beans contributed from a plugin’s beanRegistrar() are not generated. Spring marks them as registrar-owned and skips them, and they are registered again at runtime when Grails applies the plugin registrars. They behave correctly, but the plugin scan that produces them still runs on every start, so they do not benefit from generation.
Writing bean definitions that hold live objects will fail generation. A definition is a recipe rather than an instance, and there is no general way to generate code that reconstructs an arbitrary object, so a constructor argument or property value holding one cannot be processed:
UnsupportedTypeValueCodeGenerationException:
Code generation does not support com.example.SomeService
Reference another bean by name instead, or express the collaborator as a nested bean definition, and the generator can emit both.
Abstract bean definitions — templates that exist only to be inherited through bean.parent — are excluded from generation, because Spring has no representation for them in generated code. Definitions that inherit from one are unaffected: they are generated from their merged definition, with the inherited values already folded in.
AOT processing on its own does not make an application ready for a native image. A native image additionally requires reachability metadata covering the resources and reflection an application performs at runtime.
Recording reflection for a native image
Some of that metadata is written for you. The build reads its own output — the compiled artefacts, and the manifest naming each compiled page — and registers them, so a controller, a domain class or a GSP survives into an image without anything having to run.
What that cannot describe is the framework reflecting on its own behalf while a request is served: a controller method reached through Groovy’s dispatch, a conversion asked for while a form is bound. Those are not the application’s classes, so nothing in the build output names them. An image built without them starts, serves its home page, and fails on the first request that takes such a path — with MissingReflectionRegistrationError, naming a method nobody wrote.
traceNativeMetadata runs the application under GraalVM’s tracing agent and records what it actually did:
grails {
nativeMetadata {
paths = ['/', '/login', '/book', '/book/create']
forms = ['/book/create']
}
}
./gradlew traceNativeMetadata
The result is merged into src/native/resources/META-INF/native-image, alongside the source rather than under build, because which paths an image was built to cover is worth reviewing and worth committing.
paths are asked for. forms are asked for and submitted: the page is read, the fields the form declares are filled in, and they are posted to the action the form names. Submitting matters separately from rendering, because binding a form is a different half of the framework from rendering one — and it is the half that converts. A checkbox arrives as a string and lands on a boolean property, so a form submitted without one never asks the conversion service anything, and the image is built without the answer.
Pages behind a login
Forms are submitted before paths are asked for, whichever order they appear in the configuration, and the session a form establishes is carried through the rest of the trace. That is what makes a protected page traceable: list its login form, and the pages named in paths are reached as a signed-in user.
grails {
nativeMetadata {
forms = ['/login?username=admin&password=secret', '/book/create']
paths = ['/', '/book', '/book/show/1']
}
}
A form may be told what to put in it with a query string, as /login is above. Only fields the form actually declares are set from it; a value naming a field that is not there is reported rather than posted, because it was meant for a form that has since changed.
Choosing which form
A page often carries more than one form — a layout’s search box or sign-out button ahead of the page’s own. The form declaring the most fields is the one submitted, and the trace output says which it was:
POST /book/save -> 200 (4 fields, from /book/create form 2 of 2, the one with the most fields)
Where the wanted form is not the fullest, name it by its id:
forms = ['/book/create#bookForm']
When the trace fails
The task fails if what was recorded is not what was asked for: a request that answers with an error or a not-found, a page listed under forms that carries no form, a named form that is not on the page, or a form page that answered from somewhere else.
That last one is the case worth knowing about. A form page reached without whatever makes it reachable answers from the login form instead, and every check after that passes — the page it landed on has a form, its fields are filled in, it posts, and it answers successfully. What would be reported is the login form submitted a second time under the name of the page that was wanted, while the binding the page was listed for went unexercised. So a form that answers from anywhere other than where it was asked for fails the trace, and the message names where it landed.
Redirects are followed, so an application that sends / to its real home page records that page. A request that ends up somewhere other than where it was asked for is reported as a warning naming where it landed — usually the sign that a page needs its login form listed under forms.
The agent ships with GraalVM rather than with a JDK, and it must trace the archive the image will be built from — which is compiled for GraalVM’s Java. The task uses the project toolchain, which for a project that builds an image is already a GraalVM; grails.nativeMetadata.javaExecutable points it elsewhere.
|
| The agent records only what ran. A path that is not listed is not covered, and it will be missing in the way that only appears when somebody uses that page. Treat the list as the coverage it is, and extend it as the application grows. |
26.5 Ahead-of-Time Caching
Spring’s AOT processing removes the work of deciding what the beans are. What remains is the work the JVM does regardless: loading and linking classes, and interpreting methods until they are compiled. An AOT cache removes most of that as well.
The JDK can record a run of an application — the classes it loaded and linked, and the profiles of the methods it executed — into a cache file, and read that file on the next start instead of doing the work again. The cache is a JDK feature rather than a framework one, so it applies to the whole application: the framework, Spring, Hibernate, and the application’s own classes alike.
It requires JDK 25 or later.
Training is POSIX-only. The cache is written as the training JVM exits normally, so the run has to be asked to stop rather than killed — and a child process on Windows can only be killed. trainAotCache says so and stops rather than training a run it could not end. Reading a cache has no such restriction; only producing one does.
Enabling the cache
The cache is produced by running the application, so it is off unless asked for. Say which pages matter:
grails {
aotCache {
enabled = true
paths = ['/', '/login', '/book/index', '/book/show/1']
}
}
That adds two tasks. extractAotCacheApplication unpacks the executable jar into build/aot-cache/application, and trainAotCache runs what it unpacked, asks for each path, stops it, and leaves the cache beside the extracted application:
./gradlew trainAotCache
Trained myapp.aot (167 MB) over 4 paths
The application is unpacked first because a cache is read against the layout it was trained on. An executable jar loads its dependencies through a nested-jar classloader; the extracted form loads them as ordinary jars on the classpath, and only the second is a layout a cache can be reused against.
Running with the cache
build/aot-cache is what is deployed: the extracted application, and beside it the cache and what it was trained from.
build/aot-cache/
├── application/ (1)
├── myapp.aot (2)
└── aot-cache.properties (3)
| 1 | the extracted application, which is the layout the cache was trained against |
| 2 | the cache |
| 3 | what the cache was trained from |
java -XX:AOTCache=build/aot-cache/myapp.aot -Dspring.aot.enabled=true -Dgrails.env=production \
-jar build/aot-cache/application/myapp.jar
It does not matter which directory it is started from. Moving the whole of aot-cache elsewhere is fine too — a container image that unpacks it under /opt is the usual case — as long as the cache and application/ move together and keep their timestamps, which the cache checks the archive against. Copying only the jar out of application/ does not work, and not because of the cache: the extracted layout reaches its dependencies through lib/ beside it.
A JVM given a cache it cannot use does not fail. It reports why, ignores the cache, and starts as it would have anyway — so an invalid cache costs the startup time it was meant to save, silently, and without any other symptom.
Adding -XX:AOTMode=on turns that into a failure to start, which is worth doing wherever the cache is the reason the deployment meets its startup time. A cache that has been invalidated then stops the process rather than quietly making it slower:
[0.003s][warning][aot] Unable to use AOT cache.
[0.003s][error ][aot] Loading static archive failed.
What the training run should do
A cache makes the next start fast at whatever the training run did. Training a run that only refreshes the context records the framework starting, and that alone is most of what the startup time is — so most of the benefit is there before any path is named.
Asking for paths adds a little to that and a good deal more to the first request. Each path asked for during training profiles the code that answers it — URL mapping, the controller, GSP rendering, the data access behind it — so the first real request finds that work done rather than doing it itself.
So paths is worth setting for the pages that matter on a cold start: the ones a load balancer hits first, and the ones a user sees first. An application whose startup time is all that matters can leave it empty and still get most of the benefit.
A path that answers with an error still profiles the code that produced the error, which is code the application runs too, so nothing here fails the build. The run is a recording, not a test.
| The training run is a real run of the application. It executes bootstrap code, connects to whatever the configuration points at, and writes whatever that code writes. Point it at a build-time or throwaway database, never at production. |
It follows that the application has to be able to start in the environment it is trained in, which is production by default — the environment the cache is for. A datasource that only exists in development is not there during training, and the run fails with whatever the application says when it cannot reach its database:
The training run ended before it started serving.
What it printed is in build/tmp/trainAotCache/training.log
The build fails rather than carrying on, because a cache that was never written is indistinguishable at deployment time from one that was.
Where the defaults are not right, the rest of the run is configurable:
grails {
aotCache {
enabled = true
paths = ['/']
jvmArguments = ['-Dspring.aot.enabled=true', '-Dgrails.env=production']
port = 18080
startTimeoutSeconds = 120
}
}
jvmArguments is given to the training run and has to be given to every run that reads the cache. A cache records what it saw; a run configured differently from the training run is a different application as far as the cache is concerned, and its cache will be declined.
Because those arguments have to be reproduced at deployment time, they are recorded in aot-cache.properties, which ships beside the cache. Keep credentials out of them and pass them to the training run another way.
Knowing whether a cache still applies
A cache is read only by the JDK build that wrote it, against the archive it was trained on, with the arguments it was trained with. Any of those changing invalidates it, and the only symptom is that startup is no longer fast.
Training runs on the project’s Java toolchain where it declares one, so the JDK recorded here is the one the application is compiled for rather than whichever one happened to run Gradle.
So the training run writes aot-cache.properties beside the cache, recording what it was made from:
cache.file=myapp.aot
cache.bytes=175019584
application.archive=myapp-0.1.jar
application.sha256=6f1b...
training.arguments=-Dspring.aot.enabled=true -Dgrails.env=production
training.paths=/ /login /book/index /book/show/1
java.runtime.version=26.0.2+13
java.vendor=BellSoft
os.name=Mac OS X
os.arch=aarch64
A deployment can compare those values against the JVM it is about to start and the jar it is about to run, and decide whether it has a cache that applies or one that will be quietly ignored. Because the cache is tied to an exact JDK build, a CI pipeline that produces the cache must publish it alongside the JDK it was produced with, and a base image upgrade invalidates every cache built against the previous one.
What it is worth
Measured on a Grails application with GORM for Hibernate, security and asset pipeline, best of three starts on the same machine and JDK:
| Start | Startup | First hit of four cold paths |
|---|---|---|
Ordinary |
2.594s |
|
Spring AOT |
2.284s |
|
Spring AOT with a cache trained on refresh only |
1.015s |
0.495s |
Spring AOT with a cache trained over those paths |
0.933s |
0.332s |
The shape of that is the point. Spring’s AOT processing on its own moves less than might be expected, because deciding what the beans are was never most of the cost — loading and linking the classes was, and that is what the cache removes, cutting startup by more than half. Naming the paths takes a further tenth off startup and a third off the first requests.
An AOT cache and a native image solve the same problem differently and are not combined: a native image has no cache to read because it has already done all of this at build time. The cache is what an application gets when it stays on the JVM — including applications a native image cannot yet build, which is most of those using Hibernate.