Skip to main content

GitLab + Jenkins

These are production operational guidelines for a pipeline configuration using GitLab + Jenkins. The content pairs with the GitHub Actions version (GitHub Actions); the CI/CD concepts (branch strategy, quality gates, artifact distribution) are the same.

Branch Strategy​

  • main: Release-ready state. All changes are merged into main through Merge Requests

In this guide, CI is operated exclusively for main.

Pipeline Separation​

  • CI (Merge Request / push)
    • Dependency resolution
    • Validation (XML, formatting, etc.)
    • Build
    • Artifact retention
  • Release (tag)
    • Rebuild equivalent to CI
    • Attach the zip to the GitLab Release

Quality Gates​

  • Under Settings > Merge requests > Merge checks, enable "Pipelines must succeed" so that Merge Requests whose Jenkins build result is not successful cannot be merged
  • Use the Jenkins GitLab Plugin to write the commit status back to GitLab, making the Jenkins pipeline result appear as a status check on the Merge Request (verify on the actual GitLab version/edition you will use at adoption time that an external status registered via the Commit Status API is taken into account by "Pipelines must succeed")
  • Use Protected branches to forbid direct pushes to main, permitting only Merge Requests
  • Configure approval rules (at least one approval), and use the Code Owners feature as well if possible

Preparation​

Local Development Environment​

  • Bun 1.3.14 or later
  • JDK 17 recommended
  • Maven 3.9.14 or later
  • Git / VSCode
  • Commit the bun.lock generated after running bun install to the repository (CI uses bun install --frozen-lockfile, so it fails if the lockfile is missing or out of date)

Setting Up the GitLab + Jenkins Platform (docker-compose)​

This is a procedure for building GitLab CE and Jenkins with a single docker-compose, aimed at small to medium teams. If your scale requires multiple nodes, external data stores, or an HA configuration, follow GitLab's official Reference Architecture.

Place at: any directory on the production host (e.g., /opt/gitlab-jenkins/)

services:
gitlab:
image: gitlab/gitlab-ce:latest
container_name: gitlab
hostname: gitlab.example.com
restart: unless-stopped
shm_size: '256m'
environment:
GITLAB_OMNIBUS_CONFIG: |
external_url 'https://gitlab.example.com'
gitlab_rails['gitlab_shell_ssh_port'] = 2224
letsencrypt['enable'] = true
letsencrypt['contact_emails'] = ['ops@example.com']
ports:
- "443:443"
- "80:80"
- "2224:22"
volumes:
- gitlab_config:/etc/gitlab
- gitlab_logs:/var/log/gitlab
- gitlab_data:/var/opt/gitlab
deploy:
resources:
limits:
memory: 8g

jenkins:
build:
context: ./jenkins
container_name: jenkins
restart: unless-stopped
ports:
- "127.0.0.1:8080:8080"
volumes:
- jenkins_home:/var/jenkins_home
deploy:
resources:
limits:
memory: 4g

volumes:
gitlab_config:
gitlab_logs:
gitlab_data:
jenkins_home:

Key points for production settings:

  • Specify the real domain with https:// for external_url. Either obtain certificates automatically with the letsencrypt settings built into GitLab Omnibus, or, if you terminate TLS at an external reverse proxy/load balancer, separately adjust omnibus settings such as nginx['listen_port']
  • Do not skip the Jenkins setup wizard. An init script that specifies -Djenkins.install.runSetupWizard=false in JAVA_OPTS to auto-create admin/admin is for evaluation only and must not be used in production (it is replaced by SSO integration in the "Setting Up the Jenkins Environment" section below)
  • Do not expose the Jenkins port on all host interfaces; limit it to 127.0.0.1:8080:8080 and expose it externally only through a reverse proxy
  • The 50000 (JNLP) port does not need to be exposed unless you add remote Agents (via inbound TCP). Open it, limited to the necessary range, at the point you add remote Agents with the Docker/Kubernetes Plugin
  • Raise the memory limits according to the expected number of users and build concurrency (as a rough guide, 8 GB or more for GitLab and 4 GB or more for Jenkins)
  • This configuration is a single node, so availability depends on one host. Together with regular backups (described below), confirm in advance whether the downtime requirements are acceptable
cd /opt/gitlab-jenkins
docker compose build jenkins # Build the custom Jenkins image (first time, and when the Dockerfile changes)
docker compose up -d # Start
docker compose stop # Stop (data is retained)

Because Jenkins needs build tools, add Maven/Bun to the base image in jenkins/Dockerfile.

FROM jenkins/jenkins:lts-jdk17

USER root

