Revision #1 on May 24th, 2016: Updated to improve the steps on how to package a REST service as a Docker image and publish it to the Docker Hub.

Spring Cloud Series

Subscribe to my newsletter to receive updates when content like this is published.

  1. Developing Microservices using Spring Boot, Jersey, Swagger and Docker (you are here)
  2. Integration Testing using Spring Boot, Postgres and Docker
  3. Services registration and discovery using Spring Cloud Netflix Eureka Server and client-side load-balancing using Ribbon and Feign
  4. Centralized and versioned configuration using Spring Cloud Config Server and Git
  5. Routing requests and dynamically refreshing routes using Spring Cloud Zuul Server
  6. Microservices Sidecar pattern implementation using Postgres, Spring Cloud Netflix and Docker
  7. Implementing Circuit Breaker using Hystrix, Dashboard using Spring Cloud Turbine Server (work in progress)

1. OVERVIEW

Having used Spring for some years, I was impressed how easy it is to develop Spring-based apps using Spring Boot, an opinionated framework favoring convention over configuration and more impressed how easy it is to build and use common components of distributed systems using Spring Cloud which is built on top of Spring Boot.

Microservices has been a hot topic for a couple of years now, defined as a software architectural style to compose applications from a set of small and collaborating services each one implementing a specific purpose.

This and the other posts in this series are more of a hands-on experience once it has been decided to implement some solution using this pattern. This post won’t cover the trade-off using Microservices or Monolith First as discussed here and here or Don’t Start Monolith school of thoughts. Please follow these links or browse the REFERENCES section if you are also interested in these concepts or debates.

2. REQUIREMENTS

  • Java 7+.
  • Maven 3.2+.
  • Familiarity with Spring Framework.
  • Docker host.

3. CREATE THE HELLO WORLD SERVICE

curl "https://start.spring.io/starter.tgz" -d dependencies=actuator,jersey,web -d language=java -d type=maven-project -d baseDir=springboot-jersey-swagger-docker -d groupId=com.asimio.api -d artifactId=springboot-jersey-swagger-docker -d version=0-SNAPSHOT | tar -xzvf -

This command will create a Maven project in a folder named springboot-jersey-swagger-docker with most of the dependencies required by this post. In the accompanying source code, the project has been refactored to match the packages as described in this post.

To add Spring Boot support to an application, either specify a parent element in pom.xml like previous curl command generates or bringing in Spring Boot dependencies management as a BOM.

...
<dependencyManagement>
  <dependencies>
    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-dependencies</artifactId>
      <version>1.3.5.RELEASE</version>
      <type>pom</type>
      <scope>import</scope>
    </dependency>
  </dependencies>
</dependencyManagement>
...

Again, in this example we are sticking with the parent version.
Add start-class property to pom.xml, it will be the entry point to the application.

...
<parent>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-parent</artifactId>
  <version>1.3.5.RELEASE</version>
  <relativePath /> <!-- lookup parent from repository -->
</parent>
...
<properties>
  <start-class>com.asimio.jerseyexample.main.Application</start-class>
...
</properties>
...

This is a snippet of com.asimio.jerseyexample.main.Application class, this application’s start class as defined in pom.xml.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
package com.asimio.jerseyexample.main;
...
@SpringBootApplication(
  scanBasePackages = {
    "com.asimio.jerseyexample.config", "com.asimio.jerseyexample.rest"
  }
)
public class Application extends SpringBootServletInitializer {

  @Override
  protected SpringApplicationBuilder configure(SpringApplicationBuilder builder) {
    return builder.sources(Application.class);
  }

  public static void main(String[] args) {
    SpringApplication.run(Application.class, args);
  }
}

In line 3, @SpringBootApplication annotation is equivalent to @EnableAutoConfiguration, @Configuration and @ComponentScan.

@SpringBootApplication’s scanBasePackages attribute is just an alias for basePackages attribute in @ComponentScan annotation, meaning Spring IoC container will scan in those packages for components (classes annotated with @Component or its descendants) and wire dependencies.

@Configuration indicates this class can be used by the Spring IoC container as a source of bean definitions (objects returned by methods annotated with @Bean), none in this example.

