Running Play in an alpine-based Docker container


#1

Has anyone had success running Play on an alpine-based Docker container? It seems broken per https://github.com/playframework/playframework/issues/8282.


(Rich Dougherty) #2

Sounds like a useful thing to get working! I personally haven’t had any experience with that. It sounds like Play or sbt-native-packager might need a patch. ;)


#3

I got it working with the below in build.sbt, by inspecting the docker commands via show dockerCommands in sbt and then duplicating it, with the addition of adding bash.

Now, the point of these minimal Alpine releases is that they don’t have bash :) e.g. when there was the “ShellShock” bash vulnerability, these images were unaffected because they don’t have bash… that’s desirable.

It would be great to support these images out of the box. Is this a Play issue or a sbt-package-manager issue?

Update: Also, the docker image size (with just the base image and a minimal Play app) is something like 859 MB for the standard image and 229 MB for the alpine-based one. Big difference.

import com.typesafe.sbt.packager.docker._

dockerCommands := Seq(
  Cmd("FROM", "openjdk:8-jre-alpine"),
  Cmd("RUN", "apk --no-cache add bash"),
  Cmd("WORKDIR", "/opt/docker"),
  Cmd("ADD", "--chown=daemon:daemon opt /opt"),
  Cmd("USER", "daemon"),
  Cmd("ENTRYPOINT", """["bin/foo-service"]"""),
  Cmd("CMD", """[]""")
)

(Rich Dougherty) #4

If you want this recipe to be easily reusable you write a small sbt plugin that basically just adds these settings to a Play project. Here’s how to write a plugin: https://www.scala-sbt.org/1.0/docs/Plugins.html. Then users who want to use it would just need to add your plugin to their projects!


(Schmitt Christian) #5

Actually using alpine based images is not the best idea for JRE stuff.
It’s better to use sbt-assembly and use gcr.io/distroless/java.

Something like that:

FROM MY_SBT_BUILD_IMAGE
ARG JAVA_OPTS
ADD . /app
WORKDIR /app
RUN /usr/local/sbt/bin/sbt server/assembly
RUN mkdir -p /out && mv server/target/scala-2.12/server.jar /out

FROM gcr.io/distroless/java
COPY --from=build-env /out /app
WORKDIR /app
EXPOSE 9000
CMD ["server.jar"]

#6

I consider what I did a hack - if the underlying docker commands change in sbt-package-manager, the above code (or plugin) would not reflect that.

It would be great to get Play apps running on a stock Alpine-based docker image. This means looking into the runscript generation. I think this means there’s an issue here: https://github.com/sbt/sbt-native-packager/blob/master/src/main/scala/com/typesafe/sbt/packager/archetypes/scripts/AshScriptPlugin.scala


#7

Could you please expand on why (using alpine based images is not good with JRE)? There are official alpine-based ‘openjdk’ docker images… Doing a brief search, I see a number of posts which recommend this route:

https://medium.com/@hudsonmendes/docker-spring-boot-choosing-the-base-image-for-java-8-9-microservices-on-linux-and-windows-c459ec0c238]


(Schmitt Christian) #8

Well first of all alpine still comes with tools you don’t need (package manager, etc.)

Second a docker image should be as small as possible. and a sbt-assembly image with distroless can be even smaller.

(Well alpine is fine, ok-ish and at least better than most options, but removing any distro is even better and mostly more secure)
(Also your last link basically does the same I did with distroless, just with jdk9 (which does not work that “good” with play (yet)))

Of course the best image would be some kind of image where you have the following layers:

  1. base image with jre
  2. image with play dependencies & your dependencies
  3. your application jar

of course this won’t work sbt-assembly but it will make your application even better deployable.
There are upsides and downsides of both approaches.
The first approach adopts for a overall “smallness”, while the second tries to minimize diff size if your deps won’t change.


(Rich Dougherty) #9

Can you raise an issue over there on that project?


(Schmitt Christian) #10

There is already an issue:

