Skip to main content

GitLab CI Pipeline - Containerize and Deploy a Javen Maven Web Application as WAR Package; Container Image Naming Convention

1531 words·
GitLab GitLab CI CI Pipeline GitLab Pages Java Maven WAR Multistage Dockerfile Unprivileged Container
Table of Contents
GitHub Repository Available

Overview
#

My GitLab Setup
#

In this tutorial I’m using the following setup based on Ubuntu 22.04 servers, GitLab is containerized:

192.168.70.4 # GitLab Server
192.168.70.6 # Deployment Server, with Docker platform installed

Container Image Name
#

The build container image name in the GitLab CI pipeline follows this naming convention: $CI_REGISTRY_IMAGE/$CI_COMMIT_REF_SLUG:$CI_COMMIT_SHA

  • $CI_REGISTRY_IMAGE Points to the container registry for the project.

  • $CI_COMMIT_REF_SLUG Represents the branch or tag.

  • $CI_COMMIT_SHA Ensures every image is uniquely tied to a specific commit for traceability and reproducibility.


In my project named “simple-java-maven-web-project”, the final image name looks like this:

# Container image name
gitlab-registry.jklug.work/java/simple-java-maven-web-project/main:aefb34cb0913963e4ea76968ab7d528fe2ba274a
  • Registry: gitlab-registry.jklug.work/java

  • Project: simple-java-maven-web-project

  • Branch: main

  • Commit: aefb34cb0913963e4ea76968ab7d528fe2ba274a



Java Maven Web Application
#

Create Project
#

# Create a new Maven project: Maven webapp structure
mvn archetype:generate \
-DgroupId=work.jklug \
-DartifactId=Maven-Web-App \
-DarchetypeArtifactId=maven-archetype-webapp \
-DarchetypeVersion=1.5 \
-DinteractiveMode=false
  • -DgroupId Unique identifier for the project, typically your domain name in reverse.

  • -DartifactId=Maven-Web-App The name of your project / project folder name.

  • -DarchetypeArtifactId Specifies the type of project template.

  • -DarchetypeVersion The version of the web application template.

  • -DinteractiveMode=false Disables prompts, so everything runs automatically


Verify Project Structure
#

# Navigate into the project
cd Maven-Web-App
Maven-Web-App
├── pom.xml  # Maven configuration file
└── src
    └── main
        └── webapp
            ├── index.jsp  # The default website, written in JSP (JavaServer Pages)
            └── WEB-INF
                └── web.xml  # Deployment descriptor for the web application, defining servlets, filters, and mappings

Application Files
#

Website: index.jsp
#

Adapt the default website index.jsp to output the current date:

  • src/main/webapp/index.jsp

Original version:

<html>
<body>
<h2><%= "Hello World!" %></h2>
</body>
</html>

Adapted version:

<!-- Import the JSTL tag library for formatting -->
<%@ taglib prefix="fmt" uri="http://java.sun.com/jsp/jstl/fmt"%>
<!-- Import the Java Date class -->
<%@ page import="java.util.Date" %>
<!-- Set the current date as a request attribute -->
<% request.setAttribute("today", new java.util.Date()); %>

<html>
<body>
    <!-- Format and display the current date -->
    <h2>Date: <fmt:formatDate value="${today}" pattern="yyyy-MM-dd" /></h2>
</body>
</html>

Dependencies: pom.xml
#

Add the “JSTL Dependency”:

  • pom.xml
<?xml version="1.0" encoding="UTF-8"?>

<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
  <modelVersion>4.0.0</modelVersion>

  <groupId>work.jklug</groupId>
  <artifactId>Maven-Web-App</artifactId>
  <version>1.0-SNAPSHOT</version>
  <packaging>war</packaging>

  <name>Maven-Web-App Maven Webapp</name>
  <!-- FIXME change it to the project's website -->
  <url>http://www.example.com</url>

  <properties>
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    <maven.compiler.source>8</maven.compiler.source>
    <maven.compiler.target>8</maven.compiler.target>
  </properties>

  <dependencies>
    <dependency>
      <groupId>junit</groupId>
      <artifactId>junit</artifactId>
      <version>4.13.1</version>
      <scope>test</scope>
    </dependency>

    <!-- JSTL Dependency -->
    <dependency>
      <groupId>javax.servlet</groupId>
      <artifactId>jstl</artifactId>
      <version>1.2</version>
    </dependency>
  </dependencies>

  <build>
    <finalName>Maven-Web-App</finalName>
    <pluginManagement><!-- lock down plugins versions to avoid using Maven defaults (may be moved to parent pom) -->
      <plugins>
        <plugin>
          <artifactId>maven-clean-plugin</artifactId>
          <version>3.4.0</version>
        </plugin>
        <!-- see http://maven.apache.org/ref/current/maven-core/default-bindings.html#Plugin_bindings_for_war_packaging -->
        <plugin>
          <artifactId>maven-resources-plugin</artifactId>
          <version>3.3.1</version>
        </plugin>
        <plugin>
          <artifactId>maven-compiler-plugin</artifactId>
          <version>3.13.0</version>
        </plugin>
        <plugin>
          <artifactId>maven-surefire-plugin</artifactId>
          <version>3.3.0</version>
        </plugin>
        <plugin>
          <artifactId>maven-war-plugin</artifactId>
          <version>3.4.0</version>
        </plugin>
        <plugin>
          <artifactId>maven-install-plugin</artifactId>
          <version>3.1.2</version>
        </plugin>
        <plugin>
          <artifactId>maven-deploy-plugin</artifactId>
          <version>3.1.2</version>
        </plugin>
      </plugins>
    </pluginManagement>
  </build>
