How to Use Quarkus Live Coding in Docker

How to Use Quarkus Live Coding in Docker

Here at LogicMonitor, we like to experiment and try new things. In fact, one of our core tenets is “better every day”. This means that we’re always hungry to learn new things, experiment, innovate, and take chances.

One thing we’ve been experimenting with lately (and loving!) is the Quarkus framework. We love how lightweight it is and that it’s built from the ground up with Kubernetes in mind. But most of all, our developers love its uncompromising prioritization of Developer Joy.

What is Developer Joy? The Quarkus team puts it simply: using Quarkus should be enjoyable. For me, it’s about those little things that Quarkus does that make me smile. There are a number of features that make that happen, but the one that has me grinning from ear to ear most often? That’s easy: Live Coding.

Live Coding is the ability to make changes to your source code and have those changes reflected in your deployed application without needing to recompile or redeploy your code. This is fantastic for tightening up your development loop which will help you push out the next iteration of your service that little bit faster.

So, whether you already know all about Live Coding or you’re still trying to wrap your head around it, we’ve got you covered. We’ll walk you through how to make your first Quarkus app, how to run it in development mode locally and use Live Coding, how to get that same app running in Docker, and how to get Live Coding working while your Quarkus app runs in a Docker container. We’ll even point out some gotchas to keep in mind if you plan on implementing this in your own production code.

Let’s go!

TL;DR

For those of you in a rush or who already know the basics of Live Coding, here are the steps for enabling Live Coding in Docker assuming you have an existing Quarkus application:

  1. Add a QUARKUS_LAUNCH_DEVMODE environment variable to your Docker container and set it to true
  2. Add the following configurations to your application(-dev).properties file:
quarkus.package.type=mutable-jar
quarkus.live-reload.password=changeit
quarkus.live-reload.url=http://localhost:8080
  1. Build the jar, build the Docker image, run the Docker container
  2. Connect to the container using
./gradlew quarkusRemoteDev

if you’ve put the above configs in application.properties or

./gradlew quarkusRemoteDev -Dquarkus.profile=dev

if you’ve put them in application-dev.properties.

If you don’t use Gradle, Quarkus also supports Maven. If you use Maven, the corresponding command would be ./mvnw quarkus:remote-dev.

Those are the basic steps. Below, we’ll get into how this all works at a very basic level so that just about anyone will feel comfortable using Live Coding in a local Docker container.

How do I make my first Quarkus app?

Quarkus offers multiple ways to set up the framework for a new service. We like the Quarkus CLI since it’s so convenient. If you want something with a UI, the Quarkus team provides a nice web page for getting started: https://code.quarkus.io/

This tutorial will use the Quarkus CLI because it is very simple and installs Java and your preferred build tool automatically if you don’t already have them installed. If you plan on following along with us, you can find the setup instructions for the Quarkus CLI here: https://quarkus.io/get-started/

Once you have that done, creating your first application is as easy as running the following command:

quarkus create app --gradle docker-live-coding

This command tells the Quarkus CLI to create a new app using Gradle as our build tool in a new directory called “docker-live-coding”. We use Gradle here at LogicMonitor, so that’s why we’re using it here. If you’re more comfortable with either Maven or JBang, the Quarkus CLI does support them. Just keep in mind that you’ll need to alter some of the below commands to use your preferred build tool instead of Gradle.

Try running quarkus -h to see what else the CLI can do!

Taking a look at the code

Thanks to the Quarkus CLI, we now have a barebones microservice! This includes an API endpoint, a unit test for that endpoint, and helpful Dockerfiles.

First, let’s take a look at the endpoint. To do this, open src/main/java/org/acme/GreetingResource.java in your favorite IDE or text editor:

@Path("/hello")
public class GreetingResource {

    @GET
    @Produces(MediaType.TEXT_PLAIN)
    public String hello() {
        return "Hello from RESTEasy Reactive";
    }
}

It’s pretty straightforward. You send the endpoint a GET request and you get a friendly greeting as a response. Neat!

We can find the unit test in src/main/test/java/org/acme/GreetingResourceTest.java:

@QuarkusTest
public class GreetingResourceTest {

