Containerization with Spring Boot and Docker

In this tutorial, you’re going to build a Spring Boot web application containerized with Docker that uses feature flags and Split to allow runtime updates to application behavior. The app you’re going to develop manages a donut factory. The donut factory will have a donut data model that will track donut inventories and various important donut attributes. Your customers will be able to buy donuts via a web controller endpoint. You’ll also be able to Create, Read, Update, and Delete (CRUD) donuts via an auto-generated Hypermedia as the Engine of Application State (HATEOAS) REST API.

Spring Boot is the application framework you’ll use to provide the web application features. It’s a Java framework that is enterprise-ready and backed by a codebase proven by decades of use in projects worldwide. You’ll use another Spring framework, Spring Data, to map your Donut Java class to a persistence store (the database). This will allow you to save and load Donut class instances simply by annotating the data model Java class and by creating a straightforward data model repository.

Next, you’re going to use Split to add feature flags to the code. Imagine your factory started rolling out a new line of chocolate donuts, and you decided on a limited release, so you limit the sales of chocolate donuts to a few states initially. But after only a few weeks, the donuts are a hit (because who doesn’t like chocolate?), and your factory figures out a way to ramp up production. Fortunately for you, you considered this possibility. You implemented feature flags controlling the availability of chocolate donuts, allowing you to update your code in real-time without rebuilding and redeploying the application.

You can think of a feature flag as a decision point in your code, a “flag” as one might have used in old-school coding. This is a place where features can be turned on and off based on the flag state. The flag can be a boolean, in its most basic form, but can also be multivariate with arbitrary string values. Split provides a service that makes it easy to implement feature flags. You’ll use their Java SDK to integrate their client into the Spring Boot application. This client talks to the Split servers to update the flag state, and you will use their dashboard to configure the split (or feature flag) and the treatment (customized state of the flag based on input from the application).

Once all that is done, you’re going to use Docker to containerize the application, ensuring that the application has a consistent operating system environment. This is super easy with Spring Boot, and there’s even a Gradle and Maven task for building Docker images. As the last step, you’re going to see how to use Docker Compose to migrate from the in-memory database to a more production-ready MySQL database configured and deployed with only a few lines of code in a yaml file.

Requirements for Spring Boot and Docker

You need to install a couple of requirements before you get started.

Java: I used Java 12 for this tutorial. You can download and install Java by going to the AdaptOpenJdk website. Or you can use a version manager like SDKMAN or even Homebrew.

Split: Sign up for a free Split account if you don’t already have one. This is how you’ll implement the feature flags.

HTTPie: This is a powerful command-line HTTP request utility you’ll use to test the Spring Boot server. Install it according to the docs on their site.

Docker Engine: You need to have Docker Engine installed on your computer. The Docker website has instructions for installing it. If you install Docker Desktop (OSX and Windows), you will need to have it running for the Docker portion of the tutorial.

Docker Compose: This allows you to manage multiple Docker images, allowing you to deploy entire application systems. Install it according to the docs on the Docker website. It is a separate install from Docker Engine.