</project>

Deployment Descriptor: web.xml
#

The automatically generated web.xml file looks like this:

  • src/main/webapp/WEB-INF/web.xml
<?xml version="1.0" encoding="UTF-8"?>
<web-app
    version="4.0"
    xmlns="http://xmlns.jcp.org/xml/ns/javaee"
    xmlns:javaee="http://xmlns.jcp.org/xml/ns/javaee"
    xmlns:xml="http://www.w3.org/XML/1998/namespace"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_4_0.xsd">
  <display-name>Archetype Created Web Application</display-name>
</web-app>



Local Build
#

Package (WAR) the Maven Web App
#

Optional, package the application into a WAR package:

# Compile the source code & create WAR package
mvn clean package

# Shell output:
[INFO] Building war: /home/ubuntu/Java-Projects/Maven-Web-App/target/Maven-Web-App.war
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time:  2.411 s
[INFO] Finished at: 2024-12-03T15:55:51Z
[INFO] ------------------------------------------------------------------------
  • clean Deletes old build artifacts

Dockerfile
#

  • Dockerfile
### Stage 1: Build Stage

# Use a lightweight JRE image as the base image / define as build stage
FROM maven:3.9.4-eclipse-temurin-17 AS builder

# Set the working directory inside the build stage
WORKDIR /app

# Copy the Maven project files into the container
COPY pom.xml .
COPY src ./src

# Run Maven to compile the project and package it as a WAR file
RUN mvn clean package



### Stage 2: Runtime Stage

# Use Apache Tomcat container
FROM tomcat:9.0.78-jdk17

# Create a non-root user and group
RUN groupadd -r tomcat && useradd -r -g tomcat tomcat

# Set the working directory inside the runtime stage
WORKDIR /usr/local/tomcat/webapps

# Copy the WAR file from the builder stage into the Tomcat webapps directory
COPY --from=builder /app/target/Maven-Web-App.war ./Maven-Web-App.war

# Set the appropriate permissions for the Tomcat user
RUN chown -R tomcat:tomcat /usr/local/tomcat

# Switch to the non-root user
USER tomcat

# Expose Tomcat's default port
EXPOSE 8080

# Start Tomcat when the container launches
ENTRYPOINT ["catalina.sh", "run"]

Build Image & Run Container
#

# Build the image
docker build -t maven-web-app .

# Run the container
docker run -d -p 8080:8080 maven-web-app

Test Maven Web Application
#

# Access the web application
curl localhost:8080/Maven-Web-App/

# Shell output:
<!-- Import the JSTL tag library for formatting -->

<!-- Import the Java Date class -->

<!-- Set the current date as a request attribute -->


<html>
<body>
    <!-- Format and display the current date -->
    <h2>Date: Tue Dec 03 17:18:54 UTC 2024</h2>
</body>
</html>



GitLab Repository
#

File and Folder Structure
#

The file and folder structure of the GitLab repository looks like this:

Maven-Web-App
├── Dockerfile  # Dockerfile
├── .gitlab-ci.yml  # CI Pipeline manifest
├── Maven-Web-App  # Simple Java Maven Webapp
│   ├── .mvn
│   │   ├── jvm.config
│   │   └── maven.config
│   ├── pom.xml  # Maven configuration file
│   └── src
│       └── main
│           └── webapp
│               ├── index.jsp  # The default website, written in JSP (JavaServer Pages)
│               └── WEB-INF
│                   └── web.xml  # Deployment descriptor for the web application, defining servlets, filters, and mappings
└── README.md

CI Pipeline Manifest
#

  • .gitlab-ci.yml
### Variables
variables:
  DEPLOY_IP: "192.168.70.6"  # Deployment server IP
  DEPLOY_USER: "gitlab-deployment"  # Deployment server SSH user
  DEPLOY_PORT_HOST: 8080  # Host port
  DEPLOY_PORT_CONT: 8080  # Container port
  CONTAINER_NAME: "simple-javen-maven-web-project"  # Name of the deployed container

  # Define the image name, tagging it with the GitLab CI registry and the current commit SHA
  APACHE_IMAGE_SHA: $CI_REGISTRY_IMAGE/$CI_COMMIT_REF_SLUG:$CI_COMMIT_SHA

### Stages
stages:
  - build
  - deploy


