Professional CI/CD

GitLab CI/CD and Jenkins: pipelines, runners and quality gates

How I build and run delivery pipelines, shown through a GitLab pipeline for a TypeScript monorepo, a self-hosted runner, and the Jenkins and quality-gate work around them.

My role
Built and maintained pipelines, set up GitLab Runners and Jenkins agents, and integrated SonarQube, Nexus and Harbor.
Context
Client and internal projects at ITGS · 2025–2026
Status
Professional work. Sanitized excerpts only; client pipelines are not public
Stack
  • GitLab CI/CD
  • GitLab Runner
  • Jenkins
  • Docker
  • Nx
  • Nexus
  • SonarQube
  • Harbor
  • Trivy
When a service job runs
Merge request
Tag
Default branch
Changesservice · shared packages · pipeline file
Otherwise manualallowed to fail
Build job · tagged runner · Node 20 image + Docker-in-Docker
  1. Install dependencies npm ci, cache keyed per branch
  2. Build shared packages, then the service Nx production build, Nx daemon off
  3. Check the build output exists fail early, before docker build
  4. Wait for the Docker daemon, build the image tags: commit SHA · branch slug · latest
  5. Push SHA and branch tags always · latest only from the default branch
A self-hosted GitLab Runner I configured
Docker executorprivileged for DinD
Builds and /tmp on tmpfsRAM-backed working directories
Per-job limits4 CPU · 8 GB · 6 concurrent jobs
Networkjob MTU 1300 · pinned DNS for jobs and services
Nexus Docker registryreceives the tagged images
DiagramReconstructed from a pipeline definition and, separately, a runner configuration file; they are shown side by side, not as one system. Registry host, project and image names removed.

Scope

Across client and internal projects, I:

  • built and maintained CI/CD pipelines with GitLab CI/CD and Jenkins across development, staging and production;
  • set up self-hosted GitLab Runners and Jenkins build agents, maintained Jenkins controllers (upgrades, plugins, backups), wrote Jenkins pipelines in Groovy and integrated GitLab with Jenkins;
  • installed and configured SonarQube and Nexus and wired them into pipelines, and deployed Harbor with image scanning and controlled image promotion;
  • added blocking quality and security gates (SonarQube quality gate, Trivy severity threshold) and smoke and health-check stages after deployment;
  • set up GitLab protected environments with manual approvals between environments, reusable pipeline templates and shared Helm charts for development teams.

The pipeline I can show

A TypeScript monorepo built with Nx: three services and shared packages, one GitLab job per service, each producing a Docker image pushed to a Nexus registry. The diagram above follows the actual definition, with names removed.

Problems the definition handles

  • Jobs that never finished. The Nx daemon kept running after the build and held the job open. The pipeline turns it off (NX_DAEMON=false).
  • Native modules on Alpine. A dependency with a native addon failed to install on the slim Node image, so the job installs python3, make and g++ first. Trade-off: every job pays for that install; a prebuilt build image would avoid it.
  • Docker-in-Docker not ready yet. A job could reach docker build before the DinD service accepted connections. A readiness loop (30 tries, 2 seconds apart) runs docker info first.
  • Fail early, with a clear message. Before building the image, the job checks that the expected build output exists, so a broken build fails in the pipeline log rather than deep inside the Dockerfile.
  • Traceable tags. Each image gets the commit SHA and the branch slug. latest moves only from the default branch, so a feature branch can never change what latest means.
  • Selective rebuilds. Merge requests, tags and the default branch always build. On other branches a service job runs only when that service, the shared packages, the workspace config or the pipeline file change; otherwise it waits for a manual start and may fail without blocking the pipeline.

The runner

A self-hosted GitLab Runner with the Docker executor, configured for Docker-in-Docker builds:

  • builds and /tmp on tmpfs, so dependency installs and image layers hit RAM instead of disk;
  • per-job limits of 4 CPUs, 8 GB of memory and 2 GB of shared memory, with six concurrent jobs;
  • a job network MTU of 1300 and fixed DNS servers for jobs and service containers. A lower MTU is the usual fix when the host’s network path carries less than Docker’s default 1500 bytes.

Trade-off: Docker-in-Docker needs privileged mode, so every job on this runner must be trusted code. Rootless builders such as Kaniko or BuildKit avoid that, at the cost of changing how images are built.

Policy as a pipeline check

My N27 project shows the same idea in a public-safe form. Its CI workflow fails if any Compose service mounts the Docker socket or runs privileged, validates the default stack and every optional profile with fake CI-only inputs, and runs API integration tests against a real PostgreSQL with the restricted application role.

Jenkins

The Jenkins work ran on client infrastructure, so there is nothing I can publish: no Jenkinsfiles and no screenshots. The scope was build agent installation and configuration, controller maintenance (upgrades, plugins, backups), Groovy pipelines, GitLab–Jenkins integration, LDAP / Active Directory sign-in for CI/CD tools, and publishing test reports from automated suites triggered by the pipelines.