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 #
GitLab CI pipeline that containerizes and deploys a Java Maven Web application.