    @Test
    public void testHelloEndpoint() {
        given()
          .when().get("/hello")
          .then()
            .statusCode(200)
            .body(is("Hello from RESTEasy Reactive"));
    }

}

This one’s also very simple. When a GET request is sent to the “/hello” endpoint, we expect to receive a 200 status code response with that friendly little greeting in the body.

Finally, you’ll find the Dockerfiles in src/main/docker. The CLI will create four different files by default: Dockerfile.jvm, Dockerfile.legacy-jar, Dockerfile.native, and Dockerfile.native-micro.

Dockerfile.jvm is for use with Quarkus’s default JAR packaging: fast-jar. If you aren’t super familiar with the different JAR options, this is the best place to start. Dockerfile.legacy-jar is for those of us who are still using the old default Quarkus JAR packaging (pre-Quarkus 1.12). This is really only useful for people working on existing projects. Dockerfile.native and Dockerfile.native-micro are for building native executables. This is just another slick feature of Quarkus that allows for super small images, small memory footprints, and blazing-fast startup.

In order to keep things simple, we’ll just focus on Dockerfile.jvm for this tutorial.

How do I run my Quarkus app?

We can quickly and easily run our new application locally by simply executing the following command in our new project directory:

quarkus dev

You’ll eventually see the following output letting you know that the app is running:

__  ____  __  _____   ___  __ ____  ______
--/ __ \/ / / / _ | / _ \/ //_/ / / / __/
-/ /_/ / /_/ / __ |/ , _/ ,< / /_/ /\ \  
--\___\_\____/_/ |_/_/|_/_/|_|\____/___/  
2022-10-03 18:41:18,701 INFO  [io.quarkus] (Quarkus Main Thread) docker-live-coding-2 1.0.0-SNAPSHOT on JVM (powered by Quarkus 2.12.2.Final) started in 1.482s. Listening on: http://localhost:8080

2022-10-03 18:41:18,704 INFO  [io.quarkus] (Quarkus Main Thread) Profile dev activated. Live Coding activated.
2022-10-03 18:41:18,704 INFO  [io.quarkus] (Quarkus Main Thread) Installed features: [cdi, resteasy-reactive, smallrye-context-propagation, vertx]

And just like that, your app is running at localhost:8080.

In fact, this command ran our Quarkus application locally in dev mode. Development mode is an awesome tool provided by Quarkus that gives us access to an interactive terminal and Live Coding.

How do you Live Code in Quarkus?

Well, we’ve been talking for a long time now, when are we finally going to see this Live Coding thing in action? You don’t have to tell me twice, let’s do it!

First, let’s see how our application behaves. Open up a new terminal tab/window, and enter the following command:

curl -w "\n" http://localhost:8080/hello

When you do, you should see the following message (just like in the unit test!):

Hello from RESTEasy Reactive

Now, that’s a nice enough message but, if you think about it, it’s kind of rude to say hello without asking how the other person is doing. Let’s fix that!

If you closed it, reopen the GreetingResource.java file, and let’s make the following change to the hello() method:

  public String hello() {
        return "Hello from RESTEasy Reactive. How are you?";
    }

Once you have that saved, go ahead and run that curl command again. What do you see?

Hello from RESTEasy Reactive. How are you?

Just like that, our application updated to use our code change while it was running. How cool is that? Smiling yet? 🙂

So how did that happen? When your Quarkus app runs in dev mode, whenever it receives an incoming request, it will hold and check to see if the app’s source code has changed. If it has, Quarkus will quickly compile the changed code and deploy it right there before executing the request. Like I said, very cool.

How do I run my app in Docker?

First, you’ll need to build the JAR file of your application. Docker will then use it when it starts up your container. There are a couple of ways to build your application’s JAR file depending on the build tool you’re using and whether or not you’re using the Quarkus CLI.

Using the CLI:

quarkus build

For Gradle, you’ll navigate to your repository’s root and run:

./gradlew build

If you’re using Maven or JBang, you’ll need to run a different but similar command (check the comments in your Dockerfile.jvm file!).

If you were following along above and made a change to the GreetingResource endpoint, make sure you either revert your change or update the unit test before running the build command. Otherwise, the build will fail since the unit test won’t pass.

