If you don’t know Heroku already, you’ll get to love it soon! But wait – do you run Spring Boot apps based on JDK 11+? Do you build them with Maven 3.5.x? Maybe you should use Docker on Heroku – then this guide is for you!
Why I love Heroku
Using Heroku to run your apps is a really great experience! I fell in love with Heroku while running a charity project where we used a combination of hackathons and student support to build a holiday fun registration app for kids in my hometown. We first experimented with the tools the big cloud vendors provide themselves, but didn’t have the time to invest what was needed to achieve a fully working software development process. We only had one day to set up a full continuous delivery pipeline including a working infrastructure and database until 20 students would come and wanted to build software.
But with Heroku this worked like a charm! As we used Java & Spring Boot in the backend, we could simply rely on the great getting-started guides about Java on Heroku . And there’s also a great introductory article on how to deploy Spring Boot applications to Heroku by my former colleague Benedikt Ritter . To achieve a great development experience, just create a complementary GitHub repository for your application and the Heroku app itself. Also, be sure to connect your Heroku app to the new GitHub repo and activate the “Automatic deploys” feature, so that Heroku will build and deploy your application every time someone pushes into your GitHub project:
With Heroku we had a great basis and delivered a registration app that is now running in beta test. Encouraged by this great experience, I used Heroku in many more of my Open Source projects.
Heroku’s Java buildpack defaults to JDK 8…
So why am I writing this article? Well, lately I wanted to use Heroku for an example project that demonstrates the usage of a Spring Boot starter I am maintaining. The cxf-spring-boot-starter helps you get started extremely fast if you have to deal with good old SOAP webservices. Based on standard implementations for XML handling like JAX-B and JAX-WS, the starter uses the JDK-shipped modules
javax.xml.bind. But they were deprecated in Java 9 and removed from the JDK completely with Java 11 . Using new dependecies, there is a way to overcome this problem. But as a consequence it’s better to build the cxf-spring-boot-starter with JDK 11. If you develop a Spring Boot starter, it’s often good practice to also provide some sample projects. This starter example project cxf-boot-simple is shipped and built with the starter – and therefore also needs a current JDK to for a successful build.
Since the default JDK on Heroku is currently version 8 , we are slowly approaching the core problem that prompted me to write this article:
Heroku currently uses OpenJDK 8 to run your application by default.
Configuring a newer JDK on Heroku is not a problem. According to the docs , we simply need to create a
system.properties file inside the root of our application to configure this:
1# Heroku configuration file 2# see https://devcenter.heroku.com/articles/java-support#specifying-a-java-version 3java.runtime.version=11
This should be it, right?! And it is! If you can live with that, you can stick to the standard Heroku Java buildpack and everything will be fine.
Heroku doesn’t support Maven 3.5.x out of the box
But if you need to build your software with a newer Maven version also, you’ll soon find yourself in the hell of being restricted to an old Maven version! Heroku currently only supports Maven versions <=3.3.9< a="">. Using Heroku to build the Spring Boot starter project leads to the following error, which is related to Maven versions older than
3.5.x (full stack trace here ):
1java.lang.IllegalArgumentException: Can not set org.eclipse.aether.spi.log.Logger field org.apache.maven.repository.internal.DefaultVersionResolver.logger to org.eclipse.aether.internal.impl.slf4j.Slf4jLoggerFactory
Sadly we can’t just simply use a newer Maven version in Heroku, although the
system.properties file provides the appropriate configuration key
maven.version. Versions newer than
3.3.9 are currently simply not supported. Also, the other escape route using the Maven Wrapper to use a newer Maven version didn’t work in my case. Heroku simply ignored it and used Maven 3.3.9 again:
1-----> Java app detected 2 3-----> Installing JDK 11... done 4 5-----> Installing Maven 3.3.9... done 6 7-----> Executing: mvn -DskipTests clean dependency:list install 8 9 [INFO] Scanning for projects...
Running Spring Boot apps with Docker on Heroku
Having no current Maven version available on Heroku made me think about a Java User Group Thüringen Talk by Kai Tödter about Spring Boot, REST & Angular , where he showed a demo of a Heroku deployment using Docker instead of a predefined buildpack. I remember discussing the pros and cons of using that additional layer on Heroku, since back then I wasn’t convinced one really needs that.
But then I found myself in a situation where Docker would really help me out. Having Docker support, we could easily use the build tool in any specific version we wanted. And we would free ourselves from having to implement Heroku-specific configuration – if some day Heroku isn’t the best choice, we can easily switch and run our Docker container somewhere else (I heard of somebody using that argument some time ago 😀 ). And the cool thing is: This described way of how to use Heroku is not restricted to Java – you can use it with nearly every language you have at hand!
And for sure there’s also some curiosity involved – so let’s just use Docker to run our Spring Boot apps on Heroku! There are two ways of how to use Docker on Heroku . The first is to use the Heroku Container Registry. It allows you to simply deploy your Docker images to Heroku. The second option is to use Heroku to build our Docker images also for us, which we will use here. According to the docs , we only need a
Dockerfile inside our sample project. Inside the project cxf-boot-simple the Dockerfile first looked like this:
1# Docker multi-stage build 2 3# 1. Building the App with Maven 4FROM maven:3-jdk-11 5 6ADD . /cxfbootsimple 7WORKDIR /cxfbootsimple 8 9# Just echo so we can see, if everything is there :) 10RUN ls -l 11 12# Run Maven build 13RUN mvn clean install 14 15 16# 2. Just using the build artifact and then removing the build-container 17FROM openjdk:11-jdk 18 19MAINTAINER Jonas Hecht 20 21VOLUME /tmp 22 23# Add Spring Boot app.jar to Container 24COPY --from=0 "/cxfbootsimple/target/cxf-boot-simple-*-SNAPSHOT.jar" app.jar 25 26# Fire up our Spring Boot app by default 27CMD [ "sh", "-c", "java $JAVA_OPTS -Djava.security.egd=file:/dev/./urandom -jar /app.jar" ]
It puts a simple Docker multi-stage build into practice. The first build container uses a current Maven version and is therefore based on
maven:3-jdk-11, which currently represents the latest tag of the Maven Docker image . The build artifact that results from a successful Maven build is copied over as app.jar to the container running on Heroku later. As we don’t want to mess with Maven-defined version numbers here, we simply use a
* inside the path to the Spring Boot jar
Configuring Heroku to use Docker
Again the Heroku docs provide an excellent guide on how to configure and run your apps with Docker on Heroku. The key configuration file here is a heroku.yml inside the root of our project. This file provides us with four sections in which we can configure everything needed to build and run our apps with Docker. For example, if you need to have a Heroku addon running, you can configure that in the
setup section. The build section is used to define the Docker build itself. The central part here is to tell Heroku where our
1build: 2 docker: 3 web: /cxf-spring-boot-starter-samples/cxf-boot-simple/Dockerfile
If you ever happen to do something between building and running your app with Docker, the third
release phase comes to the rescue. Here you can provide CDNs with assets or run database schema migrations, for example.
The last section
run defines the processes to run. If you’ve already run your apps without Docker on Heroku, you may know the
Procfile used there to configure the startup behavior of your app. This file is ignored while using a
heroku.yml – instead the run section will be used. But as you see inside the example project’s heroku.yml , there’s also another way (documented in the docs ):
If you do not include a run section in your heroku.yml manifest, the Dockerfile CMD is used instead.
And in our case where we want to use a Java backend inside our Docker container, the startup behavior should be always the same – be it on Heroku or on our local machine. Therefore I prefer to use the
CMD keyword inside our Dockerfile!
The last thing we need to configure for Heroku to use Docker instead is to change the Heroku stack of our used Dynos. The default
web stack is preconfigured in Heroku. To change it, all you have to do is open up your commandline and execute the following:
1heroku stack:set container
Just be sure to have the current heroku-cli installed – using your machine’s package manager at best (like
brew install heroku). If you have multiple apps running on Heroku, you maybe also need to define the concrete application with:
1heroku stack:set container --app your-Heroku-app-name-here
The next push either to Heroku or to your connected GitHub repository should start your application inside a Docker container running on Heroku. Taking a look into the web console, you should also be able to find the newly configured
Preventing Error R14 (Memory quota exceeded)
This should be all you need to do. But wait. Aren’t we running Java apps? Is our app really running on Heroku? Let’s check the logs of our Heroku app using the Heroku CLI again:
1$ heroku logs --tail --app your-Heroku-app-name-here 22019-07-24T02:58:48.253177+00:00 heroku[web.1]: Process running mem=836M(163.4%) 32019-07-24T02:58:48.253243+00:00 heroku[web.1]: Error R14 (Memory quota exceeded) 42019-07-24T02:58:55.236933+00:00 heroku[web.1]: State changed from starting to crashed 52019-07-24T02:58:55.111947+00:00 heroku[web.1]: Stopping process with SIGKILL 62019-07-24T02:58:55.217642+00:00 heroku[web.1]: Process exited with status 137
It seems like our app crashed because of a
Error R14 (Memory quota exceeded) 🙁 Inside the guide about Troubleshooting Memory Issues in Java Applications there’s also a section Configuring Java to run in a container , which describes how to configure the JVM correctly so it knows that it’s running inside a container and should not reserve memory directly from the host machine. And as we use our Dockerfile’s
CMD to configure the JVM startup behavior, we need to extend that one instead of the
Procfile (which is ignored using a
1# Fire up our Spring Boot app by default 2CMD [ "sh", "-c", "java $JAVA_OPTS -XX:+UseContainerSupport -Djava.security.egd=file:/dev/./urandom -jar /app.jar" ]
-XX:+UseContainerSupport option is the default from Java 10 on , I really like to set things explicitly if we rely on them. So let’s leave this option set for JDK 10+ also.
The second thing we need to take care of is the correct JVM configuration for Heroku. The docs say :
You’ll see R14 errors in your application logs when this paging starts to happen.
But the docs also state that there should be defaults with correct
-Xss settings provided out of the box by Heroku:
The default support for most JVM-based languages sets -Xss512k and sets Xmx dynamically based on Dyno type. These defaults enable most applications to avoid R14 errors.
Digging deeper into the subject, I found that these defaults should be made transparent inside the
JAVA_OPTS environment variable. But didn’t we switch the Heroku default stack from
container? Maybe we should take a look at the environment variables inside our Heroku Dyno to gain clarity. To do so, we can execute the command
printenv with the help of Heroku CLI to see all environment variables inside:
1$ heroku run printenv 2Running printenv on ⬢ cxf-boot-simple... up, run.7988 (Free) 3JAVA_URL_VERSION=11.0.4_11 4HEROKU_EXEC_URL=https://exec-manager.heroku.com/a3ea58e6-d7b3-4fa8-8148-5567be41e46f 5PORT=13303 6JAVA_BASE_URL=https://github.com/AdoptOpenJDK/openjdk11-upstream-binaries/releases/download/jdk-11.0.4%2B11/OpenJDK11U-jdk_ 7HOME=/ 8PS1=\[\033[01;34m\]\w\[\033[00m\] \[\033[01;32m\]$ \[\033[00m\] 9JAVA_VERSION=11.0.4 10TERM=xterm-256color 11COLUMNS=160 12PATH=/usr/local/openjdk-11/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin 13JAVA_OPTS= 14LANG=C.UTF-8 15JAVA_HOME=/usr/local/openjdk-11 16PWD=/ 17LINES=32 18DYNO=run.7988
And here we go: The
JAVA_OPTS variable is simply empty! If you switched back to the default
web stack configuration on Heroku, this variable would provide the right bits for us:
1JAVA_OPTS=-Xmx300m -Xss512k -XX:CICompilerCount=2 -Dfile.encoding=UTF-8
So in order to prevent us from running into
Error R14 (Memory quota exceeded) problem, we must be sure to tweak our application’s Dockerfile:
1# Fire up our Spring Boot app by default 2CMD [ "sh", "-c", "java -Xmx300m -Xss512k -XX:CICompilerCount=2 -Dfile.encoding=UTF-8 -XX:+UseContainerSupport -Djava.security.egd=file:/dev/./urandom -jar /app.jar" ]
Now our application should run on Heroku without any memory problems. If the error keeps occurring, there’s also a good medium post on what can be done then.
Preventing Error R10 (Boot timeout)
We’re nearly there! Another error might occur in the application’s startup process, though:
12019-07-24T02:58:55.236933+00:00 heroku[web.1]: State changed from starting to crashed 22019-07-24T02:58:55.111947+00:00 heroku[web.1]: Error R10 (Boot timeout) -> Web process failed to bind to $PORT within 60 seconds of launch 32019-07-24T02:58:55.111947+00:00 heroku[web.1]: Stopping process with SIGKILL 42019-07-24T02:58:55.217642+00:00 heroku[web.1]: Process exited with status 137
Did we set the
$PORT environment variable correctly? Let’s look into a
Procfile which we needed to use in the pre-Docker era on Heroku. It also had to contain the
$PORT variable so that Spring Boot is able to launch its internal Tomcat accordingly:
1web: java -Dserver.port=$PORT -jar cxf-spring-boot-starter-samples/cxf-boot-simple/target/cxf-boot-simple-*-SNAPSHOT.jar
And for sure this configuration is also needed inside our
Dockerfile! Because the docs state :
The web process must listen for HTTP traffic on
$PORT, which is set by Heroku.
EXPOSEin Dockerfile is not respected, but can be used for local testing. Only HTTP requests are supported.
So let’s tweak our example project’s Dockerfile again:
1# Fire up our Spring Boot app by default 2CMD [ "sh", "-c", "java -Dserver.port=$PORT -Xmx300m -Xss512k -XX:CICompilerCount=2 -Dfile.encoding=UTF-8 -XX:+UseContainerSupport -Djava.security.egd=file:/dev/./urandom -jar /app.jar" ]
$PORT environment variable should be used to fire up our Spring Boot app. To verify this, we can execute our Docker container locally. Here we also see another advantage of using Docker with Heroku: we can simply test things locally, which drastically reduces the time we have to invest into our development process.
Now just be sure to append
PORT as environment variable in the
docker run command:
1docker build . --tag cxfbootsimple 2docker run -e "PORT=8095" cxfbootsimple
Our application should be running now. So we can go on and take a look into our container. Simply use
docker ps to get the running container’s ID and then open up a bash inside it:
1docker exec -it containerId bash 2curl localhost:8095/my-foo-api -v
If the curl command outputs some HTML page, it should be good enough to push our updated Dockerfile into our application’s GitHub repository. Having connected Heroku to our repository and configured it to do automatic deploys, the push should result in a new Heroku deployment:
Finally, our Spring Boot app should be running successfully with Docker on Heroku! You might want to check out this article’s example project cxf-boot-simple , where you can find the running Heroku app at
Spring Boot on Heroku with Docker
I’am absolutely relieved I can keep using my beloved Heroku for that use case also! The simplicity of this development process is impressive – as a DevOps fanboy I know what I’m talking about. Using Docker, we combine the simplicity of Heroku with the power of Docker. Just remember: now we use mostly the same infrastructure both locally and in the cloud! And we’re not bound to restrictions of some predefined Heroku buildpacks – not even to a specific programming language! The same process described here could be used for any project – simply change your Dockerfile accordingly and you’re done. Have fun with Docker on Heroku! 🙂
Dein Job bei codecentric?
More articles in this subject area\n
Discover exciting further topics and let the codecentric world inspire you.