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:

# 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.