However it’s probably Play related.


(Schmitt Christian) #11

Actually one of the “best” ways at the moment is probably to just run play with the following under docker:

FROM gcr.io/distroless/java
COPY target/universal/stage/lib/* /app/lib/
COPY target/universal/stage/conf/ /app/conf/
WORKDIR /app
EXPOSE 9000
ENTRYPOINT ["java"]
CMD ["-Duser.dir=/app", "-cp", "conf/:lib/*", "play.core.server.ProdServerStart"]

create with:

sbt stage && docker build -t YOUR_TAG .

(it would probably be even more sane to actually have mutliple layer with your library, so that consider your application jars will be added after dependency jars, this makes your image bigger but it will use less space while upgrading, but I didn’t have time to fine tune that.)


#12

It’s also strongly suggested to add -XX:+UnlockExperimentalVMOptions and -XX:+UseCGroupMemoryLimitForHeap Java options to startup, to allow JVM to see correct amount of memory inside the container.


#14

Perhaps if you’re making use of -XX:MaxRAMFraction . I tried running that in production for some time (on Java 8) and found it to be unreliable. I know that Java 10 comes with additional Docker-aware features that are much more flexible (than -XX:MaxRAMFraction ), but I have not tried these. I have gone back to manually specifying the max heap size (taking care that this is aligned with what the container is allowed). On some services, I have additionally switched to G1GC.


#15

Full solution gathered from posts of @schmitch then tweaked & tested on live system, ready to roll:

FROM mozilla/sbt
WORKDIR /build
ADD . .
RUN sbt stage

FROM gcr.io/distroless/java
WORKDIR /app
COPY --from=0 /build/target/universal/stage/lib/* ./lib/
COPY --from=0 /build/target/universal/stage/conf/ ./conf/
EXPOSE 9000
ENTRYPOINT ["java", "-Duser.dir=/app", "-XX:+UnlockExperimentalVMOptions", "-XX:+UseCGroupMemoryLimitForHeap", "-cp", "conf/:lib/*"]
CMD ["play.core.server.ProdServerStart"]

Build with (no sbt necessary on host):
docker -t repo:tag .

Run with:
docker run -d -p 9000:9000 repo:tag -Dplay.http.secret.key=mysecretkey play.core.server.ProdServerStart

The only downside of this multi-stage build would be long build times due to sbt re-downloading everything on every build. However, not having to install sbt on the host for the build could be useful for some projects.

I’ve used mozilla’s sbt image but you can build yourself a newer image or use another one if you like.


(Cyril Franceschini) #16

My Docker config working in prod:

import com.typesafe.sbt.packager.docker._

lazy val root = (project in file(".")).enablePlugins(PlayJava, DockerPlugin)

daemonUser in Docker := "root"
dockerBaseImage := "openjdk:8-jre-alpine"
dockerRepository := Some("yourDockerRepo")
dockerExposedPorts := Seq(80)
dockerEntrypoint := Seq("bin/%s" format executableScriptName.value)
dockerCmd := Seq("-Dconfig.resource=prod.conf", "-Dpidfile.path=/dev/null", "-J-XX:+UnlockExperimentalVMOptions", "-J-XX:+UseCGroupMemoryLimitForHeap")
dockerCommands := dockerCommands.value.flatMap {
  case cmd@Cmd("FROM", _) => List(cmd, ExecCmd("RUN", "apk", "--update", "add", "bash"))
  case other => List(other)
}

You must login with docker login before and then:

sbt docker:publish

or to run it locally:

sbt docker:publishLocal


#17

This seems quite old, just wanted to share that I’ve had success in the past running Play 2.6 on top of the Phusion base image:

Definitely word the read.


(Yurir) #18

We use openjdk:8u181-jre-slim image in kubernetes with flags:
"-XX:+UnlockExperimentalVMOptions", "-XX:+UseCGroupMemoryLimitForHeap"
It does the job, but it’s based on debian:stretch-slim image, not alpine.