Once you have your JAR file built, we can build our Docker image. As I mentioned earlier, we’ll use Dockerfile.jvm for this. To build our image, run the following command in your terminal:

docker build -f src/main/docker/Dockerfile.jvm -t quarkus/docker-live-coding .

Don’t forget the dot (.) at the end of the command! This provides the build context for Docker.

Here, the -f src/main/docker/Dockerfile.jvm argument is pointing Docker to the Dockerfile we want to use and the -t quarkus/docker-live-coding argument is giving the image a tag that we can use to reference it later.

Once your image is created, it’s time to get things running!

docker run -i --rm -p 8080:8080 quarkus/docker-live-coding

Now, run the same curl command as we did previously and you’ll see the original (not as polite as it could be) greeting.

But what happens if we try changing things like we did last time? In a word: nothing! Live Coding doesn’t work right out of the box in Docker for a couple of reasons. Some eagle-eyed readers may have seen one of those problems in the startup logs:

022-10-03 22:13:33,453 INFO  [io.quarkus] (main) Profile prod activated.

We certainly can’t use a dev mode feature while we’re running in prod mode! Let’s see what else needs to change.

How do we get Live Coding working in Docker?

We know that we need to get our app running in dev mode, so let’s start there.

To do this, we just need to add a new environment variable, QUARKUS_LAUNCH_DEVMODE, and set it to true. There are a number of ways to do this, but the easiest way is by adding the following line to your Dockerfile:

ENV QUARKUS_LAUNCH_DEVMODE=true

Personally, I like to keep Dockerfile.jvm unchanged as a reference, so I copied it’s contents into a new file, Dockerfile.dev, and added the above line at the bottom of the file.

FROM registry.access.redhat.com/ubi8/openjdk-11:1.11

ENV LANG='en_US.UTF-8' LANGUAGE='en_US:en'

