Docker is a platform to build and run distributed applications. We can use Docker to package our Grails application, including all dependencies, as a Docker image. We can then use that image to run the application on the Docker platform. This way the only dependency for running our Grails applications is the availability of a Docker engine. And with Grails 3 it is very easy to create a runnable JAR file and use that JAR file in a Docker image. Because Grails 3 now uses Gradle as build system we can even automate all the necessary steps by using the Gradle Docker plugin.
Let's see an example of how we can make our Grails application runnable as Docker container. As extra features we want to be able to specify the Grails environment as a Docker environment variable, so we can re-use the same Docker image for different Grails environments. Next we want to be able to pass extra command line arguments to the Grails application when we run the Docker container with our application. For example we can then specify configuration properties as docker run ... --dataSource.url=jdbc:h2:./mainDB
. Finally we want to be able to specify an external configuration file with properties that need to be overridden, without changing the Docker image.
We start with a simple Grails application and make some changes to the grails-app/conf/application.yml
configuration file and the grails-app/views/index.gsp
, so we can test the support for changing configuration properties:
# File: grails-app/conf/application.yml --- # Extra configuration property. # Value is shown on grails-app/views/index.gsp. app: welcome: header: Grails sample application ...
... <g:set var="config" value="${grailsApplication.flatConfig}"/> <h1>${config['app.welcome.header']}</h1> <p> This Grails application is running in Docker container <b>${config['app.dockerContainerName']}</b>. </p> ...
Next we create a new Gradle build file gradle/docker.gradle
. This contains all the tasks to package our Grails application as a runnable JAR file, create a Docker image with this JAR file and extra tasks to create and manage Docker containers for different Grails environment values.
// File: gradle/docker.gradle buildscript { repositories { jcenter() } dependencies { // Add Gradle Docker plugin. classpath 'com.bmuschko:gradle-docker-plugin:2.6.1' } } // Add Gradle Docker plugin. // Use plugin type, because this script is used with apply from: // in main Gradle build script. apply plugin: com.bmuschko.gradle.docker.DockerRemoteApiPlugin ext { // Define tag for Docker image. Include project version and name. dockerTag = "mrhaki/${project.name}:${project.version}".toLowerCase() // Base name for Docker container with Grails application. dockerContainerName = 'grails-sample' // Staging directory for create Docker image. dockerBuildDir = mkdir("${buildDir}/docker") // Group name for tasks related to Docker. dockerBuildGroup = 'Docker' } docker { // Set Docker host URL based on existence of environment // variable DOCKER_HOST. url = System.env.DOCKER_HOST ? System.env.DOCKER_HOST.replace("tcp", "https") : 'unix:///var/run/docker.sock' } import com.bmuschko.gradle.docker.tasks.image.Dockerfile import com.bmuschko.gradle.docker.tasks.image.DockerBuildImage import com.bmuschko.gradle.docker.tasks.image.DockerRemoveImage import com.bmuschko.gradle.docker.tasks.container.DockerCreateContainer import com.bmuschko.gradle.docker.tasks.container.DockerStartContainer import com.bmuschko.gradle.docker.tasks.container.DockerStopContainer import com.bmuschko.gradle.docker.tasks.container.DockerRemoveContainer task dockerRepackage(type: BootRepackage, dependsOn: jar) { description = 'Repackage Grails application JAR to make it runnable.' group = dockerBuildGroup ext { // Extra task property with file name for the // repackaged JAR file. // We can reference this extra task property from // other tasks. dockerJar = file("${dockerBuildDir}/${jar.archiveName}") } outputFile = dockerJar withJarTask = jar } task prepareDocker(type: Copy, dependsOn: dockerRepackage) { description = 'Copy files from src/main/docker to Docker build dir.' group = dockerBuildGroup into dockerBuildDir from 'src/main/docker' } task createDockerfile(type: Dockerfile, dependsOn: prepareDocker) { description = 'Create Dockerfile to build image.' group = dockerBuildGroup destFile = file("${dockerBuildDir}/Dockerfile") // Contents of Dockerfile: from 'java:8' maintainer 'Hubert Klein Ikkink "mrhaki"' // Expose default port 8080 for Grails application. exposePort 8080 // Create environment variable so we can customize the // grails.env Java system property via Docker's environment variable // support. We can re-use this image for different Grails environment // values with this construct. environmentVariable 'GRAILS_ENV', 'production' // Create a config directory and expose as volume. // External configuration files in this volume are automatically // picked up. runCommand 'mkdir -p /app/config' volume '/app/config' // Working directory is set, so next commands are executed // in the context of /app. workingDir '/app' // Copy JAR file from dockerRepackage task that was generated in // build/docker. copyFile dockerRepackage.dockerJar.name, 'application.jar' // Copy shell script for starting application. copyFile 'docker-entrypoint.sh', 'docker-entrypoint.sh' // Make shell script executable in container. runCommand 'chmod +x docker-entrypoint.sh' // Define ENTRYPOINT to execute shell script. // By using ENTRYPOINT we can add command line arguments // when we run the container based on this image. entryPoint './docker-entrypoint.sh' } task buildImage(type: DockerBuildImage, dependsOn: createDockerfile) { description = 'Create Docker image with Grails application.' group = dockerBuildGroup inputDir = file(dockerBuildDir) tag = dockerTag } task removeImage(type: DockerRemoveImage) { description = 'Remove Docker image with Grails application.' group = dockerBuildGroup targetImageId { dockerTag } } //------------------------------------------------------------------------------ // Extra tasks to create, run, stop and remove containers // for a development and production environment. //------------------------------------------------------------------------------ ['development', 'production'].each { environment -> // Transform environment for use in task names. final String taskName = environment.capitalize() // Name for container contains the environment name. final String name = "${dockerContainerName}-${environment}" task "createContainer$taskName"(type: DockerCreateContainer) { description = "Create Docker container $name with grails.env $environment." group = dockerBuildGroup targetImageId { dockerTag } containerName = name // Expose port 8080 from container to outside as port 8080. portBindings = ['8080:8080'] // Set environment variable GRAILS_ENV to environment value. // The docker-entrypoint.sh script picks up this environment // variable and turns it into Java system property // -Dgrails.env. env = ["GRAILS_ENV=$environment"] // Example of adding extra command line arguments to the // java -jar app.jar that is executed in the container. cmd = ["--app.dockerContainerName=${containerName}"] // The image has a volume /app/config for external configuration // files that are automatically picked up by the Grails application. // In this example we use a local directory with configuration files // on our host and bind it to the volume in the container. binds = [ (file("$projectDir/src/main/config/${environment}").absolutePath): '/app/config'] } task "startContainer$taskName"(type: DockerStartContainer) { description = "Start Docker container $name." group = dockerBuildGroup targetContainerId { name } } task "stopContainer$taskName"(type: DockerStopContainer) { description = "Stop Docker container $name." group = dockerBuildGroup targetContainerId { name } } task "removeContainer$taskName"(type: DockerRemoveContainer) { description = "Remove Docker container $name." group = dockerBuildGroup targetContainerId { name } } }
We also must add a supporting shell script file to the directory src/main/docker
with the name docker-entrypoint.sh
. This script file makes it possible to specify a different Grails environment variable with environment variable GRAILS_ENV
. The value is transformed to a Java system property -Dgrails.env={value}
when the Grails application starts. Also extra commands used to start the Docker container are appended to the command line:
#!/bin/bash set -e exec java -Dgrails.env=$GRAILS_ENV -jar application.jar $@
Now we only have to add an apply from: 'gradle/docker.gradle'
at the end of the Gradle build.gradle
file:
// File: build.gradle ... apply from: 'gradle/docker.gradle'
When we invoke the Gradle tasks
command we see all our new tasks. We must at least use Gradle 2.5, because the Gradle Docker plugin requires this (see also this issue):
... Docker tasks ------------ buildImage - Create Docker image with Grails application. createContainerDevelopment - Create Docker container grails-sample-development with grails.env development. createContainerProduction - Create Docker container grails-sample-production with grails.env production. createDockerfile - Create Dockerfile to build image. dockerRepackage - Repackage Grails application JAR to make it runnable. prepareDocker - Copy files from src/main/docker to Docker build dir. removeContainerDevelopment - Remove Docker container grails-sample-development. removeContainerProduction - Remove Docker container grails-sample-production. removeImage - Remove Docker image with Grails application. startContainerDevelopment - Start Docker container grails-sample-development. startContainerProduction - Start Docker container grails-sample-production. stopContainerDevelopment - Stop Docker container grails-sample-development. stopContainerProduction - Stop Docker container grails-sample-production. ...
Now we are ready to create a Docker image with our Grails application code:
$ gradle buildImage ... :compileGroovyPages :jar :dockerRepackage :prepareDocker :createDockerfile :buildImage Building image using context '/Users/mrhaki/Projects/grails-docker-sample/build/docker'. Using tag 'mrhaki/grails-docker-sample:1.0' for image. Created image with ID 'c1d0a600c933'. BUILD SUCCESSFUL Total time: 48.68 secs
We can check with docker images
if our image is created:
$ docker images REPOSITORY TAG IMAGE ID CREATED VIRTUAL SIZE mrhaki/grails-docker-sample 1.0 c1d0a600c933 4 minutes ago 879.5 MB
We can choose to create and run new containers based on this image with the Docker run
command. But we can also use the Gradle tasks createContainerDevelopment
and createContainerProduction
. These tasks will create containers that have predefined values for the Grails environment, a command line argument --app.dockerContainerName
and directory binding on our local computer to the container volume /app/config
. The local directory is src/main/config/development
or src/main/config/production
in our project directory. Any files placed in those directories will be available in our Docker container and are picked up by the Grails application. Let's add a new configuration file for each environment to override the configuration property app.welcome.header
:
# File: src/main/config/development/application.properties app.welcome.header=Dockerized Development Grails Application!
# File: src/main/config/production/application.properties app.welcome.header=Dockerized Production Grails Application!
Now we create two Docker containers:
$ gradle createContainerDevelopment createContainerProduction :createContainerDevelopment Created container with ID 'feb56c32e3e9aa514a208b6ee15562f883ddfc615292d5ea44c38f28b08fda72'. :createContainerProduction Created container with ID 'd3066d14b23e23374fa7ea395e14a800a38032a365787e3aaf4ba546979c829d'. BUILD SUCCESSFUL Total time: 2.473 secs $ docker ps -a CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES d3066d14b23e mrhaki/grails-docker-sample:1.0 "./docker-entrypoint." 7 seconds ago Created grails-sample-production feb56c32e3e9 mrhaki/grails-docker-sample:1.0 "./docker-entrypoint." 8 seconds ago Created grails-sample-development
First we start the container with the development configuration:
$ gradle startContainerDevelopment :startContainerDevelopment Starting container with ID 'grails-sample-development'. BUILD SUCCESSFUL Total time: 1.814 secs
In our web browser we open the index page of our Grails application and see how the current Grails environment is development and that the app.welcome.header
configuration property is used from our application.properties
file. We also see that the app.dockerContainerName
configuration property set as command line argument for the Docker container is picked up by Grails and shown on the page:
If we stop this container and start the production container we see different values:
The code is also available as Grails sample project on Github.
Written with Grails 3.0.9.