Show Navigation

Use docker to provide external services to your Grails app

Learn how to use a Postgresql database in your Grails application with a Docker container

Authors: Iván López,Sergio del Amo

Grails Version: 3.3.2

1 Training

Grails Training - Developed and delivered by the folks who created and actively maintain the Grails framework!.

2 Getting Started

If you do any real Grails Development, you will need to connect to external services; Databases, services such as ElasticSearch, Redis etc.

Usually, you needed to install those services on your development machine to replicate production environments. You could be in scenarios such as developing on MacOS a Grails application which connects to MSSQL database. Installing such external services could be difficult or impossible in your development environment.

Among many other things, Docker can help us simplify such a scenario. In this Guide, you will write a Grails application which connects to a PostgreSQL database running on a docker container. You will not install Postgres on your machine. Instead, you will pull a PostgreSQL image, create a container with that image and start the container. You will modify your Grails application to connect to the PostgreSQL database which runs in the container.

2.1 How to complete the guide

To get started do the following:

or

The Grails guides repositories contain two folders:

  • initial Initial project. Often a simple Grails app with some additional code to give you a head-start.

  • complete A completed example. It is the result of working through the steps presented by the guide and applying those changes to the initial folder.

To complete the guide, go to the initial folder

  • cd into grails-guides/grails-docker-external-services/initial

and follow the instructions in the next sections.

You can go right to the completed example if you cd into grails-guides/grails-docker-external-services/complete

3 Writing the Guide

3.1 Install and configure Docker

If you do not already have Docker installed, you’ll need to install it.

Depending on your environment you may need to increase Docker’s available memory to 4GB or more.

Docker Preferences

Tip: Get Kitematic

Kitematic is an excellent desktop application for managing Docker containers. The first time you click Open Kitematic, it will prompt you to download and install it. You can then use Kitematic to view the output of your containers, manage their settings, etc.

Kitematic

3.2 Domain Class

Create a Domain Class. It will help you verify that the database schema is created in the PostgreSQL database running inside the docker container.

grails-app/domain/demo/Book.groovy
package demo

import groovy.transform.CompileStatic

@CompileStatic
class Book {
    String title
}

3.3 Gradle Docker plugin

In this guide, we use the Gradle Docker Plugin; a Gradle plugin for managing Docker images and containers.

To install the Gradle plugin, modify build.gradle:

build.gradle
buildscript {
    repositories {
        mavenLocal()
        maven { url "https://repo.grails.org/grails/core" }
    }
    dependencies {
        classpath "org.grails:grails-gradle-plugin:$grailsVersion"
        classpath "gradle.plugin.com.energizedwork.webdriver-binaries:webdriver-binaries-gradle-plugin:1.1"
        classpath "gradle.plugin.com.energizedwork:idea-gradle-plugins:1.4"
        classpath "org.grails.plugins:hibernate5:${gormVersion-".RELEASE"}"
        classpath "com.bertramlabs.plugins:asset-pipeline-gradle:2.14.6"
        classpath "com.bmuschko:gradle-docker-plugin:3.2.1" (1)
    }
}
apply plugin:"eclipse"
apply plugin:"idea"
apply plugin:"war"
apply plugin:"org.grails.grails-web"
apply plugin:"com.energizedwork.webdriver-binaries"
apply plugin:"com.energizedwork.idea-project-components"
apply plugin:"asset-pipeline"
apply plugin:"org.grails.grails-gsp"
apply plugin:"com.bmuschko.docker-remote-api" (1)
1 Gradle Docker Plugin

3.4 Gradle Docker Tasks

Configure several Gradle tasks which allow us to pull a Docker image and create/start/stop a PostgreSQL container with Gradle

build.gradle
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.image.DockerPullImage
...
..
.
task pullPostgresImage(type: DockerPullImage) {
    group = 'docker' (1)
    ext {
        imageName = 'postgres' (2)
        imageTag = '9.6.6'  (3)
    }
    description = 'Pull PostgreSQL image'
    repository = imageName
    tag = imageTag
}
task createPostgresContainer(type: DockerCreateContainer, dependsOn: pullPostgresImage) {
    group = 'docker'  (1)
    ext {
        pgContainerName = "demo-postgres" (4)
        dbName = "demo_db" (5)
        dbPort = 5432 (6)
        dbPassword = 'dev_password'  (7)
    }
    description = 'Creates PostgreSQL container'
    containerName = pgContainerName
    imageId = "${pullPostgresImage.imageName}:${pullPostgresImage.tag}"
    portBindings = ["${dbPort}:5432"]
    env = [
        "POSTGRES_PASSWORD=${dbPassword}",
        "POSTGRES_DB=${dbName}",
    ] as String[]

    onError { e ->
        if (e.class.simpleName in ['BadRequestException', 'ConflictException']) {
            logger.warn 'Container already exists' (8)
        } else {
            throw e
        }
    }
}
task startPostgresContainer(type: DockerStartContainer, dependsOn: createPostgresContainer) {
    group = 'docker' (1)
    description = 'Starts Postgres container'
    containerId = createPostgresContainer.pgContainerName
    onError { e ->
        if (e.class.simpleName == 'NotModifiedException') {
            logger.warn 'Container already started'  (8)
        } else {
            throw e
        }
    }
    onComplete {
        logger.info "Postgres is listening on port ${createPostgresContainer.dbPort}"
    }
}
task stopPostgresContainer(type: DockerStopContainer) {
    group = 'docker' (1)
    description = 'Stops Postgres container'
    containerId = createPostgresContainer.pgContainerName
    onError { e ->
        if (e.class.simpleName == 'NotModifiedException') {
            logger.warn 'Container already stopped' (8)
        } else {
            throw e
        }
    }
}
1 Group related tasks in a group. It helps to visualize Gradle tasks when you run gradlew tasks --all
2 Docker Image Name
3 Docker Image Tag
4 Docker container name
5 Database name
6 Database port
7 Database user’s password. By default, username is postgres
8 Don’t fail if the container is already created, started or stopped and the Gradle task is invoked

