gradle-docker-plugin icon indicating copy to clipboard operation
gradle-docker-plugin copied to clipboard

How to pass docker environment variables into java application?

Open vleushin opened this issue 4 years ago • 4 comments

Hello! As part of migrating from Java 12 to Java 14 I'm updating gradle-docker-plugin straight from 3.6.2 to 6.6.1.

While overall everything seems OK, we can remove a lot of old quirks (like setting and pushing latest tag) the only problem we currently have is that it is not clear how to pass docker environment variable into java application.

Old Dockerfile:

FROM openjdk:12
LABEL maintainer=vleushin
ADD myapp-node /myapp-node
ADD app-lib/myapp-node-2.18.14.jar /myapp-node/lib/myapp-node-2.18.14.jar
ENTRYPOINT ["/myapp-node/bin/myapp-node"]
EXPOSE 8080

New Dockerfile:

FROM openjdk:14
LABEL maintainer=vleushin
WORKDIR /app
COPY libs libs/
COPY resources resources/
COPY classes classes/
ENTRYPOINT ["java", "-cp", "/app/resources:/app/classes:/app/libs/*", "com.example.MyAppNode"]
EXPOSE 8080

As I understood, as part of optimisation and layering improvements -- there are new layers -- libs, resources, classes. That's all great. But the problem is ENTRYPOINT.

In old Dockerfile java application startup script has following line at the end:

# Collect all arguments for the java command, following the shell quoting and substitution rules
eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $MYAPP_NODE_OPTS -classpath "\"$CLASSPATH\"" com.example.MyAppNode "$APP_ARGS"

So it is passing DEFAULT_JVM_OPTS, JAVA_OPTS and MYAPP_NODE_OPTS into java application. Which is great way to customize docker image to run on specific environment or with specific features. We use it heavily afterwards in Kubernetes -- when we specify in deployment image, and using environment variable to pass different java flags, DEV/PROD environment, etc.

Quick example of Kubernetes deployment:

      containers:
      - name: myapp-node
        image: myapp/myapp-node:2.18.20
        env:
        - name: MYAPP_ENV
          valueFrom:
            configMapKeyRef:
              name: myapp-config
              key: env
        - name: JAVA_OPTS
          value: "-XX:+UseContainerSupport -XX:MaxRAMPercentage=40.0 -Dsun.jnu.encoding=UTF-8 -Dfile.encoding=UTF-8 -Darchaius.deployment.environment=k8s-${MYAPP_ENV} -Des.set.netty.runtime.available.processors=false -Dmyapp.node.services.excluded=GeoService,TextService -Dio.netty.allocator.useCacheForAllThreads=false -Dlog4j.configurationFile=log4j2-k8s-${MYAPP_ENV}.xml -DLog4jContextSelector=org.apache.logging.log4j.core.async.AsyncLoggerContextSelector -Dcom.sun.management.jmxremote.port=9999 -Dcom.sun.management.jmxremote.rmi.port=9999 -Dcom.sun.management.jmxremote.authenticate=false -Dcom.sun.management.jmxremote.ssl=false -Dcom.sun.management.jmxremote.local.only=false -Djava.rmi.server.hostname=127.0.0.1 -Djava.net.preferIPv4Stack=true -Dsun.net.inetaddr.ttl=5 -Dnetworkaddress.cache.ttl=5 -XX:UseAVX=2 -XX:+ExitOnOutOfMemoryError"

It is even one step ahead since MYAPP_ENV -- is config variable in Kubernetes cluster (we have one for DEV, and one for PROD), and it is propagated into deployment, and via JAVA_OPTS into java application, so we have same Kubernetes deployment for Dev and Prod.

Currently I do not see how to do it with new gradle-docker-plugin. It is not possible to use environment variables in ENTRYPOINT. I can only hardcode values (using jvmArgs) it in the image, but then we have to make a lot of images and it is not customizable.

Please advise how to achieve this in new version, maybe I'm just missing something.

vleushin avatar Sep 10 '20 06:09 vleushin

Did you solve this issue ?

eliran-mic avatar Mar 09 '21 09:03 eliran-mic

you can use docker-remote-api in gradle.build

...
apply plugin: 'com.bmuschko.docker-remote-api'
...
import com.bmuschko.gradle.docker.tasks.image.Dockerfile
import com.bmuschko.gradle.docker.tasks.image.DockerBuildImage

jar {
    archivesBaseName = 'my-app-name'
    manifest { attributes "Main-Class": "com....MyAPPApplication" }
}

task createDockerfile(type: Dockerfile) {
    from('openjdk:11-jre')
    environmentVariable("APP_JAR", "${rootProject.name}-${version}.jar")
   //you need build jar to copy
    addFile("\"./build/libs/${rootProject.name}-${version}.jar\"", "/app.jar")
    entryPoint('sh')
    defaultCommand('-c', 'java $JAVA_OPTS -Djava.security.egd=file:/dev/./urandom -jar /app.jar')
    exposePort(8080)
}

task buildDocker(type: DockerBuildImage) {
    dependsOn createDockerfile
    dockerFile = createDockerfile.destFile
    inputDir = project.projectDir
    images.add("$project.group/my-app-name:$project.version")
}

and run it with

./gradlew buildDocker

Jorgevillada avatar Mar 17 '21 18:03 Jorgevillada

@vleushin This is what I figured it out as a workaround The idea is re-construct the ENTRYPOINT by sh -c "java $JAVA_OPTS -cp xxxx Main $JAVA_ARGS"

    afterEvaluate {

        // refine ENTRYPOINT for java
        project.tasks.withType(com.bmuschko.gradle.docker.tasks.image.Dockerfile).forEach({

            it.instructions = it.instructions.get()
                    .stream()
                    .map(inst -> {
                        if (inst.keyword != 'ENTRYPOINT') {
                            return inst
                        }

                        def javacmd = groovy.util.Eval.me(inst.text.replaceFirst('ENTRYPOINT ', "")).toArray()
                        javacmd[0] = 'java $JAVA_OPTS'    // [0] == 'java'

                        return new Dockerfile.EntryPointInstruction("sh", "-c", Stream.of(javacmd).collect(Collectors.joining(" ")) + ' $JAVA_ARG')
                    })
                    .collect(Collectors.toList())
        })
    }

fuminchao avatar Jun 22 '21 14:06 fuminchao

PR is created https://github.com/bmuschko/gradle-docker-plugin/pull/1021

fuminchao avatar Jun 23 '21 05:06 fuminchao