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.
Recommended DevOps Architecture
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.lockgenerated after runningbun installto the repository (CI usesbun 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://forexternal_url. Either obtain certificates automatically with theletsencryptsettings built into GitLab Omnibus, or, if you terminate TLS at an external reverse proxy/load balancer, separately adjust omnibus settings such asnginx['listen_port'] - Do not skip the Jenkins setup wizard. An init script that specifies
-Djenkins.install.runSetupWizard=falseinJAVA_OPTSto auto-createadmin/adminis 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:8080and 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 aslts-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.ymlside) - Run apt-get/curl as
USER rootand switch back toUSER jenkinsat the end (do not start up as root)
Initial Setup
GitLab:
-
After the container starts, obtain the initial
rootpassword (it is stored in/etc/gitlab/initial_root_passwordfor 24 hours only)docker compose exec gitlab cat /etc/gitlab/initial_root_password -
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:
-
Obtain the initial administrator password generated on first startup
docker compose exec jenkins cat /var/jenkins_home/secrets/initialAdminPassword -
In the setup wizard, install the GitLab Plugin and the GitLab Branch Source Plugin in addition to the recommended plugins
-
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 createon a schedule and move the output to external storage. Because/etc/gitlab/gitlab-secrets.jsonand/etc/gitlab/gitlab.rbare not covered by the backup, store them separately - Jenkins: Regularly back up the entire
jenkins_homevolume (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.
- Create the project (keep it Private. Do not make it Public/Internal unless there is a positive reason to publish it)
- Protect
mainunder Settings > Repository > Protected branches (restrict "Allowed to push" to Maintainer or above, or to No one) - Enable approval rules and "Pipelines must succeed" under Settings > Merge requests
- 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)
- Generate and note a Secret Token for the Jenkins → GitLab webhook (used under Settings > Webhooks)
- 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/adminaccount 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
- Register the deploy key's private key under Manage Jenkins > Credentials > System > Global credentials as
SSH Username with private key(Username:git) - Register a GitLab Project/Group Access Token (
apiscope) asSecret text(for writing back commit statuses and uploading packages) - Associate the GitLab server URL with the API token above under Manage Jenkins > System > GitLab connection
- 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 buildinternally executesmvn -s settings.xml clean package- Always archive the
target/*.zipartifact 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
mainis the artifact uploaded to thelatestpackage 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 anyruns directly on the controller (thejenkinscontainer 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 toagent { 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.0re-runs Validate → Build from the sameJenkinsfileand 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
latestpackage 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>forjp.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/*.zipcan be retrieved as a Jenkins build artifact- After a push to main, the
latestpackage 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/adminaccount is in use - Backups of
JENKINS_HOMEare configured