A few years ago we had real servers with servlet containers installed and managed by administrators. These machines were bare-metal unicorns and had to be kept alive as long as possible (for many years). With the advent of first virtual machines and then container runtimes like Docker the approach and responsibilities for hosting web applications changed.
Our application not only went from Grails framework version 1 to 5 (atm, and soon to version 6) but also the above voyage regarding the hosting environment.
While the step from bare-metal to virtual machine was negligible from the developer and application side the migration to docker-based hosting is quite a step. Therefore I would like to depict the biggest changes of running a Grails application as a WAR deployment in Tomcat to a standalone application in Docker.
The original setting
We had our Grails application running on a server with a Tomcat servlet container for years. On the server we had also an ElasticSearch instance running and saved documents in a few well-defined directories on the local file system. Our database (Oracle in the past, PostgreSQL for over a year now) was running managed in a data center. We used container features like JNDI for parts of the configuration and datasources.
Deployment was essentially uploading a new WAR and context.xml file to the Tomcat servlet container using an Ansible playbook (where we restarted the servlet container to ensure not leaving any resource leaks…).
This setup worked quite well for years, only upgrades of the host operating system along with Tomcat and Java Virtual Machine (JVM) updates meant some work.
The target setup
Our customer has been in the process of converting most of the services running on virtual machines to dockerized deployments managed using Portainer. Most of the provisioning is automated and there is almost no snow flaking of the virtual machines hosting the docker runtimes. This eases the operation and administration of the services a lot and makes it much easier to setup new instances of a service and to monitor them.
So it was our task to migrate our setup on the the host VM to a docker-based deployment. In the future we basically push a docker image of our application to a docker registry of the customer and update the stack using Portainer.
The journey to a dockerized Grails app
The first thing to do were some changes to build.gradle
to build a Jar-application instead of a WAR archive. We decided it was easier and more lightweight to run the Grails app standalone with the embedded tomcat than to use Tomcat in a docker container:
- Remove the gradle war plugin
- Change
providedCompile
dependencies toimplementation
- Add a task to prepare the
Dockerfile
and context to build our bootable application image:
task prepareDocker(type: Copy, dependsOn: assemble) {
description = 'Copy files from src/main/docker and application jar to Docker temporal build directory'
group = 'Docker'
from 'src/main/docker'
from project.bootJar
into dockerBuildDir
}
- Add an appropiate
Dockerfile
to our docker context insrc/main/docker
:
FROM eclipse-temurin:11-jdk-jammy
MAINTAINER Mihael Koep "mihael.koep@softwareschneiderei.de"
EXPOSE 8080
WORKDIR /app
COPY naomi-boot.jar .
ENV GRAILS_ENV development
# Additional env variables for configuration
CMD java -Dgrails.env=$GRAILS_ENV -jar /app/application-boot.jar
- Remove JNDI usages and convert configuration from
context.xml
and JNDI to environment variables - Add a
docker-compose.yml
for the stack definition (here outlined for development with a database as part of the stack)
version: '3.7'
services:
my-app:
container_name: my-app-application
image: ${IMAGE_TAG}
environment:
- GRAILS_ENV=development
- DB_URL=jdbc:postgresql://my-app-db:5432/my-app
volumes:
- my-appdata:/app/data_home
ports:
- 8080:8080
depends_on:
- my-app-db
- my-app-elastic
my-app-db:
container_name: my-app-database-stack
image: postgres:13
environment:
- POSTGRES_USER=my-app
- POSTGRES_PASSWORD=my-db-password
- POSTGRES_DB=my-app
ports:
- 5432:5432
my-app-elastic:
container_name: my-app-elastic-stack
image: docker.elastic.co/elasticsearch/elasticsearch:7.8.0
environment:
- node.name=es01
- cluster.name=my-app-es
- discovery.type=single-node
- bootstrap.memory_lock=true
- "ES_JAVA_OPTS=-Xms512m -Xmx512m"
ulimits:
memlock:
soft: -1
hard: -1
volumes:
- searchdata:/usr/share/elasticsearch/data
ports:
- 9200:9200
- 9300:9300
volumes:
searchdata:
my-appdata:
Conclusion
The journey from classical deployments to a dockerized environment using a docker stack is not that long for a Grails application (or most other Java web applications). It makes it trivial setting up a new instance or migrating it to another host as most configuration is in version controlled files and there is next to none snowflaking. The customer has an easier time running the web application because the requirements are only a container-runtime and explicitly formulated in the stack definition.
In addition, the developers are free to choose the JVM and other details within the containers without having to cross-check with the administrators of the customer if they are supported by their organization.