Build and run with a simple Dockerfile
Just jump to the Dockerfile code, the comments hopefully make it self explanatory. It is a multi-stage Dockerfile that builds using a Maven Image and then runs using a Java image.
I have been developing with Kotlin for a while now, but these have all been for personal projects. I decided it was time I learned how to use Docker, especially as I am getting more and more into Google Cloud and want to explore using Kubernetes Engine. There were a few things I noticed when learning how to use Docker with Kotlin Web Apps:
- there aren't many examples,
- the examples are all about running apps that have already been built and not build and run (which I wanted to use Docker so the entire development lifecycle was portable).
So, my goal was to create a Multi-Stage Dockerfile that used Maven to build my web app, and then run the web app.
Below is the Dockerfile, which I will explain how / why I have created this way. Note: This is me learning how to use Docker, it may not be the best approach, so let me know if you have some advice to make this better.
This expects a directory structure like the following
./Dockerfile ./pom.xml ./src/main/kotlin/ ./src/main/resources/public/
# Part 1: Build the app using Maven FROM maven:3.6.0-jdk-8-alpine ## download dependencies ADD pom.xml / RUN mvn verify clean ## build after dependencies are down so it wont redownload unless the POM changes ADD . / RUN mvn package # Part 2: use the JAR file used in the first part and copy it across ready to RUN FROM openjdk:8-jdk-alpine WORKDIR /root/ ## COPY packaged JAR file and rename as app.jar ## → this relies on your MAVEN package command building a jar ## that matches *-jar-with-dependencies.jar with a single match COPY --from=0 /target/*-jar-with-dependencies.jar app.jar ENTRYPOINT
The Dockerfile is split into two parts, with Part 1 being responsible for Build, and Part 2 being responsible for Run.
Part 1 — Build
I have split the Build stage into two parts. First, I only add the POM.xml file, and then run the mvn verify clean which will download all the dependencies. This means that if the POM file does not change, the dependencies won’t download each time (this was what was happening to begin with and was getting to be a bit of a pain).
I then add the rest of the files and run the package command. This assumes your POM file has the package stage that builds a JAR file. I am using the maven-assembly-plugin, which by default creates a fat-jar in the format ([artefact-id]-[version]-jar-with-dependencies.jar).
Part 2 — Run
Now the build is out of the way, we start the run phase. Using the OpenJDK docker image as a base, we copy the JAR we created at the end of part 1, and name it app.jar.
We then run
java -jar ./app.jar to run the packaged application.
Where’s the Kotlin Web App Code?
I haven’t included it here, as I ran a very simple SparkJava application that simply returned a test response. My other Medium posts and Github pages are suitable examples to use however.
Using the Dockerfile
Now, to run the whole thing, we just run
docker build -t imagename:version .
Replace imagename with the name of your app, and version is just a unique identifier for you to increment your version numbers.
Then to run, we do
docker run -it --rm --expose=9000 -p 9000:9000 imagename:version
-it Runs docker in interactive mode, rather than in the background. This means that when you Ctrl+C, the container will shutdown so you can modify your code, and re-run when you are ready.
--rm This removes the docker image once finished. I generally find for running an app, it is unnecessary to keep the run image around after it has finished, and this saves space.
--expose=portnumber exposes a port number from within the container. For my Kotlin Web App, I was running on port 9000, so to be able to access port 9000 on the container, we need to expose the port. This could be done inside of the Dockerfile itself as an EXPOSE directive.
-p 9000:9000 This sets the port 9000 on the local machine to map to port 9000 on the container. It is possible from here to map a different local port to the container port. This can be useful when running multiple instances of the same app (for load-balancing).
As I mentioned, I am no Docker expert, so feel free to offer advice on how to improve this, but I certainly achieved my goal of having a maven based build process (without needing to install maven), and without having to download maven dependencies each time, and then able to run the built application from the same Docker process.