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:
-
Download and unzip the source
or
-
Clone the Git repository:
git clone https://github.com/grails-guides/grails-docker-external-services.git
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 theinitial
folder.
To complete the guide, go to the initial
folder
-
cd
intograils-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.
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.
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.
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
:
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
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:
provided "org.postgresql:postgresql:9.4.1211.jre7"
Configure the dataSource
of the development environment to use the PostgreSQL database running inside Docker.
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.
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
:
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
:
#!/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:
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