3.5 Configure PostgreSQL

Modify build.gradle and add PostgreSQL dependency:

build.gradle
provided "org.postgresql:postgresql:9.4.1211.jre7"

Configure the dataSource of the development environment to use the PostgreSQL database running inside Docker.

grails-app/conf/application.yml
dataSource:
    dialect: org.hibernate.dialect.PostgreSQLDialect
    driverClassName: org.postgresql.Driver
    username: postgres
    password: dev_password
    dbCreate: update
    url: jdbc:postgresql://localhost:5432/demo_db

4 Running the app

Before running the application, first start PostgreSQL:

./gradlew startPostgresContainer

and then start the Grails App:

./gradlew bootRun

If you connect to the database, you will see the expected database schema.

Database created

bootRun depends on PostgreSQL

You could configure the bootRun task to depends on the startPostgresContainer task adding the following to build.gradle:

bootRun.dependsOn startPostgresContainer

This will create the next dependency path:

bootRun → startPostgresContainer → createPostgresContainer → pullPostgresImage

Such a path may be of interest for a continuous integration environment.

5 Using only Docker

There is another option that does not requires the usage of a Gradle plugin. With this approach we can just use plain Docker to build a Docker image and start a container.

Defining the Docker file

Create the following Dockerfile:

src/main/docker/Dockerfile
FROM postgres:9.6.6 (1)

VOLUME /var/lib/postgresql/data (2)

COPY ["setup.sh", "/docker-entrypoint-initdb.d/"] (3)

EXPOSE 5432 (4)
1 Use the same PostgreSQL image and version as before
2 Define a volume to store the data
3 Copy a script to the entrypoint directory. Any .sh or .sql file in this directory will be executing when starting the container
4 Expose the port

Also create the file setup.sh:

src/main/docker/setup.sh
#!/bin/bash
echo "######### CREATING DATABASE ##########"

# Perform all actions as user 'postgres'
export PGUSER=postgres

# Create specific users for the application and also the databases for dev and test
psql <<EOSQL
    (1)
    CREATE DATABASE dev_demo_db;
    CREATE ROLE dev_user WITH LOGIN PASSWORD 'dev_password';
    GRANT ALL PRIVILEGES ON DATABASE dev_demo_db TO dev_user;

    (2)
    CREATE DATABASE test_demo_db;
    CREATE ROLE test_user WITH LOGIN PASSWORD 'test_password';
    GRANT ALL PRIVILEGES ON DATABASE test_demo_db TO test_user;
EOSQL

echo ""
echo "######### DATABASE CREATED ##########"
1 Create the database, user and password for development environment
2 Define another database, user and password for the test environment

Building the image

With those files created, let’s build the docker image:

$ docker build -t postgres-custom src/main/docker/

Start a container

To create and start a container for the first time based on the image we have just created:

$ docker run -d -p 5432:5432 --name demo-postgres postgres-custom

From now on, every time we want to start/stop the container, we only need to execute:

$ docker start demo-postgres

$ docker stop demo-postgres
Keep in mind that you also need to change application.yml to use the new credentials defined in setup.sh file.

Replace the database development environment configuration with:

grails-app/conf/application.yml
environments:
    development:
        dataSource:
            dialect: org.hibernate.dialect.PostgreSQLDialect
            driverClassName: org.postgresql.Driver
            username: dev_user
            password: dev_password
            dbCreate: update
            url: jdbc:postgresql://localhost:5432/dev_demo_db

6 Help with Grails

OCI sponsored the creation of this Guide. OCI offers several Grails services:

Free consultation

The OCI Grails Team includes Grails co-founders, Jeff Scott Brown and Graeme Rocher. Check our Grails courses and learn from the engineers who developed, matured and maintain Grails.

Grails OCI Team