Tomcat as code, without a plugin

Page content

A crane (Cargo) lowers a Tomcat cabinet beside a painted outline of a missing tool (no plugin); ants (Ant) carry off unwanted parts (trimmed admin apps); an engineer adds parchments (the overlay), another taps the cabinet with a hammer (integration tests), and a helper carries one to the rack (production)

Plain Tomcat is enough for many Java web applications, but it has no maintained Maven plugin that builds the server from the repository, as TomEE has. Cargo, an overlay folder and the Ant plugin fill that gap. The result passes the same six principles as TomEE as code, and it also runs integration tests on the real server.

The second and third parts of the series used the TomEE plugin. This post replaces it.

Why Cargo

Apache’s Tomcat Maven plugin had its last release in 2013 and embeds Tomcat 7, which implements Servlet 3.0. Current frameworks need a newer Servlet version.

Cargo downloads, configures, starts and packages many application servers, Tomcat included, and its Maven plugin is maintained. It describes a server with three concepts. The container is the server type and version, the configuration is the home folder that Cargo generates, and the deployables are the applications.

<properties>
    <tomcat.version>9.0.121</tomcat.version>
    <tomcat.port>8080</tomcat.port>
</properties>

<plugin>
    <groupId>org.codehaus.cargo</groupId>
    <artifactId>cargo-maven3-plugin</artifactId>
    <version>${cargo.version}</version>
    <configuration>
        <container>
            <containerId>tomcat9x</containerId>
            <!-- The Tomcat distribution comes from a Maven repository, not from a URL -->
            <artifactInstaller>
                <groupId>org.apache.tomcat</groupId>
                <artifactId>tomcat</artifactId>
                <version>${tomcat.version}</version>
            </artifactInstaller>
        </container>
        <configuration>
            <properties>
                <cargo.servlet.port>${tomcat.port}</cargo.servlet.port>
            </properties>
        </configuration>
        <deployables>
            <deployable>
                <type>war</type>
                <location>${project.build.directory}/${project.build.finalName}.war</location>
                <properties>
                    <context>ROOT</context>
                </properties>
            </deployable>
        </deployables>
        <packager>
            <outputLocation>${project.build.directory}/tomcat</outputLocation>
        </packager>
    </configuration>
    <executions>
        <execution>
            <id>package-tomcat</id>
            <phase>package</phase>
            <goals>
                <goal>configure</goal>
                <goal>package</goal>
            </goals>
        </execution>
    </executions>
</plugin>
Command or settingResult
mvn package cargo:runDownloads the declared Tomcat, generates its home under target/, deploys the WAR and runs it, for use from the IDE
mvn packageLeaves a complete Tomcat with the WAR deployed in target/tomcat, for the container image
tomcat.versionA minor upgrade is one line. A new major version also changes containerId
artifactInstallerTomcat comes from the Maven repository, through the same proxy and cache as other dependencies
explicit WAR locationWithout it, a plain mvn cargo:run starts an empty Tomcat that answers 404

What Cargo lacks

TomEE plugin featureGap in Cargo
Overlay folderCargo copies files listed one by one
Library removal by prefixCargo only adds
System properties in the packaged servercargo.jvmargs applies when Cargo starts the server and is not written into the package

Closing the gaps

An overlay folder

Keep a folder that mirrors Tomcat’s layout and holds only the files you changed:

src/main/tomcat/
└── conf/
    ├── catalina.properties
    └── logging.properties

Point Cargo at the whole folder:

<configuration>
    <files>
        <file>
            <file>${project.basedir}/src/main/tomcat</file>
            <todir>.</todir>
        </file>
    </files>
</configuration>

The setting applies to cargo:run, to the package and to every execution that uses this configuration. Leave server.xml out of the overlay, because Cargo generates it and writes the port there.

System properties in catalina.properties

Tomcat sets every entry of conf/catalina.properties as a system property at startup, whether Cargo, catalina.sh or an entrypoint starts it. Properties that every environment needs belong there:

# Resolve ${...} in server.xml and context.xml from environment variables
org.apache.tomcat.util.digester.PROPERTY_SOURCE=org.apache.tomcat.util.digester.EnvironmentPropertySource

Start from the file shipped with the same Tomcat version. Keep cargo.jvmargs for local-only settings such as a debugger agent, and memory settings for the container.

Cleanup and trimming with Ant

<plugin>
    <groupId>org.apache.maven.plugins</groupId>
    <artifactId>maven-antrun-plugin</artifactId>
    <executions>
        <execution>
            <id>clean-tomcat</id>
            <phase>prepare-package</phase>
            <goals><goal>run</goal></goals>
            <configuration>
                <target>
                    <delete dir="${project.build.directory}/tomcat"/>
                    <delete dir="${project.build.directory}/cargo/configurations"/>
                </target>
            </configuration>
        </execution>
        <execution>
            <id>trim-tomcat</id>
            <phase>package</phase>
            <goals><goal>run</goal></goals>
            <configuration>
                <target>
                    <delete includeemptydirs="true">
                        <fileset dir="${project.build.directory}/tomcat">
                            <include name="webapps/manager/**"/>
                            <include name="webapps/host-manager/**"/>
                            <include name="bin/*.bat"/>
                        </fileset>
                    </delete>
                </target>
            </configuration>
        </execution>
    </executions>
</plugin>

The first execution deletes the previous output, so every package starts from a clean server even without clean. It keeps the Tomcat that Cargo downloaded, so nothing is downloaded again. The second execution removes the administration applications and the Windows scripts. Declare the Ant plugin after Cargo in the POM, because Maven runs plugins of the same phase in declaration order.

Integration tests on the real server

Cargo can start and stop the server around Maven’s integration test phase:

<execution>
    <id>start-tomcat-it</id>
    <phase>pre-integration-test</phase>
    <goals><goal>start</goal></goals>
    <configuration>
        <configuration>
            <home>${project.build.directory}/cargo/it</home>
            <properties>
                <cargo.servlet.port>18080</cargo.servlet.port>
            </properties>
        </configuration>
    </configuration>
</execution>
<execution>
    <id>stop-tomcat-it</id>
    <phase>post-integration-test</phase>
    <goals><goal>stop</goal></goals>
    <configuration>
        <configuration>
            <home>${project.build.directory}/cargo/it</home>
        </configuration>
    </configuration>
</execution>

With the Failsafe plugin running the tests in between, mvn verify builds the server, starts it, runs HTTP tests and stops it. The tests run on the same Tomcat and overlay that go to production. The separate home and port avoid conflicts with a local server.

The container

The container follows the third part, with target/tomcat instead of the TomEE distribution. The entrypoint ends with:

exec "${CATALINA_HOME}/bin/catalina.sh" run

catalina.sh run already replaces itself with the Java process, so the stop signal reaches Tomcat. Starting org.apache.catalina.startup.Bootstrap by hand would require a copy of every flag catalina.sh sets, kept in sync with each Tomcat release.

Comparison with the TomEE plugin

TomEE pluginThis setup
Downloads a pinned serverCargo, with the server as a Maven artifact
Applies an overlay folderCargo’s files, pointing at the overlay folder
Adds libraries to the serverCargo’s container dependencies
Removes libraries by prefixAnt delete after Cargo
Runs the server from Mavenmvn package cargo:run
Builds a runnable distributionmvn package, into target/tomcat
System properties everywhereconf/catalina.properties in the overlay

The cost is two plugins instead of one, an ordering rule between them, and Cargo behaviours that need a comment where they are used.