Search code examples
gradlegrails

How do I install grails in a Docker image?


I'm working on containerizing an existing application built on grails. I've been able to successfully build the project within a debian:stretch image (yes, I know that's old, but the project was originally built with old versions of just about everything) with gradle, maven, and default-jdk installed. However I'm unsure if that means I don't need grails. When I try grails --version from a command line prompt within the container it says "bash: grails: command not found".

There does not appear to be a separate package for grails that I can install using apt and all references seem to say to install grails via sdkman.

I'm not at all familiar with grails or groovy so I'm unsure of how to proceed. Any advice on how to install (or maybe verify installation?) of grails? If it's needed, how did the build succeed without it?


Solution

  • This is what we use in our environments:

    docker-compose.yml

    version: '3.7'
    
    services:
      db:
        image: postgres:12.3
        environment:
          - POSTGRES_DB=databasename
          - POSTGRES_USER=postgres
          - POSTGRES_PASSWORD=postgres
        ports:
          - 5432:5432
        networks:
          - mynetwork
    
      backend:
        build:
          context: .
          target: development
        ports:
          - 8080:8080
          - 8083:8443
          - 5006:5005
        environment:
          - DB_HOST=db
          - DB_NAME=databasename
          - DB_PORT=5432
          - DB_USERNAME=postgres
          - DB_PASSWORD=postgres
        volumes:
          - grails-volume:/root/.m2
          - grails-volume:/root/.gradle
          - grails-volume:/root/.grails
          - ./grails-app:/app/grails-app
          - ./src:/app/src
          - ./version.txt:/version.txt
        entrypoint: ["sh", "-c", "./wait-for-it.sh db:5432 -t 30 -- grails run-app"]
        command: [""]
        depends_on:
          - db
        networks:
          - mynetwork
    
    volumes:
      grails-volume:
    
    networks:
      mynetwork:
    

    Dockerfile:

    # Image to start project and initialize dependencies.
    FROM openjdk:8 AS initializer
    ENV GRAILS_VERSION 4.0.3
    # Install Grails
    WORKDIR /usr/lib/jvm
    RUN ls -l
    RUN wget https://github.com/grails/grails-core/releases/download/v$GRAILS_VERSION/grails-$GRAILS_VERSION.zip && \
        unzip grails-$GRAILS_VERSION.zip && \
        rm -rf grails-$GRAILS_VERSION.zip && \
        ln -s grails-$GRAILS_VERSION grails
    # Setup Grails path.
    ENV GRAILS_HOME /usr/lib/jvm/grails
    ENV PATH $GRAILS_HOME/bin:$PATH
    ENV GRADLE_USER_HOME /app/.gradle
    # Create minimal structure to trigger grails build with specified profile.
    RUN mkdir /app \
        && mkdir /app/grails-app \
        && mkdir /app/grails-app/conf \
        && echo "grails.profile: rest-api" > /app/grails-app/conf/application.yml
    # Set Workdir
    WORKDIR /app
    # Copy minimun files to trigger grails download of wrapper and dependencies.
    COPY gradle.properties build.gradle /app/
    # Trigger gradle build
    RUN [ "grails", "stats" ]
    
    # Implemented to improve cache in CI
    FROM initializer as development
    # Add wait-for-it ro wait for database
    COPY wait-for-it.sh .
    RUN ["chmod", "+x", "./wait-for-it.sh"]
    # Copy source code
    COPY grails-app /app/grails-app
    COPY src /app/src
    # Set Default Behavior
    ENTRYPOINT ["./wait-for-it.sh", "db:5432", "-t", "30", "--", "grails", "run-app", "--debug-jvm"]
    CMD [ "" ]
    
    # Image used to build prod war
    FROM development AS builder
    # Build project
    RUN [ "grails", "prod", "war" ]
    RUN ls -l /app/build/libs
    
    # Production image
    FROM openjdk:8-jdk AS production
    # Set correct timezone
    ENV TZ=America/Argentina/Cordoba
    RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone
    # Add wait-for-it ro wait for database
    COPY wait-for-it.sh .
    RUN ["chmod", "+x", "/wait-for-it.sh"]
    # Copy war inside container
    COPY --from=builder /app/build/libs/app-*.war app.war
    # Expose default port
    EXPOSE 8080
    # Wait for database to be available
    ENTRYPOINT ["/wait-for-it.sh", "db-service:5432", "-t", "30", "--"]
    # War runs directly. (Uses urandom as entropy source for faster startup time)
    CMD ["java","-Djava.security.egd=file:/dev/./urandom","-jar","/app.war"]
    

    wait-for-it.sh:

    #!/usr/bin/env bash
    #   Use this script to test if a given TCP host/port are available
    
    WAITFORIT_cmdname=${0##*/}
    
    echoerr() { if [[ $WAITFORIT_QUIET -ne 1 ]]; then echo "$@" 1>&2; fi }
    
    usage()
    {
        cat << USAGE >&2
    Usage:
        $WAITFORIT_cmdname host:port [-s] [-t timeout] [-- command args]
        -h HOST | --host=HOST       Host or IP under test
        -p PORT | --port=PORT       TCP port under test
                                    Alternatively, you specify the host and port as host:port
        -s | --strict               Only execute subcommand if the test succeeds
        -q | --quiet                Don't output any status messages
        -t TIMEOUT | --timeout=TIMEOUT
                                    Timeout in seconds, zero for no timeout
        -- COMMAND ARGS             Execute command with args after the test finishes
    USAGE
        exit 1
    }
    
    wait_for()
    {
        if [[ $WAITFORIT_TIMEOUT -gt 0 ]]; then
            echoerr "$WAITFORIT_cmdname: waiting $WAITFORIT_TIMEOUT seconds for $WAITFORIT_HOST:$WAITFORIT_PORT"
        else
            echoerr "$WAITFORIT_cmdname: waiting for $WAITFORIT_HOST:$WAITFORIT_PORT without a timeout"
        fi
        WAITFORIT_start_ts=$(date +%s)
        while :
        do
            if [[ $WAITFORIT_ISBUSY -eq 1 ]]; then
                nc -z $WAITFORIT_HOST $WAITFORIT_PORT
                WAITFORIT_result=$?
            else
                (echo > /dev/tcp/$WAITFORIT_HOST/$WAITFORIT_PORT) >/dev/null 2>&1
                WAITFORIT_result=$?
            fi
            if [[ $WAITFORIT_result -eq 0 ]]; then
                WAITFORIT_end_ts=$(date +%s)
                echoerr "$WAITFORIT_cmdname: $WAITFORIT_HOST:$WAITFORIT_PORT is available after $((WAITFORIT_end_ts - WAITFORIT_start_ts)) seconds"
                break
            fi
            sleep 1
        done
        return $WAITFORIT_result
    }
    
    wait_for_wrapper()
    {
        # In order to support SIGINT during timeout: http://unix.stackexchange.com/a/57692
        if [[ $WAITFORIT_QUIET -eq 1 ]]; then
            timeout $WAITFORIT_BUSYTIMEFLAG $WAITFORIT_TIMEOUT $0 --quiet --child --host=$WAITFORIT_HOST --port=$WAITFORIT_PORT --timeout=$WAITFORIT_TIMEOUT &
        else
            timeout $WAITFORIT_BUSYTIMEFLAG $WAITFORIT_TIMEOUT $0 --child --host=$WAITFORIT_HOST --port=$WAITFORIT_PORT --timeout=$WAITFORIT_TIMEOUT &
        fi
        WAITFORIT_PID=$!
        trap "kill -INT -$WAITFORIT_PID" INT
        wait $WAITFORIT_PID
        WAITFORIT_RESULT=$?
        if [[ $WAITFORIT_RESULT -ne 0 ]]; then
            echoerr "$WAITFORIT_cmdname: timeout occurred after waiting $WAITFORIT_TIMEOUT seconds for $WAITFORIT_HOST:$WAITFORIT_PORT"
        fi
        return $WAITFORIT_RESULT
    }
    
    # process arguments
    while [[ $# -gt 0 ]]
    do
        case "$1" in
            *:* )
            WAITFORIT_hostport=(${1//:/ })
            WAITFORIT_HOST=${WAITFORIT_hostport[0]}
            WAITFORIT_PORT=${WAITFORIT_hostport[1]}
            shift 1
            ;;
            --child)
            WAITFORIT_CHILD=1
            shift 1
            ;;
            -q | --quiet)
            WAITFORIT_QUIET=1
            shift 1
            ;;
            -s | --strict)
            WAITFORIT_STRICT=1
            shift 1
            ;;
            -h)
            WAITFORIT_HOST="$2"
            if [[ $WAITFORIT_HOST == "" ]]; then break; fi
            shift 2
            ;;
            --host=*)
            WAITFORIT_HOST="${1#*=}"
            shift 1
            ;;
            -p)
            WAITFORIT_PORT="$2"
            if [[ $WAITFORIT_PORT == "" ]]; then break; fi
            shift 2
            ;;
            --port=*)
            WAITFORIT_PORT="${1#*=}"
            shift 1
            ;;
            -t)
            WAITFORIT_TIMEOUT="$2"
            if [[ $WAITFORIT_TIMEOUT == "" ]]; then break; fi
            shift 2
            ;;
            --timeout=*)
            WAITFORIT_TIMEOUT="${1#*=}"
            shift 1
            ;;
            --)
            shift
            WAITFORIT_CLI=("$@")
            break
            ;;
            --help)
            usage
            ;;
            *)
            echoerr "Unknown argument: $1"
            usage
            ;;
        esac
    done
    
    if [[ "$WAITFORIT_HOST" == "" || "$WAITFORIT_PORT" == "" ]]; then
        echoerr "Error: you need to provide a host and port to test."
        usage
    fi
    
    WAITFORIT_TIMEOUT=${WAITFORIT_TIMEOUT:-15}
    WAITFORIT_STRICT=${WAITFORIT_STRICT:-0}
    WAITFORIT_CHILD=${WAITFORIT_CHILD:-0}
    WAITFORIT_QUIET=${WAITFORIT_QUIET:-0}
    
    # check to see if timeout is from busybox?
    WAITFORIT_TIMEOUT_PATH=$(type -p timeout)
    WAITFORIT_TIMEOUT_PATH=$(realpath $WAITFORIT_TIMEOUT_PATH 2>/dev/null || readlink -f $WAITFORIT_TIMEOUT_PATH)
    if [[ $WAITFORIT_TIMEOUT_PATH =~ "busybox" ]]; then
            WAITFORIT_ISBUSY=1
            WAITFORIT_BUSYTIMEFLAG="-t"
    
    else
            WAITFORIT_ISBUSY=0
            WAITFORIT_BUSYTIMEFLAG=""
    fi
    
    if [[ $WAITFORIT_CHILD -gt 0 ]]; then
        wait_for
        WAITFORIT_RESULT=$?
        exit $WAITFORIT_RESULT
    else
        if [[ $WAITFORIT_TIMEOUT -gt 0 ]]; then
            wait_for_wrapper
            WAITFORIT_RESULT=$?
        else
            wait_for
            WAITFORIT_RESULT=$?
        fi
    fi
    
    if [[ $WAITFORIT_CLI != "" ]]; then
        if [[ $WAITFORIT_RESULT -ne 0 && $WAITFORIT_STRICT -eq 1 ]]; then
            echoerr "$WAITFORIT_cmdname: strict mode, refusing to execute subprocess"
            exit $WAITFORIT_RESULT
        fi
        exec "${WAITFORIT_CLI[@]}"
    else
        exit $WAITFORIT_RESULT
    fi
    

    application.yml

    #...Omitted code...
    environments:
        development:
            dataSource:
                driverClassName: org.postgresql.Driver
                dialect: org.hibernate.dialect.PostgreSQLDialect
                # dbCreate: create-drop
                dbCreate: update
                url: jdbc:postgresql://${DB_HOST}:${DB_PORT}/${DB_NAME}
        test:
            dataSource:
                dbCreate: update
                url: jdbc:h2:mem:testDb;MVCC=TRUE;LOCK_TIMEOUT=10000;DB_CLOSE_ON_EXIT=FALSE
        production:
            dataSource:
                driverClassName: org.postgresql.Driver
                dialect: org.hibernate.dialect.PostgreSQLDialect
                dbCreate: update
                url: jdbc:postgresql://${DB_HOST}:${DB_PORT}/${DB_NAME}
    

    In development you would use the docker-compose file to run the image (note that it has some volumes in order to avoid the need to restart the app when you change it): docker-compose up -d backend

    To generate an image for production: docker build -t yourdockerhubuser/yourproject:versionnumber .

    The wait-for-it.sh file is not needed to generate a Docker image. But it is useful to us in order to make the grails service to wait some seconds until the database service is ready to accept connections...

    If you take a look at the Dockerfile you would note that is a multistage file. That means that has like several stages on it. The first ones has grails tools on it. The last one is the one used on production and has only the jdk8 (and probably the jre would be enough). That way you don't pollute the production image resulting in a smaller image.