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:
- creating a persisted data model from a Java class (donuts, in our case),
- using
CrudRepository
to enable persistence of the data model instances, - activating custom query methods for the data model,
- creating a REST controller that manipulates data model instances,
- how to persist enum data types, and
- 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;
}
}
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;
}
}
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);
}
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();
}
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
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]
Use HTTPie to make a GET request.
http :8080
HTTP/1.1 200
...
{
"_links": {
"donuts": {
"href": "http://localhost:8080/donuts"
},
"profile": {
"href": "http://localhost:8080/profile"
}
}
}
The /donuts
endpoint listed is the controller generated by Spring Data for your data model. Make a request at that endpoint.
http :8080/donuts
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"
}
}
}
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
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"
}
GET the root endpoint again, and you’ll see the donut you added.
http :8080/donuts
...
{
"_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"
}
]
},
...
}
You could also DELETE the donut. Notice the donut ID hidden in the link.
http DELETE :8080/donuts/1
HTTP/1.1 204
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.";
}
}
}
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));
}
}
Press control-c
to stop your application, if you haven’t already, and restart it.
./gradlew bootRun
Hit the root endpoint again, and you’ll see the bootstrapped data.
http :8080/donuts
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"
}
]
},
...
}
Now, buy a donut!
http ":8080/api/buy-donut?donutName=Maple"
HTTP/1.1 200
...
Enjoy your Maple donut. It costs $1.0. There are 4 of these remaining.
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"]
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
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 .
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
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
Run the Docker image.
docker run -p 8080:8080 splitdemo/spring-boot-docker
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)
You should also be able to list and buy donuts.
http :8080/donuts
http ":8080/api/buy-donut?donutName=Maple"
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'
}
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;
}
}
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>
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.";
}
}
}
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);
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
docker build --build-arg JAR_FILE="build/libs/*.jar" -t splitdemo/spring-boot-docker .
Run the Docker image.
docker run -p 8080:8080 splitdemo/spring-boot-docker
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
Enjoy your Chocolate donut. It costs $1.5. There are 9 of these remaining.
California doesn’t.
http :8080/api/buy-donut donutName==Chocolate username==andrew userLocation==California
Wah. Wah. No chocolate donuts for you.
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
Enjoy your Chocolate donut. It costs $1.5. There are 8 of these remaining.
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
}
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"]
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:
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
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
Build the Docker containers.
docker-compose build
Start the app and the database using Docker Compose.
docker-compose up
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
HTTP/1.1 200
...
Enjoy your Glazed donut. It costs $0.75. There are 19 of these remaining.
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
You can delete the app containers AND the database volume using the next command.
docker-compose down --volumes
Credits : https://www.split.io/blog/containerization-spring-boot-docker/