@EnableAutoConfiguration provides a non-invasive mechanism to automatically configure the application based on dependencies present in the classpath if they haven’t been configured yet. For instance, if Spring Data JPA dependency is found in the classpath, Spring Boot will register a LocalContainerEntityManagerFactoryBean bean only if the developer hasn’t done it yet. As mentioned earlier, Spring Boot is an opinionated framework that favors convention over configuration and by adding Spring Data JPA as a dependency, Spring Boot interprets the application would like to access a RDBMS through a JPA Entity Manager.

In line 8, this class extends from SpringBootServletInitializer class to optionally build this service as a war file (defined as a Maven profile in pom.xml) instead of a fat jar in case it’s desirable to deploy it in an existing Servlet Container. In this article though, it will be deployed and run as a fat jar web application.

4. IMPLEMENT API ENDPOINTS USING JERSEY

If a new project was created using this command, Jersey dependency was already included, if Jersey needs to be added to an existing Spring Boot project, just add the dependency shown below to pom.xml.

...
<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-jersey</artifactId>
</dependency>
...

spring-boot-starter-jersey is now included along with spring-boot-starter-web, meaning there are two servlets, Spring MVC’s and Jersey’s, but more on this later.

A new class that extends from Jersey’s ResourceConfig class needs to be created in order to register the endpoints, providers, etc..

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
package com.asimio.jerseyexample.config;
...
@Component
public class JerseyConfig extends ResourceConfig {
...
  public JerseyConfig() {
    // Register endpoints, providers, ...
    this.registerEndpoints();
  }

  private void registerEndpoints() {
    this.register(HelloResource.class);
    // Available at /<Jersey's servlet path>/application.wadl
    this.register(WadlResource.class);
  }
...

In line 3, this class is annotated @Component, as discussed earlier, com.asimio.jerseyexample.config package is scanned for classes with such annotations and processed by the Spring IoC container.

In line 11, during JerseyConfig’s instantiation, a resource will be registered along with Jersey’s WadlResource, which will dynamically generate wadl-structured documentation accessible through a URL for clients to use as reference when developing clients of the endpoint(s).

And the resource’s implementation:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
package com.asimio.jerseyexample.rest.v1;
...
@Component
@Path("/")
@Consumes(MediaType.APPLICATION_JSON)
@Produces(MediaType.APPLICATION_JSON)
public class HelloResource {

  private static final Logger LOGGER = LoggerFactory.getLogger(HelloResource.class);

  @GET
  @Path("v1/hello/{name}")
  public Response getHelloVersionInUrl(@ApiParam @PathParam("name") String name) {
    LOGGER.info("getHelloVersionInUrl() v1");
    return this.getHello(name, "Version 1 - passed in URL");
  }

  @GET
  @Path("hello/{name}")
  @Consumes("application/vnd.asimio-v1+json")
  @Produces("application/vnd.asimio-v1+json")
  public Response getHelloVersionInAcceptHeader(@PathParam("name") String name) {
    LOGGER.info("getHelloVersionInAcceptHeader() v1");
    return this.getHello(name, "Version 1 - passed in Accept Header");
  }

