Skip to content
DevOps

Jenkins vs GitLab CI: What a Decade of Pipelines Taught Me

A candid comparison from someone who has operated both: the philosophical split, the same pipeline in Jenkinsfile and .gitlab-ci.yml, where each tool still wins, and one rm -rf war story.

5 min read Updated Sep 4, 2026
Jenkins vs GitLab CI: What a Decade of Pipelines Taught Me

I have a confession: I still have a soft spot for Jenkins. Not because it's good — parts of it are actively hostile — but because it taught a whole generation of us what CI even was. Somewhere out there is a Jenkins box I set up years ago, uptime measured in years, quietly building someone's monolith every night. Nobody dares touch it. That box is the entire Jenkins experience in one sentence.

These days most of my pipelines live in GitLab CI, and when a client asks "which one should we use?", my honest answer takes about twenty minutes and involves at least one story about a plugin upgrade gone wrong. This article is the written version of those twenty minutes.

Pipeline flow: git push triggers YAML-defined stages for build, test and deploy

The philosophical difference nobody says out loud

Jenkins is a automation server. It doesn't care about your git workflow, your merge requests, or your opinions. It runs jobs. Everything else — SCM polling, pipeline-as-code, Docker agents, notifications — arrived later, bolted on through roughly 1,900 plugins of wildly varying quality. That's not an insult; it's the design. Jenkins is a Swiss army knife that someone kept welding new blades onto for fifteen years.

GitLab CI was born inside a git platform, and it shows. The pipeline isn't a thing you connect to your repo — it is part of your repo. Merge request? Pipeline. Tag? Pipeline. The integration isn't a feature, it's the whole point.

In practice this means: Jenkins can automate literally anything, including things that have nothing to do with your codebase (I've seen it restart office printers). GitLab CI automates your software delivery, brilliantly, and gets awkward the further you stray from that path.

Two files, same job

Here's the same pipeline in both dialects. First, a Jenkinsfile (declarative — if you're still writing scripted pipelines in Groovy, we need to talk):

pipeline {
    agent { docker { image 'mcr.microsoft.com/dotnet/sdk:8.0' } }

    stages {
        stage('Build') {
            steps { sh 'dotnet build -c Release' }
        }
        stage('Test') {
            steps { sh 'dotnet test --logger trx' }
            post {
                always { junit '**/TestResults/*.trx' }  // needs the MSTest plugin. Of course it does.
            }
        }
        stage('Deploy') {
            when { branch 'main' }
            steps { sh './deploy.sh staging' }
        }
    }

    post {
        failure { slackSend channel: '#builds', message: "💥 ${env.JOB_NAME} ${env.BUILD_NUMBER}" }
    }
}

And the .gitlab-ci.yml equivalent:

image: mcr.microsoft.com/dotnet/sdk:8.0

stages: [build, test, deploy]

build:
  stage: build
  script: dotnet build -c Release

test:
  stage: test
  script: dotnet test --logger trx
  artifacts:
    when: always
    reports:
      junit: '**/TestResults/*.trx'   # built in. No plugin. This is the pitch.

deploy-staging:
  stage: deploy
  script: ./deploy.sh staging
  rules:
    - if: $CI_COMMIT_BRANCH == "main"
  environment: staging

Look at the test-report lines in both. That's the whole comparison in miniature: in GitLab it's two lines of YAML that were always going to work; in Jenkins it's a plugin, and the plugin has a changelog, and the changelog has a line that says "breaking change" somewhere in your future.

Where Jenkins still wins

  • Weird environments. Building for Windows, macOS, some ancient AIX box, and an FPGA toolchain in the same pipeline? Jenkins agents will happily run anywhere Java runs, which is everywhere. GitLab runners are catching up, but Jenkins has twenty years of "we build on that too".
  • Organizational inertia, the good kind. If a company already has Jenkins with 400 jobs and a team that knows it, migrating is a project, not a decision. Sometimes the right architecture is the one you already operate well.
  • Truly custom orchestration. Pipelines that coordinate humans (approval chains across departments, scheduled windows, tickets) bend Jenkins' shared libraries into shapes YAML doesn't want to make.

Where GitLab CI wins (which is most places, now)

  • Everything is versioned. Pipeline config lives in the merge request that changes the code. Review the build change and the code change together. Once you've worked this way, Jenkins job configs feel like editing production by hand.
  • Zero-maintenance runners. Autoscaling Docker/Kubernetes runners you set up once. Compare: Jenkins agent AMIs, plugin compatibility matrices, and the biannual "let's upgrade Jenkins" ritual that eats a sprint.
  • The platform gravity. Container registry, environments, review apps, security scanning — all wired into the same YAML. You'd need a dozen Jenkins plugins and a prayer circle.

My actual decision rule

Greenfield, or team under ~50 engineers, or already on GitLab: GitLab CI, no meeting required. Big org with heavy Jenkins investment and exotic build targets: keep Jenkins for the exotic stuff, move application pipelines to GitLab gradually, and stop writing new Jenkinsfiles. The hybrid period is annoying but shorter than you fear.

And whichever you pick: put the pipeline in the repo, keep stages under ten minutes, and make the failure notification land where humans actually look. The tool matters less than those three habits.

One last war story

The worst outage I've debugged in CI wasn't caused by either tool. It was a shell script inside the pipeline that did rm -rf $BUILD_DIR/ — and one day $BUILD_DIR was empty. Jenkins ran it obediently. GitLab would have too. YAML or Groovy, the pipeline is production code; review it like production code, quote your variables, and set -euo pipefail like your weekend depends on it. Because it does.

Running a messy CI setup you'd like untangled? That's a fun week for me — get in touch. Related reading: containerizing services with Docker and centralized logging with Graylog.

Keep reading

Related articles

DevOps 5 min read

Metrics, Traces, Logs: OpenTelemetry in Anger

Three signals and the different questions they answer, the trace-ID join that delivers 80% of the value, the collector pattern, tail-based sampling that keeps the interesting 100%, and the incident-driven rollout.

DevOps 4 min read

Feature Flags: Deploy Is Not Release

The four flag species and why conflating them causes misery, sticky percentage rollouts with one decision point, the staged rollout playbook with observability hooks, and the hygiene that prevents flag archaeology.