Containerized REST endpoint
This example explains how to create a Docker container image for running an application that exposes a REST endpoint by implementing a JAX-RS resource. It runs an embedded Tomcat server and creates a context with a Jersey servlet for allocating the resource.
Yes, another image type could be created, such as VirtualBox ISO or Amazon Machine Image. And another application technology could be used as well, such Spring Boot Web for writing REST controllers.
Project layout
In order to create the Docker image through Packer, a build execution is declared to take place when installing. In this example, only vars
configuration property is used to define two variables, dockerRepository and dockerTag, which will be referenced on Packer template.
pom.xml (packer-maven-plugin)
<plugin> <groupId>com.github.codeteapot.maven.plugins</groupId> <artifactId>packer-maven-plugin</artifactId> <version>1.0.0</version> <executions> <execution> <id>packer-build</id> <goals> <goal>build</goal> </goals> <phase>install</phase> <configuration> <vars> <property> <name>dockerRepository</name> <value>examples/rest-endpoint</value> </property> <property> <name>dockerTag</name> <value>${project.version}</value> </property> </vars> </configuration> </execution> </executions> </plugin>
By default, the working directory of Packer build command specified by inputDirectory
property is ${project.build.directory}/packer/input
. So, all contents used by Packer must be put there.
One can take advantage of copy-resources goal of Maven Resources Plugin to copy needed resources to build the image.
The following snippet copies
- filtered Packer resources from
${basedir}/src/main/packer
, - non-filtered web application resources from
${basedir}/src/main/webapp
and - non-filtered JAR generated by the project
to the suitable destination inside Packer input directory.
pom.xml (maven-resources-plugin)
<plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-resources-plugin</artifactId> <version>3.2.0</version> <executions> <execution> <id>packer-resources</id> <goals> <goal>copy-resources</goal> </goals> <phase>generate-resources</phase> <configuration> <outputDirectory>${project.build.directory}/packer/input</outputDirectory> <resources> <resource> <directory>${basedir}/src/main/packer</directory> <filtering>true</filtering> </resource> </resources> </configuration> </execution> <execution> <id>webapp-resources</id> <goals> <goal>copy-resources</goal> </goals> <phase>generate-resources</phase> <configuration> <outputDirectory>${project.build.directory}/packer/input/tomcat/webapps/ROOT</outputDirectory> <resources> <resource> <directory>${basedir}/src/main/webapp</directory> <filtering>false</filtering> </resource> </resources> </configuration> </execution> <execution> <id>application-resources</id> <goals> <goal>copy-resources</goal> </goals> <phase>package</phase> <configuration> <outputDirectory>${project.build.directory}/packer/input/lib</outputDirectory> <resources> <resource> <directory>${project.build.directory}</directory> <include>${project.artifactId}-${project.version}.jar</include> <filtering>false</filtering> </resource> </resources> </configuration> </execution> </executions> </plugin>
In addition, non-provided dependencies must be available at classpath. In this case, copy-dependencies of Maven Dependency Plugin could be used.
The following snippet shows how to do it.
pom.xml (maven-dependency-plugin)
<plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-dependency-plugin</artifactId> <version>3.1.2</version> <executions> <execution> <id>application-dependencies</id> <goals> <goal>copy-dependencies</goal> </goals> <phase>package</phase> <configuration> <outputDirectory>${project.build.directory}/packer/input/lib</outputDirectory> </configuration> </execution> </executions> </plugin>
Packer source folder is expected to have the template used by the build command, which is template.json
by default, unless another is specified by template
configuration parameter. The following one buids a Docker image, based on openjdk:8, that
- executes a setup script,
- copies web application resources, and
- copies application JAR and its dependencies to classpath.
src/main/packer/template.json
{ "variables": { "dockerRepository": "repository", "dockerTag": "tag", "tomcatLibDir": "/var/lib/tomcat", "applicationLibDir": "/usr/local/lib/application" }, "builders": [ { "type": "docker", "image": "openjdk:8", "commit": true, "changes": [ "ENTRYPOINT /init" ] } ], "provisioners": [ { "type": "shell", "script": "{{template_dir}}/setup.sh", "environment_vars": [ "TOMCAT_LIB_DIR={{ user `tomcatLibDir` }}", "APPLICATION_LIB_DIR={{ user `applicationLibDir` }}" ] }, { "type": "file", "source": "{{template_dir}}/tomcat/", "destination": "{{ user `tomcatLibDir` }}" }, { "type": "file", "source": "{{template_dir}}/lib/", "destination": "{{ user `applicationLibDir` }}" } ], "post-processors": [ { "type": "docker-tag", "repository": "{{ user `dockerRepository` }}", "tag": "{{ user `dockerTag` }}" } ] }
Variables dockerRepository and dockerTag, defined at packer-build
execution configuration, are used here to determine repository and tag respectively of docker-tag post-processor.
Setup script creates the container file-system layout and prepares an init script that acts as the ENTRYPOINT of Docker process.
src/main/packer/setup.sh
#!/bin/sh mkdir $TOMCAT_LIB_DIR mkdir $APPLICATION_LIB_DIR cat << EOF >> /init #!/bin/sh java \ -classpath $APPLICATION_LIB_DIR \ -Dcom.github.codeteapot.maven.plugins.packer.example.tomcatLibDir=$TOMCAT_LIB_DIR \ -jar \ $APPLICATION_LIB_DIR/${project.artifactId}-${project.version}.jar EOF chmod +x /init
Properties project.artifactId
and project.version
are substituted by their actual values before this file is copied to ${project.build.directory}/packer/input
directory since it is a filtered resource.
At the end, JAR manifest must be configured to specify main class of this application and to add classpath entries as described on Set Up The Classpath example of Maven JAR Plugin. In this example, com.github.codeteapot.maven.plugins.packer.example.ExampleService
is the main class.
pom.xml (maven-jar-plugin)
<plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-jar-plugin</artifactId> <version>3.2.0</version> <configuration> <archive> <manifest> <addClasspath>true</addClasspath> <mainClass>com.github.codeteapot.maven.plugins.packer.example.ExampleService</mainClass> </manifest> </archive> </configuration> </plugin>
As summary, this layout is valid for this example, but a similar approach can be used on many other use cases. The main idea is to run an application through a Docker container. Anyway, this is a most common use case, using a Tomcat embedded server.
Application implementation
This example REST endpoint is implemented through JAX-RS resource deployed on an embedded Tomcat server using a Jersey servlet.
First of all, a Tomcat instance listening on port 8000 is created. Directory on path specified by com.github.codeteapot.maven.plugins.packer.example.tomcatLibDir
system property is used as base directory for allocating server files.
Since it is designed to have only one web application, a single root context is used. There is created a "/index.html" file as welcome one, so root path will serve this file.
Two servlets are configured.
- The
default-servlet
is responsible of serving static files. - The
jax-rs-servlet
is responsible of serving JAX-RS resources.
Server is then ready to be started.
src/main/java/com/github/codeteapot/maven/plugins/packer/example/ExampleService.java
public static void main(String[] args) throws LifecycleException { Tomcat tomcat = new Tomcat(); tomcat.setPort(8000); tomcat.setBaseDir( getProperty("com.github.codeteapot.maven.plugins.packer.example.tomcatLibDir")); tomcat.getConnector(); Context context = tomcat.addContext("", "ROOT"); context.addWelcomeFile("/index.html"); Wrapper defaultServlet = context.createWrapper(); defaultServlet.setName("default-servlet"); defaultServlet.setServletClass("org.apache.catalina.servlets.DefaultServlet"); defaultServlet.setLoadOnStartup(1); context.addChild(defaultServlet); context.addServletMappingDecoded("/", "default-servlet"); Wrapper restServlet = context.createWrapper(); restServlet.setName("jax-rs-servlet"); restServlet.setServletClass("org.glassfish.jersey.servlet.ServletContainer"); restServlet.addInitParameter( "jersey.config.server.provider.packages", "com.github.codeteapot.maven.plugins.packer.example.resources"); restServlet.setLoadOnStartup(1); context.addChild(restServlet); context.addServletMappingDecoded("/rest/*", "jax-rs-servlet"); tomcat.start(); }
Static files comes from ${basedir}/main/webapp
directory. They are accessible across all root children paths by default.
Welcome file for root path that gives two links to a pair of hypothetical example resources. They are absolute from root and inside the same host, so their references are prefixed by "/rest" context path.
src/main/webapp/index.html
<html> <head> <title>REST Example</title> </head> <body> <h1>REST Example</h1> <ul> <li><a href="/rest/examples/1">Example #1</a></li> <li><a href="/rest/examples/2">Example #2</a></li> </ul> </body> </html>
On the other hand, "/rest/*" children paths are mapped to be served by Jersey servlet. It scans all JAX-RS-annotated classes inside com.github.codeteapot.maven.plugins.packer.example.resources
package in order to instantiate appropriated resource classes.
Here is being an "/examples" resource, items of which are available by its identifier. Single example response represents any example item with its only identifier field.
src/main/java/com/github/codeteapot/maven/plugins/packer/example/resources/ExamplesResource.java
@Path("/examples") @Produces(APPLICATION_JSON) public class ExamplesResource { @GET @Path("/{id}") public Response get(@PathParam("id") long id) { return status(OK) .entity(singletonMap("id", id)) .build(); } }
This application has a little few dependencies.
- jakarta.ws.rs-api
- Makes JAX-RS API available at compile time. It is needed to be non-provided, or else
jakarta.ws.rs.ProcessingException
is not found at runtime. - tomcat-catalina
- Is used at compile time to instantiate and configure a Tomcat embedded server.
- jersey-container-servlet-core
- Makes Jersey servlet class available at runtime.
- jersey-hk2
- Is only added to avoid "InjectionManagerFactory" to be not found at runtime.
- jersey-media-json-jackson
- Implements a JSON serializer for having a JSON response based on an arbitrary map of resource fields.
pom.xml (dependencies)
<dependencies> <dependency> <groupId>jakarta.ws.rs</groupId> <artifactId>jakarta.ws.rs-api</artifactId> <version>3.0.0</version> </dependency> <dependency> <groupId>org.apache.tomcat</groupId> <artifactId>tomcat-catalina</artifactId> <version>10.0.0-M9</version> </dependency> <dependency> <groupId>org.glassfish.jersey.containers</groupId> <artifactId>jersey-container-servlet-core</artifactId> <version>3.0.0-M6</version> <scope>runtime</scope> </dependency> <dependency> <groupId>org.glassfish.jersey.inject</groupId> <artifactId>jersey-hk2</artifactId> <version>3.0.0-M6</version> <scope>runtime</scope> </dependency> <dependency> <groupId>org.glassfish.jersey.media</groupId> <artifactId>jersey-media-json-jackson</artifactId> <version>3.0.0-M6</version> <scope>runtime</scope> </dependency> </dependencies>
All of them, including all transitive dependencies, will be copied along the application artifact inside classpath.
Running container
After packaging phase is completed, the Docker image is created. As indicated through variables of build command, its repository is examples/rest-endpoint
and its tag is 1.0.0
, that is equivalent to the artifact version.
A container could be run as follows.
$ docker run --rm -it -p 80:8000 examples/rest-endpoint:1.0.0
Mapping port 80 to 8000 permits the application welcome page to be visited at http://localhost/. Port 80 must be free before running it.
Example files
Files used to write this example are available here.
- pom.xml
- src/main/java/com/github/codeteapot/maven/plugins/packer/example/ExampleService.java
- src/main/java/com/github/codeteapot/maven/plugins/packer/example/resources/ExamplesResource.java
- src/main/packer/setup.sh
- src/main/packer/template.json
- src/main/webapp/index.html
Take it into account that these files are used to write snippets for this page, so snippet demarcation comments may be present.