### Build Container Image
build_image:
  image: docker:stable
  stage: build
  services:
    - docker:dind
  variables:
    DOCKER_TLS_CERTDIR: ""
  before_script:
    # Login to GitLab Container Registry using predefined CI/CD variables
    - docker login -u "$CI_REGISTRY_USER" -p "$CI_REGISTRY_PASSWORD" $CI_REGISTRY
  script:
    # Build the Docker image from the Dockerfile in the current directory
    - docker build --pull -t $APACHE_IMAGE_SHA .
    # Push the built Docker image to the GitLab Container Registry
    - docker push $APACHE_IMAGE_SHA
  rules: 
    # Rule: Run this job only for main branch and if the Dockerfile exists
    - if: $CI_COMMIT_BRANCH == "main"
      exists:
        - Dockerfile


### Deploy Container to Virtual Machine
deploy_container:
  stage: deploy
  image: alpine:latest
  needs:
    # Run this job only if the 'build_image' job succeeds
    - build_image
  before_script:
    # Update the package index, install the OpenSSH client for SSH connections
    - apk update && apk add openssh-client
    # If the private SSH key file ($ID_RSA) exists, set secure permissions (read/write for the owner only)
    - if [ -f "$ID_RSA" ]; then chmod og= $ID_RSA; fi
  script:
    #  SSH into the deployment server, log in to the GitLab Container registry
    - ssh -i $ID_RSA -o StrictHostKeyChecking=no $DEPLOY_USER@$DEPLOY_IP "docker login -u gitlab-ci-token -p $CI_JOB_TOKEN $CI_REGISTRY"
     # SSH into the deployment server, pull the image from the registry
    - ssh -i $ID_RSA -o StrictHostKeyChecking=no $DEPLOY_USER@$DEPLOY_IP "docker pull $APACHE_IMAGE_SHA"
    # SSH into the deployment server, remove the existing container (if it exists)
    - ssh -i $ID_RSA -o StrictHostKeyChecking=no $DEPLOY_USER@$DEPLOY_IP "docker container rm -f $CONTAINER_NAME || true"
    # SSH into the deployment server, run the new container
    - ssh -i $ID_RSA -o StrictHostKeyChecking=no $DEPLOY_USER@$DEPLOY_IP "docker run -d -p $DEPLOY_PORT_HOST:$DEPLOY_PORT_CONT --restart=unless-stopped --name $CONTAINER_NAME $APACHE_IMAGE_SHA"
  rules:
    # Rule: Run this job only for main branch
    - if: $CI_COMMIT_BRANCH == "main"

Dockerfile
#

  • Dockerfile
### Stage 1: Build Stage

# Use a lightweight JRE image as the base image / define as build stage
FROM maven:3.9.4-eclipse-temurin-17 AS builder

# Set the working directory inside the build stage
WORKDIR /app

# Copy the Maven project files into the container
COPY Maven-Web-App/pom.xml .
COPY Maven-Web-App/src ./src

# Run Maven to compile the project and package it as a WAR file
RUN mvn clean package



### Stage 2: Runtime Stage

# Use Apache Tomcat container
FROM tomcat:9.0.78-jdk17

# Create a non-root user and group
RUN groupadd -r tomcat && useradd -r -g tomcat tomcat

# Set the working directory inside the runtime stage
WORKDIR /usr/local/tomcat/webapps

# Copy the WAR file from the builder stage into the Tomcat webapps directory
COPY --from=builder /app/target/Maven-Web-App.war ./Maven-Web-App.war

# Set the appropriate permissions for the Tomcat user
RUN chown -R tomcat:tomcat /usr/local/tomcat

# Switch to the non-root user
USER tomcat

# Expose Tomcat's default port
EXPOSE 8080

# Start Tomcat when the container launches
ENTRYPOINT ["catalina.sh", "run"]



Verify Deployment
#

Verify the Container
#

# List containers
docker ps

# Shell output:
CONTAINER ID   IMAGE                                                                                                         COMMAND             CREATED              STATUS              PORTS                                       NAMES
accb0451da82   gitlab-registry.jklug.work/java/simple-java-maven-web-project/main:aefb34cb0913963e4ea76968ab7d528fe2ba274a   "catalina.sh run"   About a minute ago   Up About a minute   0.0.0.0:8080->8080/tcp, :::8080->8080/tcp   simple-javen-maven-web-project

Verify the Container runs Unprivileged
#

# Access the container terminal
docker exec -it accb0451da82 sh

# List information of current user
id

# Shell output:
uid=999(tomcat) gid=999(tomcat) groups=999(tomcat)

Verify Java Maven Application
#

# Access the web application
curl localhost:8080/Maven-Web-App/

# Shell output:
<!-- Import the JSTL tag library for formatting -->

<!-- Import the Java Date class -->

<!-- Set the current date as a request attribute -->


<html>
<body>
    <!-- Format and display the current date -->
    <h2>Date: Tue Dec 03 17:50:54 UTC 2024</h2>
</body>
</html>



Links #

jueklu/gitlab-ci-simple-java-maven-web-project

GitLab CI pipeline that containerizes and deploys a Java Maven Web application.

Dockerfile
0
0