Search

Dark theme | Light theme

October 21, 2015

Grails Goodness: Run Grails Application As Docker Container

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:

 
1
2
3
4
5
6
7
# File: grails-app/conf/application.yml
---
# Extra configuration property.
# Value is shown on grails-app/views/index.gsp.
app:
    welcome:
        header: Grails sample application
...
 
1
2
3
4
5
6
7
8
...
<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.

 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
// 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:

 
1
2
3
#!/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:

 
1
2
// 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:

 
1
# File: src/main/config/development/application.properties
app.welcome.header=Dockerized Development Grails Application!
 
1
# 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.