  private Response getHello(String name, String partialMsg) {
    if ("404".equals(name)) {
      return Response.status(Status.NOT_FOUND).build();
    }
    Hello result = new Hello();
    result.setMsg(String.format("Hello %s. %s", name, partialMsg));
    return Response.status(Status.OK).entity(result).build();
  }
...
}

This is a partial implementation since the completed version is available in accompanying Bitbucket repo.

@Component has been explained earlier and @Path, @GET, @Produces and @Consumes are JAX-RS-related annotations.

The interesting part here is the two endpoints implementation to get a “hello” resource. The implementation version of this API could be passed either in the URL (/api/v1/hello/Orlando) or in the Accept Header (/api/hello/World). This is supported because of @Path annotation at class level being set to the root path of the Jersey’s servlet, which is mapped to /api. Why was version included in …
Because APIs evolve overtime and it should never breaks backwards compatibility.
But …
APIs should never break backwards compatibility.

The last part covered in this section is application.yml, one of the Spring Boot applications default configuration files, along with bootstrap.yml, application.properties and bootstrap.properties.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
...
# Spring MVC dispatcher servlet path. Needs to be different than Jersey's to enable/disable Actuator endpoints access (/info, /health, ...)
server.servlet-path: /
# Jersey dispatcher servlet
spring.jersey.application-path: /api

# http://docs.spring.io/spring-boot/docs/current/reference/htmlsingle/#production-ready-endpoints
# http://docs.spring.io/spring-boot/docs/current/reference/htmlsingle/#howto-use-actuator-with-jersey
endpoints:
  enabled: false
  info:
    enabled: true
  health:
    enabled: true
  metrics:
    enabled: true

# app name and build version updated during build process from Maven properties.
info:
  app:
    name: @project.artifactId@
  build:
    version: @project.version@

Lines 3 and 5 correspond with Spring MVC and Jersey servlet mappings respectively. Spring MVC is required in case the application would like to expose actuator endpoints such as health, info, metrics etc.. Visit this link to get a list of all of the actuator endpoints Spring Boot provides. According to Spring Boot documentation, Spring MVC and Jersey servlets are mapped to the same path (/), that’s why it’s required to change such a mapping in one or both servlets.

Lines 9 through 16 defines which actuator endpoints are enabled, it’s a good practice to disable all of them, as done in line 10 and enable just the ones needed, for instance, one of the actuator endpoints provided is shutdown, so you get the idea. It would also be wise to protect some of the endpoints enabled via Spring Security, but to prevent this post getting overly complicated, it won’t be covered at the moment.

Lines 19 through 23 defines what’s going to be the output when info actuator endpoint is accessed, the placeholders will be replaced with Maven information at build time.

5. BUILDING AND RUNNING THE SERVICE

This application can be run from your preferred IDE as a regular Java application, it can also run from command line using:

mvn clean package
java -jar target/springboot-jersey-swagger-docker.jar

Hitting the endpoints should produce similar output to:

info actuator endpoint

$ curl -v "http://localhost:8000/info"
*   Trying ::1...
* Connected to localhost (::1) port 8000 (#0)
> GET /info HTTP/1.1
> Host: localhost:8000
> User-Agent: curl/7.43.0
> Accept: */*
>
< HTTP/1.1 200 OK
< Server: Apache-Coyote/1.1
< X-Application-Context: application:8000
< Content-Type: application/json;charset=UTF-8
< Transfer-Encoding: chunked
< Date: Wed, 30 Mar 2016 05:49:13 GMT
<
* Connection #0 to host localhost left intact
{"app":{"name":"springboot-jersey-swagger-docker"},"build":{"version":"0-SNAPSHOT"}}

Get resource - Version in URL

$ curl -v "http://localhost:8000/api/v1/hello/world"
*   Trying ::1...
* Connected to localhost (::1) port 8000 (#0)
> GET /api/v1/hello/world HTTP/1.1
> Host: localhost:8000
> User-Agent: curl/7.43.0
> Accept: */*
>
< HTTP/1.1 200 OK
< Server: Apache-Coyote/1.1
< X-Application-Context: application:8000
< Content-Type: application/json;charset=UTF-8
< Content-Length: 48
< Date: Fri, 01 Apr 2016 05:55:29 GMT
<
* Connection #0 to host localhost left intact
{"msg":"Hello world. Version 1 - passed in URL"}

Get resource - Version in Accept Header

$ curl -v -H "Accept: application/vnd.asimio-v1+json" "http://localhost:8000/api/hello/world"
*   Trying ::1...
* Connected to localhost (::1) port 8000 (#0)
> GET /api/hello/world HTTP/1.1
> Host: localhost:8000
> User-Agent: curl/7.43.0
> Accept: application/vnd.asimio-v1+json
>
< HTTP/1.1 200 OK
< Server: Apache-Coyote/1.1
< X-Application-Context: application:8000
< Content-Type: application/vnd.asimio-v1+json;charset=UTF-8
< Content-Length: 58
< Date: Fri, 01 Apr 2016 05:57:57 GMT
<
* Connection #0 to host localhost left intact
{"msg":"Hello world. Version 1 - passed in Accept Header"}

Resource not found - Version in URL

$ curl -v "http://localhost:8000/api/v1/hello/404"
*   Trying ::1...
* Connected to localhost (::1) port 8000 (#0)
> GET /api/v1/hello/404 HTTP/1.1
> Host: localhost:8000
> User-Agent: curl/7.43.0
> Accept: */*
>
< HTTP/1.1 404 Not Found
< Server: Apache-Coyote/1.1
< X-Application-Context: application:8000
< Content-Type: application/json;charset=UTF-8
< Transfer-Encoding: chunked
< Date: Sat, 02 Apr 2016 03:21:16 GMT
<
* Connection #0 to host localhost left intact
{"timestamp":1459567275985,"status":404,"error":"Not Found","message":"Not Found","path":"/api/v1/hello/404"}

Create resource - Version in Accept Header

$ curl -v -X POST -H 'Content-Type: application/vnd.asimio-v1+json' -d '{ "msg": "world"}' http://120.240.1.192:8000/api/hello
*   Trying 120.240.1.192...
* Connected to 120.240.1.192 (120.240.1.192) port 8000 (#0)
> POST /api/hello HTTP/1.1
> Host: 120.240.1.192:8000
> User-Agent: curl/7.43.0
> Accept: */*
> Content-Type: application/vnd.asimio-v1+json
> Content-Length: 17
>
* upload completely sent off: 17 out of 17 bytes
< HTTP/1.1 201 Created
< Server: Apache-Coyote/1.1
< X-Application-Context: application:8000
< Location: http://120.240.1.192:8000/api/hello/world
< Content-Length: 0
< Date: Fri, 01 Apr 2016 06:10:44 GMT
<
* Connection #0 to host 120.240.1.192 left intact

Resources WADL

$ curl -v "http://localhost:8000/api/application.wadl"
*   Trying ::1...
* Connected to localhost (::1) port 8000 (#0)
> GET /api/application.wadl HTTP/1.1
> Host: localhost:8000
> User-Agent: curl/7.43.0
> Accept: */*
>
< HTTP/1.1 200 OK
< Server: Apache-Coyote/1.1
< X-Application-Context: application:8000
< Last-modified: Fri, 01 Apr 2016 02:12:50 EDT
< Content-Type: application/vnd.sun.wadl+xml;charset=UTF-8
< Content-Length: 2758
< Date: Fri, 01 Apr 2016 06:12:50 GMT
<
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<application xmlns="http://wadl.dev.java.net/2009/02">
  <doc xmlns:jersey="http://jersey.java.net/" jersey:generatedBy="Jersey: 2.22.2 2016-02-16 13:32:17"/>
  <doc xmlns:jersey="http://jersey.java.net/" jersey:hint="This is simplified WADL with user and core resources only. To get full WADL with extended resources use the query parameter detail. Link: http://localhost:8000/api/application.wadl?detail=true"/>
  <grammars/>
  <resources base="http://localhost:8000/api/">
    <resource path="/swagger.{type:json|yaml}">
      <param xmlns:xs="http://www.w3.org/2001/XMLSchema" name="type" style="template" type="xs:string"/>
      <method id="getListing" name="GET">
        <response>
          <representation mediaType="application/json"/>
          <representation mediaType="application/yaml"/>
        </response>
      </method>
    </resource>
    <resource path="/">
      <resource path="v1/hello/{name}">
        <param xmlns:xs="http://www.w3.org/2001/XMLSchema" name="name" style="template" type="xs:string"/>
        <method id="getHelloVersionInUrl" name="GET">
          <response>
            <representation mediaType="application/json"/>
          </response>
        </method>
      </resource>
      <resource path="hello/{name}">
        <param xmlns:xs="http://www.w3.org/2001/XMLSchema" name="name" style="template" type="xs:string"/>
        <method id="getHelloVersionInAcceptHeader" name="GET">
          <response>
            <representation mediaType="application/vnd.asimio-v1+json"/>
          </response>
        </method>
      </resource>
      <resource path="v1/hello">
        <method id="createHelloVersionInUrl" name="POST">
          <request>
            <representation mediaType="application/json"/>
          </request>
          <response>
            <representation mediaType="application/json"/>
          </response>
        </method>
      </resource>
      <resource path="hello">
        <method id="createHelloVersionInAcceptHeader" name="POST">
          <request>
            <representation mediaType="application/vnd.asimio-v1+json"/>
          </request>
          <response>
            <representation mediaType="application/json"/>
          </response>
        </method>
      </resource>
    </resource>
  </resources>
</application>
* Connection #0 to host localhost left intact

6. DOCUMENT THE REST APIs WITH SWAGGER

Next we need to add Swagger dependency to pom.xml and Swagger UI to the application to provide a really nice UI that would allow developers to use it not only as documentation but also to interact with the API endpoints.

...
<properties>
  <swagger.version>1.5.8</swagger.version>
</properties>
...
<dependency>
  <groupId>io.swagger</groupId>
  <artifactId>swagger-jersey2-jaxrs</artifactId>
  <version>${swagger.version}</version>
</dependency>
...

Download Swagger UI zip file from https://github.com/swagger-api/swagger-ui/releases, 2.1.4 being the latest at the time this post was written.
Extract and move resulting folder to src/main/resources/static since this set of files is that, static content including JavaScript, CSS, images and HTML files (other locations Spring Boot apps used to serve static content are: /META-INF/resources, /resources, and /public in the classpath).
Update src/main/resources/static/index.html file for Swagger UI to find dynamically-generated Swagger definition file.
Replace:

url = "http://petstore.swagger.io/v2/swagger.json";

with:

url = "/api/swagger.json";

Now an instance of Swagger’s BeanConfig class is needed, it’s responsible for dynamically generating the Swagger definition file used to feed Swagger UI. This instance doesn’t need to be a Spring-managed bean, it could be instantiated in a Servlet’s init() method, or any other Singleton instance. In the example application bundled with this blog entry, JerseyConfig class was re-used:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
package com.asimio.jerseyexample.config;
...
@Component
public class JerseyConfig extends ResourceConfig {

  @Value("${spring.jersey.application-path:/}")
  private String apiPath;

...
  @PostConstruct
  public void init() {
    // Register components where DI is needed
    this.configureSwagger();
  }
...
  private void configureSwagger() {
    // Available at localhost:port/api/swagger.json
    this.register(ApiListingResource.class);
    this.register(SwaggerSerializers.class);

    BeanConfig config = new BeanConfig();
    config.setConfigId("springboot-jersey-swagger-docker-example");
    config.setTitle("Spring Boot + Jersey + Swagger + Docker Example");
    config.setVersion("v1");
    config.setContact("Orlando L Otero");
    config.setSchemes(new String[] { "http", "https" });
    config.setBasePath(this.apiPath);
    config.setResourcePackage("com.asimio.jerseyexample.rest.v1");
    config.setPrettyPrint(true);
    config.setScan(true);
  }
}

In lines 18 and 19 a couple of Swagger JAX-RS providers are registered. A few settings are important from BeanConfig instantiation and configuration. setScan(true) indicates to scan packages set via setResourcePackage() for Swagger annotations and setBasePath() indicates the path where the dynamically generated swagger.json would be reachable.

We still need to add Swagger annotations to the resource class and its endpoints we would like to document, these would be processed by Swagger’s BeanConfig instance discussed earlier.

package com.asimio.jerseyexample.rest.v1;
...
@Component
...
@Api(value = "Hello resource", produces = "application/json")
public class HelloResource {

  ...
  @ApiOperation(value = "Gets a hello resource. Version 1 - (version in URL)", response = Hello.class)
  @ApiResponses(value = {
    @ApiResponse(code = 200, message = "hello resource found"),
    @ApiResponse(code = 404, message = "Given admin user not found")
  })
  public Response getHelloVersionInUrl(@ApiParam @PathParam("name") String name) {
    ...
  }

  ...
  @ApiOperation(value = "Gets a hello resource. World Version 1 (version in Accept Header)", response = Hello.class)
  @ApiResponses(value = {
    @ApiResponse(code = 200, message = "hello resource found"),
    @ApiResponse(code = 404, message = "hello resource not found")
  })
  public Response getHelloVersionInAcceptHeader(@PathParam("name") String name) {
    ...
  }

  ...
  @ApiOperation(value = "Creates hello resource. Version 1 - (version in URL)", response = Hello.class)
  @ApiResponses(value = {
    @ApiResponse(code = 201, message = "hello resource created", responseHeaders = {
      @ResponseHeader(name = "Location", description = "The URL to retrieve created resource", response = String.class)
    })
  })
  public Response createHelloVersionInUrl(Hello hello, @Context UriInfo uriInfo) {
    ...
  }

  ...
  @ApiOperation(value = "Creates hello resource. Version 1 - (version in Accept Header)", response = Hello.class)
  @ApiResponses(value = {
    @ApiResponse(code = 201, message = "hello resource created", responseHeaders = {
      @ResponseHeader(name = "Location", description = "The URL to retrieve created resource", response = String.class)
    })
  })
  public Response createHelloVersionInAcceptHeader(Hello hello, @Context UriInfo uriInfo) {
    ...
  }
...
}

Some screenshots taken from running the application:

  • Available endpoints Spring Boot, Jersey, Swagger - Available endpoints Spring Boot, Jersey, Swagger - Available endpoints

  • Get Hello resource - Version in URL Spring Boot, Jersey, Swagger - Get resource - Version in URL Spring Boot, Jersey, Swagger - Get resource - Version in URL

  • Create Hello resource - Version in Accept Header Spring Boot, Jersey, Swagger - Create resource - Version in Accept Header Spring Boot, Jersey, Swagger - Create resource - Version in Accept Header

So, what if there would be a need to add another implementation version of the “Hello” resource documenting both versions using Swagger? Unfortunately the BeanConfig’s approach used for this blog post won’t work. I spent some time trying to get versions being passed in the URL and Accept Header to work with Swagger and ended up updating the initial multi-version API example to just include one version.

The reason I believe it won’t work is because BeanConfig instance dynamically creates only one Swagger definition file and it would require one for each version. A possible solution would be to keep just the endpoints where the version is passed in the URL and the single Swagger definition file would include description for all the versions. It just doesn’t feel the right solution, the documentation might get bloated and I didn’t want to give up on the version being passed in the Accept Header.

But I have good news, I have a working example of a multi-versioned API using Jersey and Swagger generating multiple Swagger definition files, one for each version, all of them used by the same Swagger UI, so stay tuned, I’ll be creating another post Documenting multiple REST API versions using Spring Boot Jersey and Swagger in the next 2-3 days to review this approach.

7. PACKAGE THE SERVICE INTO A DOCKER IMAGE

Last topic to be covered will be packaging the service as a Docker image and running it in a Docker container. Some familiarity with Docker is assumed.
In this blog entry a Docker maven plugin from Spotify was used to build the image and push it to its public Docker registry repo.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
...
<properties>
  <docker.image.prefix>asimio</docker.image.prefix>
  <push.image>false</push.image>
  <docker-maven-plugin-spotify.version>0.4.10</docker-maven-plugin-spotify.version>
</properties>
...
<build>
  <finalName>${project.artifactId}</finalName>
  <plugins>
    ...
    <plugin>
      <groupId>com.spotify</groupId>
      <artifactId>docker-maven-plugin</artifactId>
      <version>${docker-maven-plugin-spotify.version}</version>
      <!-- Include:
            export DOCKER_HOST=tcp://docker:4243
            in host executing mvn docker:build
      -->
      <configuration>
        <!-- One or the other -->
        <!-- First add server entry in settings.xml -->
        <serverId>docker-hub</serverId>
        <!-- Uses ~/.docker/config.json created once logged in using "docker login" command -->
        <!-- <useConfigFile>true</useConfigFile> -->

        <imageName>${docker.image.prefix}/${project.artifactId}:${project.version}</imageName>
        <pushImage>${push.image}</pushImage>
        <forceTags>true</forceTags>
        <imageTags>
          <imageTag>${project.version}</imageTag>
          <imageTag>latest</imageTag>
        </imageTags>
        <dockerDirectory>src/main/docker</dockerDirectory>
        <resources>
          <resource>
            <targetPath>/</targetPath>
            <directory>${project.build.directory}</directory>
            <include>${project.build.finalName}.jar</include>
          </resource>
        </resources>
      </configuration>
    </plugin>
...

From previous pom.xml’s snippet, Dockefile, discussed next, can be found in src/main/docker, the resulting Java artifact, whose version has been stripped via <build>’s <finalName> is included in the Docker image.

The image is named asimio/springboot-jersey-swagger-docker (see Plugin documentation for more options, like pushing Docker image to a different or private Docker registry) and will be tagged with latest and 1.0.x. A build number replacement for ${project.version} instead of 0-SNAPSHOT.

It can also be optionally pushed to Docker registry passing push.image as VM argument, lets say false for local builds but true in a CI environment, for instance:

mvn -U -X clean versions:set -DnewVersion=1.0.37
mvn -U -X package docker:build -Dpush.image=true

But before being able to push this image to the Docker Hub, an account is required and its credentials being stored in ~/.m2/settings.xml (for Maven to use) of the Box/VM/Docker container building the Java REST application and bundling in a Docker image. Such a settings.xml’s section might look like:

...
<server>
  <id>docker-hub</id>
  <username>Your Docker Hub username</username>
  <password>Your Docker Hub password</password>
  <configuration>
    <email>Your Docker Hub email used during registration process</email>
  </configuration>
</server>
...

And this server is referred in pom.xml via:

...
<serverId>docker-hub</serverId>
...

Application’s Dockerfile

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
FROM azul/zulu-openjdk:8
MAINTAINER Orlando L Otero <ootero@asimio.net>, https://bitbucket.org/asimio/springboot-jersey-swagger-docker-example.git

VOLUME /tmp

# Update Ubuntu
RUN \
  bash -c 'apt-get -qq update && apt-get -y upgrade && apt-get -y autoclean && apt-get -y autoremove' && \
  bash -c 'DEBIAN_FRONTEND=noninteractive apt-get install -y curl wget tar'

ENV USER_NAME jerseyexample
ENV APP_HOME /opt/poc-api/$USER_NAME

RUN \
  useradd -ms /bin/bash $USER_NAME && \
  mkdir -p $APP_HOME

ADD springboot-jersey-swagger-docker.jar ${APP_HOME}/springboot-jersey-swagger-docker.jar
RUN \
  chown $USER_NAME $APP_HOME/springboot-jersey-swagger-docker.jar && \
  bash -c 'touch ${APP_HOME}/springboot-jersey-swagger-docker.jar'

ENV JAVA_TOOL_OPTIONS "-Xms128M -Xmx128M -Djava.awt.headless=true -Djava.security.egd=file:/dev/./urandom"

USER $USER_NAME
WORKDIR $APP_HOME
ENTRYPOINT ["java", "-jar", "springboot-jersey-swagger-docker.jar"]

# Run as:
# docker run -idt -p 8701:8701 -e appPort=8701 asimio/springboot-jersey-swagger-docker:latest

8. RUNNING THE SERVICE IN A DOCKER CONTAINER

sudo docker pull asimio/springboot-jersey-swagger-docker:1.0.52
...
sudo docker run -idt -p 8701:8701 -e appPort=8701 asimio/springboot-jersey-swagger-docker:latest
162f5d2773b1b5615bac91d864aa28096f0f114c83bbac6bccef08a6e809e674
sudo docker logs 162f5d2773b1b5615bac91d864aa28096f0f114c83bbac6bccef08a6e809e674
...
2016-04-05 03:15:48 INFO  EndpointMBeanExporter:674 - Located managed bean 'healthEndpoint': registering with JMX server as MBean [org.springframework.boot:type=Endpoint,name=healthEndpoint]
2016-04-05 03:15:48 INFO  EndpointMBeanExporter:674 - Located managed bean 'infoEndpoint': registering with JMX server as MBean [org.springframework.boot:type=Endpoint,name=infoEndpoint]
2016-04-05 03:15:48 INFO  EndpointMBeanExporter:674 - Located managed bean 'metricsEndpoint': registering with JMX server as MBean [org.springframework.boot:type=Endpoint,name=metricsEndpoint]
2016-04-05 03:15:48 INFO  Http11NioProtocol:180 - Initializing ProtocolHandler ["http-nio-8701"]
2016-04-05 03:15:48 INFO  Http11NioProtocol:180 - Starting ProtocolHandler ["http-nio-8701"]
2016-04-05 03:15:48 INFO  NioSelectorPool:180 - Using a shared selector for servlet write/read
2016-04-05 03:15:48 INFO  TomcatEmbeddedServletContainer:162 - Tomcat started on port(s): 8701 (http)
2016-04-05 03:15:48 INFO  Application:57 - Started Application in 6.502 seconds (JVM running for 7.734)

First pulling the image from the public Docker Hub, then just start a container, mapping the host port to the container port (8701:8701) and passing an environment variable (-e appPort) telling Spring Boot to configure Tomcat to listen on port 8701 instead of the default port 8000 (as defined in application.yml).

Thanks for reading and as always, feedback is very much appreciated. If you found this post helpful and would like to receive updates when content like this gets published, sign up to the newsletter.

9. SOURCE CODE

Accompanying source code for this blog post can be found at:

10. REFERENCES