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
Simple Javen Maven Project #
- 
The HelloMaven-appJava Maven application is a basic HelloWorld example that utilizes the Joda-Time library to display the current date and time.
- 
To ensure the container remains running after the application completes, the tail -f /dev/nullcommand has been added to the container’sENTRYPOINT:
# Container entrypoint
ENTRYPOINT ["sh", "-c", "java -jar app.jar && tail -f /dev/null"]
- The functionality of the application can be verified by inspecting the container logs:
# Verify the Javen Maven application
docker logs c505a764bce1
# Shell output:
Hello, from Maven. Time:
2024-12-02 16:18:29
- More details about this Javen Maven project can be found in my previous blog post “Java Programming Language”.
 
GitLab Repository #
File and Folder Structure #
GitLab-Repository
├── Dockerfile
├── .gitlab-ci.yml
├── HelloMaven-app  # Simple Java Maven Application
│   ├── .mvn
│   │   ├── jvm.config
│   │   └── maven.config
│   ├── pom.xml  # Maven configuration file
│   └── src
│       ├── main
│       │   └── java
│       │       └── com
│       │           └── mycompany
│       │               └── app
│       │                   └── App.java  # Main Java file
│       └── test
│           └── java
│               └── com
│                   └── mycompany
│                       └── app
│                           └── AppTest.java  # Test file
└── README.md
GitLab CI Pipeline #
- .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-project"  # Name of the deployed container
  # Set a custom Maven local repository path to avoid polluting the default location and enable caching
  MAVEN_OPTS: "-Dmaven.repo.local=/builds/$CI_PROJECT_NAMESPACE/$CI_PROJECT_NAME/.m2/repository"
### Stages
stages:
  - test
  - pages
  - build
  - deploy