You won’t need to install Gradle, the dependency and build manager you’ll use for this project, because the bootstrapped project will include a Gradle wrapper (version-locked instance of the Gradle executable included with the project itself. However, if you want to install Gradle or learn more about it, check out their website.

Use Spring Initizlizr To Bootstrap Your Project

Spring has been kind enough to create the Spring Initializer. This app allows you to quickly and easily configure Spring Boot projects. You can even browse the projects before you download them. It also allows you to share the configured project via a link.

Follow this link for the preconfigured starter for this tutorial.

Click the Generate button at the bottom of the page and download the project to your computer. Unzip it. This folder, demo, will be the root directory of the project.

The project configures some basic properties:

  • Gradle for the build tool
  • Java as the base language
  • Java 11 as the Java version
  • Spring Boot version 2.4.5 It also includes the following dependencies:
  • Lombok
  • Spring Web
  • Rest Repositories
  • Spring Data JPA
  • H2 Database

Project Lombok is an annotation-based helper that saves you (or me, really) from creating many getters, setters, and constructors. Check out their website to see all the features.
Spring Web provides the basic Spring MVC features that allow you to create web services.
Rest Repositories allows Spring Data to create REST APIs from JPA data models and repositories automatically.
Spring Data JPA includes the ability to interact with databases and map Java objects to data model instances and database structures.
H2 Database adds an in-memory database implementation that does not persist between sessions and is excellent for testing and projects like this.

For this example, we did not change any of the project metadata (group, artifact, name, description, etc…), but you can also configure that using the Spring Initializr.

You can go ahead and test your sample project. It won’t do much, but it should run without error. Open a bash shell and run the following command in the project root directory.

./gradlew bootRun

Notice you’re using a local script, ./gradlew and not gradle itself. That’s the Gradle wrapper. It has a couple of nice advantages. First, it locks the Gradle version so that the build script is guaranteed to work with the Gradle version running it. Second, it means you don’t have to install Gradle locally.

Time To Make The Donuts!

Now you need to make a donut factory. This will demonstrate a number of key Spring Boot features:

  1. creating a persisted data model from a Java class (donuts, in our case),
  2. using CrudRepository to enable persistence of the data model instances,
  3. activating custom query methods for the data model,
  4. creating a REST controller that manipulates data model instances,
  5. how to persist enum data types, and
  6. using constructor dependency injection to inject the repository in our controllers.

Create a new Java file for the donut data model.

src/main/java/com/example/demo/Donut.java

@Entity @Data @NoArgsConstructor public class Donut { @Id @GeneratedValue(strategy= GenerationType.AUTO) private Long id; @Enumerated(EnumType.STRING) private DonutToppings toppings; public String name; public Double costDollars; public Integer numberAvailable; Donut(String name, DonutToppings toppings, Double costDollars, Integer numberAvailable) { this.name = name; this.toppings = toppings; this.costDollars = costDollars; this.numberAvailable = numberAvailable; } }
Code language: Java (java)

Also, create a DonutToppings enum class that is used in the donut data model.

public enum DonutToppings { CHOCOLATE("Chocolate Icing"), SPRINKLES("Sprinkles"), MAPLE("Maple Icing"), GLAZED("Sugar Glaze"), BACON("Bacon"), POWDERED_SUGAR("Powdered Sugar"), NONE("None"); private final String value; DonutToppings(String value) { this.value = value; } }
Code language: C# (cs)

The donut data model has four attributes: id, name, costDollars, toppings, and numberAvailable. The first three are pretty self-explanatory. The property toppings is a little more complex because it’s an enumerated type, so you have to define a separate class for the enum. The last property, numberAvailable, is a simple inventory tracking that tells you how many of those types of donuts you have available.

The two annotations @Data @NoArgsConstructor are from Project Lombok and save you from defining getters and setters for all the properties as well as an empty, no-args constructor. The @Entity annotation is the Spring Boot Java Persistence API (JPA) annotation that tells Spring Boot that this is a data model class.

The next file is the repository. The data model alone does very little. It is the repository that can save and retrieve the data model instances from the database.

java/com/example/demo/DonutRepository.java

public interface DonutRepository extends CrudRepository<Donut, Long> { List<Donut> findByNameIgnoreCase(String name); }
Code language: Java (java)

The repository code is deceptively simple. There’s a ton of “auto-magicking” going on here. The main thing to know is that this is an instance of a CrudRepository which not only can persist and retrieve data model entities but also automatically enables (in conjunction with the Rest Repositories dependency) a REST API for those entities. If you don’t want this behavior, you can simply omit the Rest Repositories dependence.

The one method in the repository, findByNameIgnoreCase(), uses Spring’s natural-language query creation method. It maps method names to database queries based on a descriptive, natural-language system.

Note: For a complete look at this feature, check out the Spring Data JPA docs.

If you look at the interface for CrudRepository, you’ll see that it implements all of the basic methods needed for CREATE, READ, UPDATE, and DELETE-ing data models. These methods can be overridden to modify their behavior, disable them, or add role-based security.

public interface CrudRepository<T, ID> extends Repository<T, ID> { <S extends T> S save(S entity); <S extends T> Iterable<S> saveAll(Iterable<S> entities); Optional<T> findById(ID id); boolean existsById(ID id); Iterable<T> findAll(); Iterable<T> findAllById(Iterable<ID> ids); long count(); void deleteById(ID id); void delete(T entity); void deleteAll(Iterable<? extends T> entities); void deleteAll(); }
Code language: Java (java)

Notice, however, that the descriptive find method added in the DonutRepository subclass adds a simple way to perform a query based on a model property that didn’t exist in the parent class. This is part of the magic of JPA (ok, not magic, but reflection).

At this point, you have a Spring Boot application capable of creating, reading, updating, and deleting donuts. Run the app using the following command.

./gradlew bootRun
Code language: Bash (bash)

The console output should end with this:

... 2021-01-17 13:20:45.752 INFO 12125 --- [ main] o.s.b.w.embedded.tomcat.TomcatWebServer : Tomcat started on port(s): 8080 (http) with context path '' 2021-01-17 13:20:45.758 INFO 12125 --- [ main] com.example.demo.DemoApplication : Started DemoApplication in 2.517 seconds (JVM running for 2.755) <==========---> 80% EXECUTING [1m 28s]
Code language: Bash (bash)

Use HTTPie to make a GET request.

http :8080
Code language: Bash (bash)
HTTP/1.1 200 ... { "_links": { "donuts": { "href": "http://localhost:8080/donuts" }, "profile": { "href": "http://localhost:8080/profile" } } }
Code language: Bash (bash)

The /donuts endpoint listed is the controller generated by Spring Data for your data model. Make a request at that endpoint.

http :8080/donuts
Code language: Bash (bash)
HTTP/1.1 200 ... { "_embedded": { "donuts": [] }, "_links": { "profile": { "href": "http://localhost:8080/profile/donuts" }, "search": { "href": "http://localhost:8080/donuts/search" }, "self": { "href": "http://localhost:8080/donuts" } } }
Code language: Bash (bash)

The generated rest interface is based on Hypermedia as the Engine of Application State (HATEOAS – rhymes with adios). The idea is to use hypermedia to describe a REST interface. I won’t dig into this here. Check out the Spring Docs on Spring HATEOAS if you want to get a more detailed explanation.

The main thing to notice above is the empty array "donuts": [].

A donut factory without any donuts is pretty sad. Add a donut. Or 124.

http :8080/donuts name=Glazed costDollars=0.5 toppings=GLAZED numberAvailable=124
Code language: Bash (bash)
HTTP/1.1 201 ... { "_links": { "donut": { "href": "http://localhost:8080/donuts/1" }, "self": { "href": "http://localhost:8080/donuts/1" } }, "costDollars": 0.5, "name": "Glazed", "numberAvailable": 124, "toppings": "GLAZED" }
Code language: Bash (bash)

GET the root endpoint again, and you’ll see the donut you added.

http :8080/donuts
Code language: Bash (bash)
... { "_embedded": { "donuts": [ { "_links": { "donut": { "href": "http://localhost:8080/donuts/1" }, "self": { "href": "http://localhost:8080/donuts/1" } }, "costDollars": 0.5, "name": "Glazed", "numberAvailable": 124, "toppings": "GLAZED" } ] }, ... }
Code language: Bash (bash)

You could also DELETE the donut. Notice the donut ID hidden in the link.

http DELETE :8080/donuts/1
Code language: Bash (bash)
HTTP/1.1 204
Code language: Bash (bash)

That’s all using the auto-generated CrudRepository methods. In the next section, you’re going to add a custom controller and a buyDonut() method.

Add A Custom Controller To The Spring Boot App

Add a new Java file, DonutController.

java/com/example/demo/DonutController.java

@Controller @RequestMapping("/api") public class DonutController { DonutRepository donutRepository; DonutController(DonutRepository donutRepository) { this.donutRepository = donutRepository; } @RequestMapping("/buy-donut") @ResponseBody String buyDonut(@RequestParam String donutName) { List<Donut> found = this.donutRepository.findByNameIgnoreCase(donutName); if (found.size() <= 0) { return "Wah. Wah. No donuts for you."; } else { Donut donut = found.get(0); if (donut.numberAvailable <= 0) { return "Sorry. All out of those."; } donut.numberAvailable = donut.numberAvailable - 1; this.donutRepository.save(donut); return "Enjoy your " + donutName + " donut. It costs $" + donut.costDollars + ". There are " + donut.numberAvailable + " of these remaining."; } } }
Code language: Arduino (arduino)

This controller encompasses some business logic that uses the donut repository. It allows clients to request a donut by name. It checks to see if the donut exists and if there are donuts of that type available. If there are not, a sad message is returned. If there are donuts of that type available, it returns an exciting success message (but, sadly, not an actual donut because we’re still not living in Star Trek).

This controller method is available at /api/buy-donut, as determined by the two @RequestMapping methods.

Also, update your DemoApplication class. The code below bootstraps a few donut types into the database when the program loads.

java/com/example/demo/DemoApplication.java

@SpringBootApplication public class DemoApplication { public static void main(String[] args) { SpringApplication.run(DemoApplication.class, args); } DonutRepository donutRepository; DemoApplication(DonutRepository donutRepository) { this.donutRepository = donutRepository; } @PostConstruct public void initApplication() { if (donutRepository.count() > 1) return; donutRepository.save(new Donut("Chocolate", DonutToppings.NONE, 1.50, 10)); donutRepository.save(new Donut("Maple", DonutToppings.MAPLE, 1.0, 5)); donutRepository.save(new Donut("Glazed", DonutToppings.GLAZED, 0.75, 20)); } }
Code language: Java (java)

Press control-c to stop your application, if you haven’t already, and restart it.

./gradlew bootRun
Code language: Bash (bash)

Hit the root endpoint again, and you’ll see the bootstrapped data.

http :8080/donuts
Code language: Bash (bash)
HTTP/1.1 200 ... { "_embedded": { "donuts": [ { ... "costDollars": 1.5, "name": "Chocolate", "numberAvailable": 10, "toppings": "NONE" }, { ... "costDollars": 1.0, "name": "Maple", "numberAvailable": 5, "toppings": "MAPLE" }, { ... "costDollars": 0.75, "name": "Glazed", "numberAvailable": 20, "toppings": "GLAZED" } ] }, ... }
Code language: Bash (bash)

Now, buy a donut!

http ":8080/api/buy-donut?donutName=Maple"
Code language: Bash (bash)
HTTP/1.1 200 ... Enjoy your Maple donut. It costs $1.0. There are 4 of these remaining.
Code language: Bash (bash)

Great. You got your donut factory. Now, since you don’t want your DevOps team upgrading your operating system or Linux distribution behind your back, crashing your factory, and depriving the world of donuts, you’re going to containerize your donut factory in Docker. This will provide a consistent and predictable operating environment, ensuring that the donuts flow and crisis are averted.

Dockerize the Spring Boot App

Dockerizing the app is very, very simple. Create a Dockerfile in your project root. This Docker file simply specifies using a source image with OpenJDK 12 and tells Docker to run the JAR file built from the project.

Dockerfile

FROM openjdk:12 ARG JAR_FILE=build/libs/*.jar COPY ${JAR_FILE} app.jar ENTRYPOINT ["java","-jar","/app.jar"]
Code language: Bash (bash)

If you installed Docker Desktop, make sure it’s running. You need to have Docker Engine installed and running locally (you should be able to run docker -v and get a Docker version).

Stop your app (control-c).

Docker uses the .jar file, so you need to tell Gradle to build it.

./gradlew build
Code language: Bash (bash)

Build the Docker image. Here I’m applying the tag splitdemo/spring-boot-docker to the image.

docker build --build-arg JAR_FILE="build/libs/*.jar" -t splitdemo/spring-boot-docker .
Code language: Bash (bash)

Alternative, just so you know, you could both build the project and the Docker image simultaneously using Gradle (you don’t need to run the following command, it’s just an alternative to the previous two).

./gradlew bootBuildImage --imageName=splitdemo/spring-boot-docker
Code language: Bash (bash)
Sending build context to Docker daemon 66.18MB Step 1/4 : FROM openjdk:12 ---> e1e07dfba89c Step 2/4 : ARG JAR_FILE=target/*.jar ---> Using cache ---> 085ee17b7ea0 Step 3/4 : COPY ${JAR_FILE} app.jar ---> 28447f07ad6f Step 4/4 : ENTRYPOINT ["java","-jar","/app.jar"] ---> Running in 3ce1950c4acd Removing intermediate container 3ce1950c4acd ---> f0551a6ea168 Successfully built f0551a6ea168 Successfully tagged splitdemo/spring-boot-docker:latest
Code language: Bash (bash)

Run the Docker image.

docker run -p 8080:8080 splitdemo/spring-boot-docker
Code language: Bash (bash)

In the command above, you told Docker to expose the 8080 port, mapped to the 8080 port inside the container, and you specified the tagged image you just built.

When it runs, you should see the same console output you saw previously.

... 2021-01-17 21:57:52.785 INFO 1 --- [ main] o.s.b.w.embedded.tomcat.TomcatWebServer : Tomcat started on port(s): 8080 (http) with context path '' 2021-01-17 21:57:52.794 INFO 1 --- [ main] com.example.demo.DemoApplication : Started DemoApplication in 4.244 seconds (JVM running for 4.7)
Code language: Bash (bash)

You should also be able to list and buy donuts.

http :8080/donuts
Code language: Bash (bash)
http ":8080/api/buy-donut?donutName=Maple"
Code language: Bash (bash)

Great. Your donut factory is now safe from overzealous sysops.

But, wait! You just realized your chocolate donuts are not ready for production yet. They weren’t supposed to be released yet. And when they are released, they’re supposed to be available only to a select number of users, at least at first, because of limited production. Ugh. Now you’ve got to fix the code, rebuild the image, re-test everything, and re-deploy.

This is where feature flags could have saved you a headache.

Implement Feature Flags In Your Spring Boot + Docker Project

As mentioned previously, feature flags are decision points in your code that can be changed remotely. Split provides the implementation of this service that you’re going to use here. If you have not already signed up for a free Split account, please do so now.

Add the Split Java SDK to your application by adding the dependency to the build.gradle file.

dependencies { ... compile 'io.split.client:java-client:4.1.3' }
Code language: JSON / JSON with Comments (json)

Next, create the SplitConfig class. This class initializes the SplicClient bean that you will use in your controller class to retrieve the feature flag state.

java/com/example/demo/SplitConfig.java

@Configuration public class SplitConfig { @Value("#{ @environment['split.api-key'] }") private String splitApiKey; @Bean public SplitClient splitClient() throws Exception { SplitClientConfig config = SplitClientConfig.builder() .setBlockUntilReadyTimeout(1000) .enableDebug() .build(); SplitFactory splitFactory = SplitFactoryBuilder.build(splitApiKey, config); SplitClient client = splitFactory.client(); client.blockUntilReady(); return client; } }
Code language: Java (java)

You will need to implement the split in code using the client and add your Split API keys to the project configuration, but before you do that, you need to create the treatment on Split.

Create The Split Treatment

You should have already signed up for a Split account. Open your Split dashboard.

Remember that a split is the same thing as a feature flag: a decision point in your code that can be toggled at runtime, live, as opposed to being baked into the code in a way that requires rebuilding and redeploying. Every split can have multiple treatments. Treatments are the different states the feature flag can take depending on the user attributes according to the treatment rules. In this case, there are two treatments: on and off, a simple boolean, but you can have far more complex splits with multivariate treatments if you like.

Create a new split by clicking the blue Create Split button.

Name the split DonutFactory. Click Create.

Add a targeting rule to the split. Click the blue Add Rules button.

The default rule is a boolean rule with two values: on and off. The default value is off. Now you need to define when the rule changes from off to on. In this example, you’re going to use attributes to determine the flag state. Attributes are arbitrary metadata passed along with the user identifier that can be used to control splits (aka. feature flags).

Scroll down to Set targeting rules. Add a rule to match the image below. The user attribute should be location. The drop-down should be is in list. Add three values: Texas, Oregon, Alaska. Change the serve dropdown from Off to On. These are the states that will get the chocolate donuts.

Click the blue Save Changes button at the top. And click Confirm on the next page. Once you click confirm, the changes are live. That’s the whole point of the feature flags. However, if you make a mistake, you can affect an awful lot of users very quickly. This is why Split has wisely implemented change tracking and the confirmation screen. As they say: “With great responsibility comes the ability to really screw things up.”

You need to retrieve your API keys so that you can put them in the Java app. Click the rounded square icon in the top-left that says DE, and in the popup menu, click Admin Settings. Click API Keys in the side panel. You want to copy the staging-default SDK keys (not the Javascript SDK keys).

Back in the Java app, add this to your application.properties.

src/main/resources/application.properties

split.api-key=<your API key>
Code language: Bash (bash)

Add the Feature Flag to Spring Boot

Now you need to update the DonutController to use the SplitClient. Just replace the contents of your file with the following contents.

java/com/example/demo/DonutController.java

@Controller @RequestMapping("/api") public class DonutController { Logger logger = LoggerFactory.getLogger(DonutController.class); DonutRepository donutRepository; SplitClient splitClient; DonutController(DonutRepository donutRepository, SplitClient splitClient) { this.donutRepository = donutRepository; this.splitClient = splitClient; } @RequestMapping("/buy-donut") @ResponseBody String buyDonut(@RequestParam String donutName, @RequestParam String username, @RequestParam String userLocation) { // Create the attributes map Map<String, Object> attributes = Map.of("location", userLocation); // Get the treatment from the Split Client String treatment = this.splitClient.getTreatment(username,"DonutFactory", attributes); // Log the treatment, just for fun logger.info("Treatment="+treatment.toString()); // Make sure only people with the treatment "on" get the chocolate donuts if (treatment.equals("off") && donutName.toLowerCase().equals("chocolate")) { return "Wah. Wah. No chocolate donuts for you."; } List<Donut> found = this.donutRepository.findByNameIgnoreCase(donutName); if (found.size() <= 0) { return "Wah. Wah. No donuts for you."; } else { Donut donut = found.get(0); if (donut.numberAvailable <= 0) { return "Sorry. All out of those."; } donut.numberAvailable = donut.numberAvailable - 1; this.donutRepository.save(donut); return "Enjoy your " + donutName + " donut. It costs $" + donut.costDollars + ". There are " + donut.numberAvailable + " of these remaining."; } } }
Code language: Arduino (arduino)

In the new file, you use constructor dependency injection to pass the SplitClient to the DonutController, just like you did with the repository. There are also two new parameters to the method: username and userLocation. Now, just passing this information as a query parameter on an unsecured API is not exactly secure. In a real scenario, Russian and Chinese hackers would totally be ordering chocolate donuts in microseconds. However, for our hypothetical tutorial, it’s enough to assume the incoming data is accurate.

The actual treatment is requested and retrieved in this section of the code:

// Create the attributes map Map<String, Object> attributes = Map.of("location", userLocation); // Get the treatment from the Split Client String treatment = this.splitClient.getTreatment(username,"DonutFactory", attributes);
Code language: JavaScript (javascript)

You pass the splitClient.getTreatment() method three parameters: 1) the username, 2) the split name (DonutFactory), and 3) the map of attributes. The split returns the treatment (the state of the feature flag or split) based on these values. This happens very quickly because split values are cached locally and updated asynchronously instead of being requested when the client method is called. This means that the splitClient.getTreatment() method is very fast and is updated behind the scenes but doesn’t risk hanging waiting on a network response.

Rebuild the JAR file and the Docker image using either the Gradle task bootBuildImage or by building the JAR and using Docker to build the image.

./gradlew build
Code language: Bash (bash)
docker build --build-arg JAR_FILE="build/libs/*.jar" -t splitdemo/spring-boot-docker .
Code language: Bash (bash)

Run the Docker image.

docker run -p 8080:8080 splitdemo/spring-boot-docker
Code language: Bash (bash)

Your donut factory is live.

Try the following request. Notice that you’re using HTTPie to simplify passing URL params by using == instead of =, which is used to denote HTTP POST form params. See HTTPie’s docs on query string parameters.

Texas gets chocolate donutes.

http :8080/api/buy-donut donutName==Chocolate username==andrew userLocation==Texas
Code language: Bash (bash)
Enjoy your Chocolate donut. It costs $1.5. There are 9 of these remaining.
Code language: Bash (bash)

California doesn’t.

http :8080/api/buy-donut donutName==Chocolate username==andrew userLocation==California
Code language: Bash (bash)
Wah. Wah. No chocolate donuts for you.
Code language: Shell Session (shell)

Now imagine your donut supply line ramps up, and you have more chocolate donuts, and people from California are calling and complaining that they aren’t getting chocolate donuts. They’re angry. They’re gonna sue. You need to update the donut availability NOW. You can’t afford to wait for a rebuild and redeployment of the code. All you have to do is go to your Split dashboard and update the targeting rules for your DonutFactory split.

Go to the Set targeting rules section. In the text field where you entered the previous states (Texas, Alaska, and Oregon), enter California.

Click Save Changes (at the top of the screen). Click Confirm.

In just moments, without changing a line of code, California now gets donuts.

http :8080/api/buy-donut donutName==Chocolate username==andrew userLocation==California
Code language: Bash (bash)
Enjoy your Chocolate donut. It costs $1.5. There are 8 of these remaining.
Code language: Shell Session (shell)

Make Your Spring Boot App Production Ready With Docker Compose

The last step is to remove the H2 in-memory database and use a production-ready MySQL database. You’re going to use Docker Compose to run the app image and the database image simultaneously while also mapping their ports.

You can think of Docker Compose as a kind of “meta” Docker or a Docker orchestrator. It allows you to control the deployment of multiple Docker images so that you can do things like spin up a web application, a REDIS cache, an Elasticsearch instance, and a backing database simultaneously. In our somewhat simpler use-case, you’ll use it to deploy a MySQL database along with the Spring Boot web application.

In the build.gradle file, comment out or delete the h2database dependency and replace it with the MySQL connector.

dependencies { // runtimeOnly 'com.h2database:h2' <-- DELETE ME OR COMMENT OUT runtimeOnly 'mysql:mysql-connector-java' <-- ADD ME }
Code language: JSON / JSON with Comments (json)

Download the wait-for-it.sh shell script from the project GitHub page or copy it from this tutorial’s GitHub repository. Copy it in the project root directory. Make it executable using the following command: sudo chmod +x wait-for-it.sh.

You have to use the wait-for-it.sh script because Docker Compose will start both of the containers, and Spring Boot will try to connect to the database before it’s ready, leading to an error. This script causes the execution of the Spring Boot application to wait on the availability of the database. You’ll see below you update the command in the Dockerfile to use this script.

Another project that does a similar thing to the wait-for-it.sh script is Dockerize. I chose not to use it for this example project, but it also allows you to wait for other services to load in a Docker Compose script, so I thought I’d mention it. You can take a look at the Dockerize GitHub page for more info.

Change the Dockerfile in the project root.

FROM openjdk:12 ARG JAR_FILE=build/libs/*.jar COPY ${JAR_FILE} app.jar COPY wait-for-it.sh wait-for-it.sh ENTRYPOINT ["./wait-for-it.sh", "db:3306", "--", "java", "-jar", "app.jar"]
Code language: Bash (bash)

Create a docker-compose.yml file in the project root.

version: '3' services: db: image: mysql:8.0 restart: always environment: - MYSQL_ROOT_PASSWORD=root - MYSQL_DATABASE=donuts - MYSQL_USER=splitdemo - MYSQL_PASSWORD=splitdemo ports: - 3306:3306 volumes: - db-data:/var/lib/mysql app: build: ./ restart: on-failure depends_on: - db ports: - 8080:8080 environment: - DATABASE_HOST=db - DATABASE_USER=splitdemo - DATABASE_PASSWORD=splitdemo - DATABASE_NAME=donuts - DATABASE_PORT=3306 volumes: db-data:
Code language: YAML (yaml)

Take note that the docker-compose.yml file has your database passwords in it. Don’t check this file into a repository if you’re worried about application security.

Add to src/main/resources/application.properties the following properties. You’re getting all of the necessary database configuration from the environment variables defined in the docker-compose.yml file and passing it to Spring Data here.

split.api-key=<Your API key> spring.datasource.url=jdbc:mysql://${DATABASE_HOST}:${DATABASE_PORT}/${DATABASE_NAME} spring.datasource.username=${DATABASE_USER} spring.datasource.password=${DATABASE_PASSWORD} spring.jpa.hibernate.ddl-auto=update
Code language: Bash (bash)

What you’ve done is configure two Docker services using two images. The first service, db, uses a MySQL 8.0 image and exposes the standard MySQL port 3306. The second is the Spring Boot app built from the Dockerfile you defined and just updated to use the wait-for-it.sh script. You also define the necessary database configuration information and a volume for the database so that the data is persisted.

The property spring.jpa.hibernate.ddl-auto is set to update because it will allow Spring to auto-create the database tables for the data models but won’t drop them between sessions. In an actual production scenario, you would want to set this to none to turn this feature off and manage your database creation separately.

Build the application JAR file. Run the following command from a shell in your project root.

./gradlew build
Code language: Bash (bash)

Build the Docker containers.

docker-compose build
Code language: Bash (bash)

Start the app and the database using Docker Compose.

docker-compose up
Code language: Bash (bash)

You should be able to buy donuts. From a separate shell, run the following command.

http :8080/api/buy-donut donutName==Glazed username==andrew userLocation==Alaska
Code language: Bash (bash)
HTTP/1.1 200 ... Enjoy your Glazed donut. It costs $0.75. There are 19 of these remaining.
Code language: Bash (bash)

To stop the app, press control-c in the shell running Docker Compose.

You can delete the containers using the following command (run from the project root directory).

docker-compose down
Code language: Bash (bash)

You can delete the app containers AND the database volume using the next command.

docker-compose down --volumes
Code language: Bash (bash)

Learn More About Spring Boot, Docker, and Feature Flags

In this tutorial, you created a Spring Boot web application that used Spring Data to persist your data model. You saw how Spring Boot with Spring Data allows you to create a fully-featured REST API from your data model easily. You also added a custom web controller and used dependency injection to inject your data model repository into your controller. You used Split to add a feature flag to your application and saw how it could be used to make changes to application behavior at runtime without having to rebuild or redeploy. Finally, you saw how you could use Docker and Docker Compose to containerize your application.

Ready to learn more? We’ve got some additional resources covering all of these topics and more! Check out:

And as always, we’d love to have you follow us on YouTube and Twitter for more great content!