RUN apt-get update \
&& apt-get install -y --no-install-recommends maven \
&& rm -rf /var/lib/apt/lists/*

ENV BUN_INSTALL=/usr/local/bun
RUN curl -fsSL https://bun.sh/install | bash -s "bun-v1.3.14" \
&& ln -s /usr/local/bun/bin/bun /usr/local/bin/bun
ENV PATH="${BUN_INSTALL}/bin:${PATH}"

USER jenkins
  • Pin the base image tag to a known concrete version (e.g., jenkins/jenkins:2.492.1-lts-jdk17) rather than a moving tag such as lts-jdk17, to avoid unintended automatic updates
  • Maven is installed with apt-get install maven (if you need to pin a specific version, switch to extracting a tarball)
  • Bun uses the official install script with the version fixed by argument (align it with the tool versions on the docker-compose.yml side)
  • Run apt-get/curl as USER root and switch back to USER jenkins at the end (do not start up as root)

Initial Setup​

GitLab:

  1. After the container starts, obtain the initial root password (it is stored in /etc/gitlab/initial_root_password for 24 hours only)

    docker compose exec gitlab cat /etc/gitlab/initial_root_password
  2. After logging in, change the password immediately. If your organization uses SSO (SAML/OIDC/LDAP), configure identity integration under Admin Area > Settings > General > Sign-in restrictions and similar settings

Jenkins:

  1. Obtain the initial administrator password generated on first startup

    docker compose exec jenkins cat /var/jenkins_home/secrets/initialAdminPassword
  2. In the setup wizard, install the GitLab Plugin and the GitLab Branch Source Plugin in addition to the recommended plugins

  3. After creating the first administrator user, switch to a security realm integrated with your organization's SSO using the procedure in the "Setting Up the Jenkins Environment" section

Backups​

  • GitLab: Run docker compose exec gitlab gitlab-backup create on a schedule and move the output to external storage. Because /etc/gitlab/gitlab-secrets.json and /etc/gitlab/gitlab.rb are not covered by the backup, store them separately
  • Jenkins: Regularly back up the entire jenkins_home volume (job configuration, credentials, build history)

GitLab Repository Settings​

The following are settings on the repository side. If you use GitLab.com (SaaS), the docker-compose setup above is unnecessary and you can start from this section.

  1. Create the project (keep it Private. Do not make it Public/Internal unless there is a positive reason to publish it)
  2. Protect main under Settings > Repository > Protected branches (restrict "Allowed to push" to Maintainer or above, or to No one)
  3. Enable approval rules and "Pipelines must succeed" under Settings > Merge requests
  4. Issue a deploy key dedicated to CI and register the public key with Write permission under the project's Settings > Repository > Deploy keys (do not reuse personal SSH keys)
  5. Generate and note a Secret Token for the Jenkins → GitLab webhook (used under Settings > Webhooks)
  6. As an SSRF countermeasure, GitLab blocks webhooks addressed to private IPs/local networks by default. This usually does not apply if Jenkins is reachable by a legitimate hostname on the internal network, but if it does apply, permit only the minimum necessary range under Admin Area > Settings > Network > Outbound requests

Setting Up the Jenkins Environment​

  • Authentication: A fixed admin/admin account created by skipping the setup wizard is for evaluation only and must never be used in production. Configure a security realm integrated with your organization's LDAP/SAML/OIDC and assign minimal permissions with matrix-based security (or the Role-based Authorization Strategy)
  • Credentials: Register SSH private keys and API tokens under Manage Jenkins > Credentials, and if possible integrate with an external secret manager (HashiCorp Vault Plugin, etc.) through a Credentials Provider. Do not hard-code tokens or keys in the Jenkinsfile or in SCM
  • Agents: Do not run builds directly on the controller; separate them onto dedicated build Agents using the Docker Plugin, Kubernetes Plugin, or similar
  • Plugins: Install the GitLab Plugin, GitLab Branch Source Plugin, Pipeline: Multibranch, and Credentials Binding, and establish a policy for regular plugin updates
  • Backups: Regularly back up JENKINS_HOME (which includes job configuration, credentials, and build history)
  • Publication: Publish Jenkins over HTTPS with TLS terminated at a reverse proxy (nginx, etc.)

Detailed Procedure​

The Jenkinsfile created by this procedure goes directly under the root of the module project that has package.json / pom.xml / module.xml — not the documentation management repository.

Step 1: Preparation on the GitLab Side​

Items 1 through 5 of "GitLab Repository Settings" above must already be done.

Step 2: Preparation on the Jenkins Side​

  1. Register the deploy key's private key under Manage Jenkins > Credentials > System > Global credentials as SSH Username with private key (Username: git)
  2. Register a GitLab Project/Group Access Token (api scope) as Secret text (for writing back commit statuses and uploading packages)
  3. Associate the GitLab server URL with the API token above under Manage Jenkins > System > GitLab connection
  4. Create a Multibranch Pipeline job and specify GitLab Project as the Branch Source. Set the webhook Secret Token and let it auto-detect branches, Merge Requests, and tags

Step 3: Jenkinsfile​

Create at: Jenkinsfile (directly under the module project root)

pipeline {
agent any

options {
timeout(time: 20, unit: 'MINUTES')
}

environment {
// Unlike GitLab CI (.gitlab-ci.yml), Jenkins does not automatically inject
// CI_API_V4_URL / CI_PROJECT_ID. State the actual per-project values here.
GITLAB_API_URL = 'https://gitlab.example.com/api/v4'
GITLAB_PROJECT_ID = '123'
}

stages {
stage('Checkout') {
steps { checkout scm }
}

stage('Install') {
steps { sh 'bun install --frozen-lockfile' }
}

stage('Validate') {
steps { sh 'bun run validate' }
}

stage('Build') {
steps { sh 'bun run build' }
}

stage('Archive') {
steps { archiveArtifacts artifacts: 'target/*.zip', fingerprint: true }
}

stage('Publish latest (main only)') {
when { branch 'main' }
steps {
withCredentials([string(credentialsId: 'gitlab-api-token', variable: 'GITLAB_API_TOKEN')]) {
sh '''
curl --fail --header "PRIVATE-TOKEN: ${GITLAB_API_TOKEN}" \
--upload-file target/*.zip \
"${GITLAB_API_URL}/projects/${GITLAB_PROJECT_ID}/packages/generic/module/latest/module.zip"
'''
}
}
}
}

post {
always {
updateGitlabCommitStatus name: 'jenkins', state: currentBuild.currentResult == 'SUCCESS' ? 'success' : 'failed'
}
}
}

Key points:

  • bun run build internally executes mvn -s settings.xml clean package
  • Always archive the target/*.zip artifact to ensure traceability
  • When a Merge Request is created or updated, Jenkins runs the build automatically and reflects the result on the GitLab Merge Request with updateGitlabCommitStatus
  • Only on a successful push to main is the artifact uploaded to the latest package in the GitLab Generic Package Registry, which serves as a fixed distribution point for the last successful build
  • Do not mix the normal build and the distribution upload in the same stage; separate them with when { branch 'main' } so the upload does not run for Merge Request builds
  • So that consecutive pushes to the same branch do not leave old builds behind, enable "Discard old builds" and the equivalent of "abort previous builds" (Build Discarder / Disable Concurrent Builds) in the Multibranch Pipeline job configuration
  • agent any runs directly on the controller (the jenkins container of docker-compose). When build volume grows and you separate Agents, attach a label (e.g., build) to the Agents added via the Docker Plugin / Kubernetes Plugin and change this to agent { label 'build' }

Step 4: Tag Releases​

Because the Multibranch Pipeline auto-detects tags as well, add a tag stage to the same Jenkinsfile.

stage('Create GitLab Release') {
when { tag 'v*' }
steps {
withCredentials([string(credentialsId: 'gitlab-api-token', variable: 'GITLAB_API_TOKEN')]) {
sh '''
curl --fail --header "PRIVATE-TOKEN: ${GITLAB_API_TOKEN}" \
--upload-file target/*.zip \
"${GITLAB_API_URL}/projects/${GITLAB_PROJECT_ID}/packages/generic/module/${TAG_NAME}/module.zip"

curl --fail --header "PRIVATE-TOKEN: ${GITLAB_API_TOKEN}" \
--data "name=${TAG_NAME}" \
--data "tag_name=${TAG_NAME}" \
--data "assets[links][][name]=module.zip" \
--data "assets[links][][url]=${GITLAB_API_URL}/projects/${GITLAB_PROJECT_ID}/packages/generic/module/${TAG_NAME}/module.zip" \
"${GITLAB_API_URL}/projects/${GITLAB_PROJECT_ID}/releases"
'''
}
}
}

Operation:

  • Pushing a tag such as v0.1.0 re-runs Validate → Build from the same Jenkinsfile and then creates the release
  • The tag-specific artifact is stored in the Generic Package Registry and associated as an asset link of the GitLab Release (the Release itself doubles as the artifact store)

Step 5: Distribution Point for the Last Successful Build​

  • Use the latest package in the GitLab Generic Package Registry (packages/generic/module/latest/module.zip) as a fixed distribution point, equivalent to Jenkins' lastSuccessfulBuild
  • Because it is updated automatically after a successful push to main, consumers can always retrieve it from the same URL

Step 6: Merge Request Template (Optional)​

Including at least the following in .gitlab/merge_request_templates/Default.md keeps review quality consistent.

  • The module ID/version that changed
  • Whether module.xml is affected
  • Dependency changes (pom.xml)
  • Results of running bun run validate / bun run build

Step 7: Put the Dependency Policy in Writing​

pom.xml operational rules:

  • As a rule, do not write <version> for jp.co.intra_mart:* (prefer BOM management)
  • Fix the parent suffix (e.g., 2026-spring) as an organizational standard
  • Perform version updates all at once in a Merge Request, and require the pipeline to succeed

Minimum Adoption Checklist​

  • A protected branch (main) is configured
  • Merge request approval rules and "Pipelines must succeed" are enabled
  • The Jenkins Multibranch Pipeline auto-detects branches/Merge Requests/tags via GitLab Branch Source
  • The webhook is protected with a Secret Token
  • The commit status is reflected on the Merge Request
  • target/*.zip can be retrieved as a Jenkins build artifact
  • After a push to main, the latest package in the Generic Package Registry is updated
  • Pushing a tag attaches the zip to the GitLab Release
  • Jenkins authentication is integrated with the organization's SSO, and no fixed admin/admin account is in use
  • Backups of JENKINS_HOME are configured