COPY --chown=185 build/quarkus-app/lib/ /deployments/lib/
COPY --chown=185 build/quarkus-app/*.jar /deployments/
COPY --chown=185 build/quarkus-app/app/ /deployments/app/
COPY --chown=185 build/quarkus-app/quarkus/ /deployments/quarkus/

EXPOSE 8080
USER 185
ENV AB_JOLOKIA_OFF=""
ENV JAVA_OPTS="-Dquarkus.http.host=0.0.0.0 -Djava.util.logging.manager=org.jboss.logmanager.LogManager"
ENV JAVA_APP_JAR="/deployments/quarkus-run.jar"

ENV QUARKUS_LAUNCH_DEVMODE=true

If you’re reading this in the after publication, there’s a chance that the Quarkus CLI has altered the automatically generated Dockerfiles in some way. If you see a difference between yours and what’s above, don’t worry. So long as you add the environment variable to your Dockerfile, you should be good to go.

Once that’s done, we need to tell Quarkus to alter the build file to be mutable (so we can change the code on the fly) and we need to tell it where our application will be running and how to reach it. To do this, you’ll want to open up the application.properties file in the src/main/resources directory. This is another file that the Quarkus CLI automatically generated and it is responsible for determining your app’s configuration. You can learn more about it here.

Once opened, add the following lines:

quarkus.package.type=mutable-jar
quarkus.live-reload.password=changeit
quarkus.live-reload.url=http://localhost:8080

As the value hints, you should give your own password for the quarkus.live-reload.password config. Admittedly, if you’re only running dev mode/Live Coding in Docker locally, there isn’t much risk of having an unsecured password. However, it doesn’t hurt to follow best practices and update it to something more secure. In fact, there may be a way to run Live Coding in the cloud *hint hint* – you wouldn’t want to use a default password in that case!

Note: you don’t technically need the quarkus.live-reload.url config in application.properties as you can specify the value as a command line argument (we’ll show an example below). It is convenient though, so we recommend this approach.

Our process for building and running the app is pretty much the same as before:

quarkus build # our code hasn't changed, but we added configs to application.properties
docker build -f src/main/docker/Dockerfile.dev -t quarkus/docker-live-coding . # use Dockerfile.jvm if you didn't make a Dockerfile.dev
docker run -i --rm -p 8080:8080 quarkus/docker-live-coding

Now your app is running in a container AND in dev mode but, if you try it, Live Coding still isn’t working.

Thankfully, it’s a quick solution – we just need to tell our local machine to connect with the “remote” machine (in this example, it’s really our Docker container running on our local machine, but this applies to truly remote machines too!) so that our Quarkus application can watch the source code. To do that, open up another terminal window and run:

./gradlew quarkusRemoteDev

As mentioned above if you didn’t specify the quarkus.live-reload.url config in your application.properties you’ll need to run the following command so Quarkus knows where your app is running:

./gradlew quarkusRemoteDev -Dquarkus.live-reload.url=http://localhost:8080

If it connected successfully, you should see something like the following logs:

> Task :quarkusRemoteDev
Listening for transport dt_socket at address: 5005
2022-10-03 15:39:37,025 INFO  [io.qua.dep.QuarkusAugmentor] (main) Quarkus augmentation completed in 934ms
> :qu2022-10-03 15:39:37,531 INFO  [io.qua.ver.htt.dep.dev.HttpRemoteDevClient] (Remote dev client thread) Sending lib/deployment/appmodel.dat
2022-10-03 15:39:37,538 INFO  [io.qua.ver.htt.dep.dev.HttpRemoteDevClient] (Remote dev client thread) Sending app/quarkus-application.jar
2022-10-03 15:39:37,547 INFO  [io.qua.ver.htt.dep.dev.HttpRemoteDevClient] (Remote dev client thread) Sending quarkus-run.jar
2022-10-03 15:39:37,549 INFO  [io.qua.ver.htt.dep.dev.HttpRemoteDevClient] (Remote dev client thread) Connected to remote server

<=========----> 75% EXECUTING [3m 55s]
> :quarkusRemoteDev

Once that’s done, you’re all set. Have fun Live Coding in Docker!

But can we do any better?

How do we refine our solution?

Now that we have everything working, we should think about the implications of the changes we’ve made. Most notably, we’ve added new configs!

By adding these configs without any qualifying profiles (learn more about configuration profiles here), we’ve guaranteed that these values will be passed to our application no matter what build we run! We certainly don’t want a mutable JAR to be running in production and we definitely don’t want live reloading to be available either! That’s just asking for trouble.

Thankfully, Quarkus has a simple solution: configuration profiles! These profiles help us dictate which configs should be present in the application based on which environment we plan to run the app in.

Assuming you only want to run Live Coding in a development environment, we can leverage the dev profile. The easiest way to do this is to create a new file named application-dev.properties in the same location as the original application.properties file. Just move the configs we added in the previous step from the original properties file to our new dev version.

Now, these configs will only be passed to the application when we build it in dev mode. If we use any other profile, these configs will simply be ignored.

Go ahead and try rebuilding the application and testing out Live Coding. The process will be nearly the same except, when you build the JAR, you’ll have to tell Gradle (or your favorite build tool) to build it in dev mode.

quarkus build -Dquarkus.profile=dev

You’ll need to add the same argument when you connect to the remoted dev session so Quarkus knows the Live Coding URL and password:

./gradlew quarkusRemoteDev -Dquarkus.profile=dev

For extra credit, try doing this process without the new argument and see if it will let you connect for Live Coding (SPOILER: it won’t).

Further exploration

Now that we know how to run Live Coding in our local Docker setup, the question becomes: can we also use Live Coding in containers running in the cloud?

The answer is a resounding YES! Quarkus supports Live Coding in Kubernetes, Minikube, and Openshift. We’ll leave the full implementation for each as an exercise for the reader – everything we’ve covered above should give you most of the information you’ll need to get things working.

Conclusion

In this blog post, we’ve learned how to create a Quarkus app from scratch, run it locally in dev mode, use Live Coding locally, deploy the app to a local Docker container, use Live Coding while the app runs in a local Docker container, and how to refine this process for use in a professional development environment.We hope you’ve learned a thing or two while reading this and, most of all, we hope Quarkus brings you as much Developer Joy as it has us! If you’d like to learn even more, check out all the awesome guides the Quarkus team has put together: https://quarkus.io/guides/