Gitlab Runner and Maven – Guide [With the efficient cache method]

Bits Lovers
Written by Bits Lovers on
Gitlab Runner and Maven – Guide [With the efficient cache method]

If you are building Java applications, you need Gitlab Runner and Maven in your CI/CD pipeline. This post walks through everything required to get your Java project building on Gitlab, from the basic setup to a caching strategy that actually scales.

Gitlab is a Git repository manager with solid CI/CD integration. Unlike Jenkins, you don’t have to manage a dozen plugins just to get a build running. And because the pipeline config lives right next to your code, you can see everything in one place.

Gitlab also lets you set up multiple Runners, which are the machines that actually execute your builds. The builds don’t run on the Gitlab server itself.

Every build runs inside a Docker container. That means you don’t install Java, Maven, or anything else directly on the Runner. The container has what it needs, and it gets destroyed when the job finishes.

There are two files you need to care about:

  • .gitlab-ci.yml
  • settings.xml

The .gitlab-ci.yml File

What does it do?

The .gitlab-ci.yml file is where you define your entire pipeline. It sits in the root of your repository.

It is a YAML file, so it is easy to read. You specify stages, then list the commands for each stage. If you already know which Maven commands you run locally to build your project, translating that into a Gitlab pipeline is straightforward.

A first attempt

To build a Java application with Maven, you probably run something like this on your machine:

mvn package -U

or

mvn deploy

So you might write a pipeline like this:

build:
  stage: build
  script:
    - mvn package -U

Here is what happens when that runs:

Running with gitlab-runner 17.x
  on gitlab-runner-maven B-Scbyv_
Preparing the "docker" executor
00:03
Using Docker executor with image alpine ...
Using locally found image version due to "if-not-present" pull policy
Using docker image sha256:d4ff818577bc193b309b355b02ebc9220427090057b54a59e73b79bdfe139b83 for alpine with digest alpine@sha256:adab3844f497ab9171f070d4cae4114b5aec565ac772e2f2579405b78be67c96 ...
Preparing environment
00:00
Running on runner-n-ubbyq-project-546-concurrent-0 via ip-192-168-15-25...
Getting source from Git repository
00:02
Fetching <a href="/git-unstaged-changes/">changes with git</a> depth set to 50...
Reinitialized existing Git repository in /builds/gitlab/runnner/maven/docker/.git
Checking out be7dafc1 as temp2...
Executing "step_script" stage of the job script
00:01
Using docker image sha256:d4ff818577bc193b309b355b02ebc9220427090057b54a59e73b79bdfe139b83 for alpine with digest alpine@sha256:adab3844f497ab9171f070d4cae4114b5aec565ac772e2f2579405b78be67c96 ...
$ mvn package -U
/bin/sh: eval: line 104: mvn: not found
Cleaning up file based variables
00:01
ERROR: Job failed: exit code 127

The problem: mvn not found

The error /bin/sh: eval: line 104: mvn: not found tells you exactly what went wrong. The default Docker image is alpine, which is a minimal Linux image. It does not have Maven installed.

Since builds run inside Docker containers, you just need to pick an image that has Maven. You specify the image in your .gitlab-ci.yml.

Picking the right Maven Docker image

Maven publishes official Docker images on Docker Hub. You can browse the tags to find the version you need.

Gitlab Runner Maven - How to build Java application using the CI from Gitlab.

One thing to keep in mind: Maven 3.8.1 and later block HTTP repository URLs by default. You must use HTTPS. If you run into certificate issues with HTTPS and need a quick workaround, you can pin an older Maven version and use HTTP. But in production, you should fix the certificate and use HTTPS.

The current recommended images use Eclipse Temurin as the JDK base. For example:

  • maven:3.9-eclipse-temurin-17 (JDK 17 LTS)
  • maven:3.9-eclipse-temurin-21 (JDK 21 LTS)

If your project still targets Java 8, use maven:3.9-eclipse-temurin-8. The older maven:3.6-jdk-8 tag still works but ships an outdated Maven version.

A working pipeline

Here is a .gitlab-ci.yml that actually builds your project:

image: maven:3.9-eclipse-temurin-17
variables:
  MAVEN_CLI_OPTS: "-s .m2/settings.xml --batch-mode"
  MAVEN_OPTS: "-Dmaven.repo.local=.m2/repository -Dmaven.artifact.threads=50"
cache:
  paths:
    - .m2/repository/
build:
  stage: build
  script:
    - mvn clean $MAVEN_CLI_OPTS $MAVEN_OPTS deploy -DskipTests

The image line tells the Runner which Docker image to pull. The Gitlab variables MAVEN_CLI_OPTS and MAVEN_OPTS keep the script line clean and make it easy to tweak settings without touching the command itself.

The cache section tells Gitlab to save the .m2/repository/ directory between runs. On the next build, Maven can resolve dependencies from the cache instead of downloading everything again.

When you need a custom image

Sometimes the official Maven image is not enough. If your project has a frontend built with Node.js, you need both Maven and npm in the same container. In that case, you can build a custom Docker image based on the official Maven image, install Node.js on top of it, and push it to a registry like Docker Hub or AWS ECR.

Just make sure the Runner has network access to whichever registry you use. You can read more about that in the post about how to build a Docker image and push it to ECR.

The settings.xml file

Maven needs a settings.xml file to know where to find your repositories and how to authenticate. Here is a minimal example:

