Introduction to running Spring Boot Web App in Docker
If you are new to Spring Boot like I am, a useful start is to try one of the tutorials available on the Spring website. But what if you want to take it a step further? This repository contains what you need to understand how to start a Spring Boot Restful web service and containerise it using Docker.
Requirements
In order to complete this tutorial, you will need the following installed on your machine:
- Java JDK for development (I used 13.0.2)
- Maven (feel free to use Gradle instead, though the build command and jar name will be different)
- Docker
- Docker compose which comes standard with a Windows Docker installation
- Your favorite IDE will be useful, mine is VS Code
Run
In the cloud
If you'd like to get some experience running this containerised app in the cloud, take a look at my other tutorial on using Terraform to provision this app to AWS cloud services.
From the built project
If you want to jump-start without doing it manually as laid out below, then simply clone this repository and follow these steps:
- Open a terminal in the root directory of this project
- Run
./mvnw clean package
from the root directory will build a jar file containing the app - Move the jar file from the
target/
directory to thedocker/
directory- Why? Docker does not support cross-directory ADDs
- Run
docker-compose up
from thedocker/directory
ordocker-compose -f ../docker/docker-compose.yml up -d
if you're in another directory - open http://localhost:8080 in your web browser or use
curl http://localhost:8080
if you have Windows Subsystem for Linux on your machine - Take a look at the source code for this app - what endpoints are there to play with?
- What happens if you greet the app too many times?
Build-it-yourself
Follow these steps to complete this tutorial manually - it's a much better way to get to grips with the services involved in running a web-app of this nature. So instead of cloning this repository, just follow along below.
Head to the Spring Initializr to create a boiler plate for your project. This project is built with Maven, Java, the lastest stable Spring Boot, Jar packaging and Java 11. I gave the project a name: restservice. Add the following dependencies:
- Rest Repositories
- Thymeleaf
- JPA
- H2
Generate the project, save the zip file, and extract it.
Controller and Request Mapping Annotation:
Let's start by creating an index endpoint that returns information when the server is contacted. Because we are creating a Spring-based Restful web-app, we want to begin by creating both Index.java and IndexController.java files in the src/main/java/com/example/restservice directory.
The resource controller is what handles the HTTP requests. Components are identified with the @RestController
annotation, and the @GetMapping()
annotation routes HTTP GET requests to the annotated method.
Index Endpoint:
Index.java
contains the classic Java class with local variables, constructor and functions. In this example, we want to return a value and string from the index endpoint:
package com.example.restservice;
public class Index {
private final long id;
private final String content;
public Index(long id, String content) {
this.id = id;
this.content = content;
}
public long getId() {
return id;
}
public String getContent() {
return content;
}
}
IndexController.java
contains the resource controller that Spring uses to direct HTTP requests. In this case, when the client accesses the https://localhost:8080/ endpoint we will return a counter and a string that accepts a value if supplied:
package com.example.restservice;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.concurrent.atomic.AtomicLong;
import org.springframework.web.bind.annotation.RequestParam;
@RestController
public class IndexController {
private static final String template = "Hello %s, this index endpoint has %d hits!";
private final AtomicLong counter = new AtomicLong();
@GetMapping("/")
public Index index(@RequestParam(value = "name", defaultValue = "World") String name) {
counter.incrementAndGet();
return new Index(counter.get(), String.format(template, name, counter.get()));
}
}
Because we are returning an object, the output is formatted in JSON using the Jackson JSON library which is automatically included in the Spring web starter dependency.
Greeting Endpoint
Repeating this process, we create Greeting.java and GreetingController.java files. Greeting.java
contains the class of which an object instance is returned:
package com.example.restservice;
public class Greeting {
private final long id;
private final String content;
public Greeting(long id, String content) {
this.id = id;
this.content = content;
}
public long getId() {
return id;
}
public String getContent() {
return content;
}
}
GreetingController.java
contains the @RestController for the Greeting class. When you hit that https://localhost:8080/greeting endpoint, you should see a JSON formatted response.
package com.example.restservice;
import java.util.concurrent.atomic.AtomicLong;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class GreetingController {
private static final String template = "Greetings %s!";
private final AtomicLong counter = new AtomicLong();
@GetMapping("/greeting")
public Greeting greeting(@RequestParam(value = "name", defaultValue = "world") String name) {
if (counter.incrementAndGet() > 3)
return new Greeting(counter.get(), "I think that's enough now...");
else
return new Greeting(counter.get(), String.format(template, name));
}
}
Of course you can use the usual Java syntax to implement interesting features in your web-app. Looking at the greeting
function below, what happens when the web-app is greeted too many times?
Build
To build this project using maven, open a terminal and run .mvnw clean package
from the root directory. This will create a .jar file in a new target/
directory. If you'd like, you can run this jar file using the java -jar target/<file-name>.jar
command. You should see something like this in the terminal:
You should now be able to access http://localhost:8080 and it will display
{"id":1,"content":"Hello World, this index page has 1 hits!"}
As you refresh the page, the counter increments using the incrementAndGet()
function.
Containerise
Create a docker
directory and in it create a Dockerfile
and docker-compose.yml
file. Now copy the jar file to the docker folder.
In the Dockerfile, add the following:
FROM adoptopenjdk/openjdk11:alpine-jre
as the base image - this supports the build of this project (Java 11, or 55)WORKDIR /
to set the working directoryADD rest-service-0.0.1-SNAPSHOT.jar rest-service.jar
to add the jar fileEXPOSE 8080
to expose a container portCMD java -jar rest-service.jar
run the jar file when starting the container
In the docker-compose.yml
file:
- specify the version
- start a service called 'web' which is built from a dockerifle in the same directory
- map a local port to a port in the container:
version: "3.8"
services:
web:
build: .
ports:
- "8080:8080"
Now you can run docker-compose up
when you're in the docker directory.
This builds the image and spins up a container which you can see the details of by running docker ps
.
Test the Service
You should now be able to access http://localhost:8080. Play with the endpoints by adding a ?name=<string>
and seeing what it returns. How is this achieved using class objects and the resource controllers?
Stop the Service
This container can be stopped by executing docker stop <container id>
.
Well Done!
And that is it - congrats on reaching the end of this tutorial. If you want to practice more, try out some official Spring guides. Thanks for stopping by!