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-app
Java 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/null
command 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.zip
file 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.