<settings>
    <profiles>
        <profile>
            <id>default-profile</id>
            <activation>
                <activeByDefault>true</activeByDefault>
            </activation>
            <properties></properties>
            <repositories>
                <repository>
                    <id>release-repo</id>
                    <url>https://your-maven-repo.example.com/repository/maven-releases/</url>
                    <releases>
                        <enabled>true</enabled>
                    </releases>
                </repository>
            </repositories>
            <pluginRepositories>
                <pluginRepository>
                    <id>release-repo</id>
                    <url>https://your-maven-repo.example.com/repository/maven-releases/</url>
                    <releases>
                        <enabled>true</enabled>
                    </releases>
                    <snapshots>
                        <enabled>true</enabled>
                    </snapshots>
                </pluginRepository>
            </pluginRepositories>
        </profile>
    </profiles>
    <servers>
        <server>
            <id>release-repo</id>
            <username>${env.REPO_USER}</username>
            <password>${env.REPO_PASS}</password>
        </server>
    </servers>
</settings>

Notice the <url> entries use HTTPS. Since Maven 3.8.1, HTTP URLs are blocked by default. If you are still using HTTP, Maven will refuse to connect.

Also notice the <username> and <password> fields reference environment variables (${env.REPO_USER} and ${env.REPO_PASS}) instead of hardcoded values. You define those variables in Gitlab under Settings > CI/CD > Variables. This way the credentials never appear in your source code.

If you are using the GitLab Package Registry, you can skip the username and password entirely. GitLab provides a built-in token called CI_JOB_TOKEN that authenticates automatically. Your repository URL would look like this:

<url>${env.CI_API_V4_URL}/projects/${env.CI_PROJECT_ID}/packages/maven</url>

Why the per-project approach has problems

The setup above works fine for a single project. But if you manage dozens of repositories, it breaks down in a few ways:

1. Security – Storing credentials in settings.xml inside the repo means anyone with read access can see them. Even if you use Gitlab CI/CD Variables, you have to configure them for each project.

2. Maintenance – If your repository URL changes or you rotate credentials, you have to update every project individually. That is tedious and error-prone.

3. Cache is inefficient – Each project caches its own .m2/repository/. If ten projects depend on the same libraries, you download them ten times. The cache is not shared.

A better approach: centralize on the Runner

There is a cleaner way to handle all of this. Instead of putting settings.xml and the cache inside each project, you put them on the Runner itself and share them with every build container using Docker volumes.

Here is how it works.

When Gitlab receives a pipeline job, it contacts the Runner and asks it to spin up a Docker container. The Runner pulls the image, mounts any volumes you configured, runs the build, and destroys the container. The volumes persist across builds.

If you are using Fargate Runner, Docker volumes are not available. You need to use an S3 bucket for cache instead.

Configuring Docker volumes on the Runner

You set up the shared volume when you register the Runner. The --docker-volumes flag tells the Runner to mount a host directory into every build container:

gitlab-runner register \
  --name gitlab-runner-maven \
  --executor docker \
  --docker-image maven:3.9-eclipse-temurin-17 \
  --docker-volumes "/opt/maven/.m2:/root/.m2"

This mounts /opt/maven/.m2 from the Runner host into /root/.m2 inside the container. You place your settings.xml and the local Maven repository on that host path.

Since developers typically don’t have SSH access to the Runner, storing the settings.xml with real credentials on the Runner is reasonably secure. It is certainly better than committing them to the repository.

With this setup, your .gitlab-ci.yml becomes much simpler:

image: maven:3.9-eclipse-temurin-17
build:
  stage: build
  script:
    - mvn package -U -DskipTests

No settings.xml in the repo, no cache configuration, no credentials in source control. The Runner handles all of that.

Note on the new runner registration workflow

GitLab introduced a new runner registration system starting in GitLab 16.0. The old gitlab-runner register command with a registration token is being deprecated in favor of runner authentication tokens. If you are setting up a new Runner, check the GitLab documentation for the current registration method for your version.

Multi-stage pipeline with Maven on Gitlab

You can split your pipeline into multiple stages. For example, you might want to check code style before building. If your tests require a database or other backing service, see GitLab CI Services for how to spin up containers alongside your jobs.

image: maven:3.9-eclipse-temurin-17
stages:
  - lint
  - build

lint:
  stage: lint
  script:
    - mvn checkstyle:check -Dcheckstyle.config.location=checkstyle-rules.xml

build:
  stage: build
  script:
    - mvn package -U -DskipTests

Using mvn checkstyle:check is generally better than running a standalone JAR, since it integrates with your existing POM configuration.

Conclusion

You now have a working Gitlab CI/CD pipeline for Java projects using Maven. You know how to pick the right Docker image, configure settings.xml with environment variables instead of hardcoded credentials, and set up shared caching on the Runner using Docker volumes.

The key takeaway: for small projects, the per-project cache approach in .gitlab-ci.yml is fine. For anything larger, centralize your settings.xml and Maven cache on the Runner with Docker volumes. It is more secure and it is faster because all your projects share the same dependency cache.

If you have more than one Runner and need to route specific projects to specific Runners, use Gitlab Tags to select the right one.

The Gitlab CI/CD documentation and the Maven documentation are good references to keep bookmarked.

GitLab CI Artifacts — pass files between pipeline jobs and store build outputs.

GitLab CI Parallel Matrix for Monorepos — run Maven builds across multiple modules in parallel using matrix builds.

Setup GitLab CI to authenticate with AWS ECR without long-lived credentials.

Pipeline to build Docker in Docker on Gitlab.

How to execute Cloud Formation on Gitlab.

How to execute Terraform on Gitlab.

How delete a project on Gitlab.

Bits Lovers

Bits Lovers

Professional writer and blogger. Focus on Cloud Computing.

Comments

comments powered by Disqus