### Test
test:
  image: maven:3.9.4-eclipse-temurin-17
  stage: test
  script:
    # Navigate to the HelloMaven-app directory
    - cd HelloMaven-app
    # Output a message to indicate the start of test execution
    - echo "Running Maven tests..."
    # Run the tests using Maven
    - mvn test
    # Generate an HTML test report using the Maven Surefire Report plugin
    - mvn surefire-report:report
  cache:
    paths:
      # Cache the Maven dependencies to speed up subsequent builds
      - .m2/repository
  artifacts:
    when: always  # Always upload artifacts, regardless of whether the job succeeds or fails
    paths:
      # Collect XML test results
      - HelloMaven-app/target/surefire-reports/*.xml
      # Collect the HTML test report
      - HelloMaven-app/target/site/surefire-report.html
    expire_in: 1 week
### Deploy Test Results to GitLab Pages
pages:
  image: maven:3.9.4-eclipse-temurin-17
  stage: pages
  needs:
    # Run this job only if the 'test' job succeeds
    - test
  script:
    # Ensure the public directory exists
    - mkdir -p public
    # Move the report to the 'public/' directory for GitLab Pages
    - mv HelloMaven-app/target/site/surefire-report.html public/index.html
    # Debugging: List the public directory contents
    - ls -l public
  artifacts:
    paths:
      # Specify the 'public/' directory to be published via GitLab Pages
      - public/
### 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 "$CI_REGISTRY_IMAGE:$CI_COMMIT_REF_SLUG" .
    # Push the built Docker image to the GitLab Container Registry
    - docker push "$CI_REGISTRY_IMAGE:$CI_COMMIT_REF_SLUG"
  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 $CI_REGISTRY_IMAGE:$CI_COMMIT_REF_SLUG"
    # 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 $CI_REGISTRY_IMAGE:$CI_COMMIT_REF_SLUG"
  rules:
    # Rule: Run this job only for main branch
    - if: $CI_COMMIT_BRANCH == "main"
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 build
# Set the working directory inside the build stage
WORKDIR /app
# Copy the Maven project files into the container
COPY HelloMaven-app/pom.xml ./
COPY HelloMaven-app/src ./src
# Build the application with dependencies included
RUN mvn clean compile assembly:single
### Stage 2: Runtime Stage
# Use a lightweight JRE image as the base image
FROM eclipse-temurin:17-jre-alpine
# Create a user & group
RUN addgroup -S app-group && adduser -S app-user -G app-group
# Set the working directory inside the runtime stage
WORKDIR /app
# Copy the built JAR file from the build stage to the runtime stage
COPY --from=build /app/target/HelloMaven-app-1.0-SNAPSHOT-jar-with-dependencies.jar app.jar
# Change ownership of the application files
RUN chown -R app-user:app-group /app
# Switch to the non-root user
USER app-user
# Expose port 80 for the web server
EXPOSE 80
# Command to run the application
ENTRYPOINT ["sh", "-c", "java -jar app.jar && tail -f /dev/null"]
 
Java Maven Application #
Dependencies & Plugins: pom.xml #
- HelloMaven-app/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>com.mycompany.app</groupId>
  <artifactId>HelloMaven-app</artifactId>
  <version>1.0-SNAPSHOT</version>
  <name>HelloMaven-app</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.release>17</maven.compiler.release>
  </properties>
  <dependencyManagement>
    <dependencies>
      <dependency>
        <groupId>org.junit</groupId>
        <artifactId>junit-bom</artifactId>
        <version>5.11.0</version>
        <type>pom</type>
        <scope>import</scope>
      </dependency>
    </dependencies>
  </dependencyManagement>
  <dependencies>
    <dependency>
      <groupId>org.junit.jupiter</groupId>
      <artifactId>junit-jupiter-api</artifactId>
      <scope>test</scope>
    </dependency>
    <!-- Optionally: parameterized tests support -->
    <dependency>
      <groupId>org.junit.jupiter</groupId>
      <artifactId>junit-jupiter-params</artifactId>
      <scope>test</scope>
    </dependency>
    <!-- Joda-Time dependency -->
    <dependency>
      <groupId>joda-time</groupId>
      <artifactId>joda-time</artifactId>
      <version>2.10.14</version>
    </dependency>
    <!-- Junit dependency -->
    <dependency>
      <groupId>junit</groupId>
      <artifactId>junit</artifactId>
      <version>4.12</version>
      <scope>test</scope>
    </dependency>
  </dependencies>
  <build>
    <pluginManagement><!-- lock down plugins versions to avoid using Maven defaults (may be moved to parent pom) -->
      <plugins>
        <!-- clean lifecycle, see https://maven.apache.org/ref/current/maven-core/lifecycles.html#clean_Lifecycle -->
        <plugin>
          <artifactId>maven-clean-plugin</artifactId>
          <version>3.4.0</version>
        </plugin>
        <!-- default lifecycle, jar packaging: see https://maven.apache.org/ref/current/maven-core/default-bindings.html#Plugin_bindings_for_jar_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-jar-plugin</artifactId>
          <version>3.4.2</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>
        <!-- site lifecycle, see https://maven.apache.org/ref/current/maven-core/lifecycles.html#site_Lifecycle -->
        <plugin>
          <artifactId>maven-site-plugin</artifactId>
          <version>3.12.1</version>
        </plugin>
        <plugin>
          <artifactId>maven-project-info-reports-plugin</artifactId>
          <version>3.6.1</version>
        </plugin>
        <!-- Add maven-assembly-plugin -->
        <plugin>
          <artifactId>maven-assembly-plugin</artifactId>
          <configuration>
            <archive>
              <manifest>
                <mainClass>com.mycompany.app.App</mainClass>
              </manifest>
            </archive>
            <descriptorRefs>
              <descriptorRef>jar-with-dependencies</descriptorRef>
            </descriptorRefs>
          </configuration>
        </plugin>
        <!-- Add maven-surefire-report-plugin -->
        <plugin>
            <groupId>org.apache.maven.plugins</groupId>
            <artifactId>maven-surefire-report-plugin</artifactId>
            <version>3.1.0</version>
        </plugin>
      </plugins>
    </pluginManagement>
  </build>
</project>
Java Application: App.java #
- HelloMaven-app/src/main/java/com/mycompany/app/App.java
// Define the package name for this class: A package is a namespace that organizes related classes and interfaces
package com.mycompany.app;
// Import the DateTime class from the Joda-Time library
import org.joda.time.DateTime;
// Define a public class named "App"
public class App {
    // Print some text
    public static void main(String[] args) {
        System.out.println("Hello, from Maven. Time:");
        // Use Joda-Time to get and print the current date and time
        DateTime currentTime = new DateTime();
        System.out.println(currentTime.toString("yyyy-MM-dd HH:mm:ss"));
    }
}
Unit Test: AppTest.java #
- HelloMaven-app/src/test/java/com/mycompany/app/AppTest.java
package com.mycompany.app;
import static org.junit.Assert.assertTrue;
import org.joda.time.DateTime;
import org.junit.Test;
import java.io.ByteArrayOutputStream;
import java.io.PrintStream;
public class AppTest {
    @Test
    public void testMainOutput() {
        // Capture the console output
        ByteArrayOutputStream outContent = new ByteArrayOutputStream();
        System.setOut(new PrintStream(outContent));
        // Run the main method
        App.main(new String[]{});
        // Verify the output contains the greeting
        String output = outContent.toString();
        assertTrue("Output should contain the greeting", output.contains("Hello, from Maven. Time:"));
        // Verify the output contains a correctly formatted timestamp
        DateTime now = new DateTime();
        String expectedDate = now.toString("yyyy-MM-dd");
        assertTrue("Output should contain the current date", output.contains(expectedDate));
        // Reset the console output
        System.setOut(System.out);
    }
}
 
Verify the Deployment #
Verify the Container #
# List Docker containers
docker ps
# Shell output:
CONTAINER ID   IMAGE                                                            COMMAND                  CREATED          STATUS          PORTS                                               NAMES
8ed8d440f97b   gitlab-registry.jklug.work/java/simple-java-maven-project:main   "sh -c 'java -jar ap…"   32 seconds ago   Up 31 seconds   80/tcp, 0.0.0.0:8080->8080/tcp, :::8080->8080/tcp   simple-javen-maven-project
Verify Javen Maven Application #
# Verify the Javen Maven application
docker logs c505a764bce1
# Shell output:
Hello, from Maven. Time:
2024-12-02 16:18:29
Verify Test Stage Artifacts #
- 
Go to: (Project) “Build” > “Artifacts” 
- 
Download the artifacts.zipfile from the “test” job
The artifacts.zip file includes both the HTML report surefire-report.html and the XML file TEST-com.mycompany.app.AppTest.xml:
artifacts.zip
└── HelloMaven-app
    └── target
        ├── site
        │   └── surefire-report.html
        └── surefire-reports
            └── TEST-com.mycompany.app.AppTest.xml
 
Links #
GitLab CI pipeline that containerizes and deploys a Java Maven application. Integration test results are published via GitLab Pages.