Running Cypress in Jenkins and GitLab CI

8 min read

GitHub Actions covers most projects, but plenty of teams run on Jenkins, GitLab CI, CircleCI, or self-hosted runners. The good news: the concepts are identical across every platform — install dependencies, build the app, run Cypress, save artifacts. Only the YAML/Groovy syntax changes. This lesson covers Jenkins and GitLab CI head-to-head, the Cypress Docker images that make any of them painless, and the gotchas that come up when you move off GitHub.

The Cypress Docker images — your shortcut

Every CI platform that supports Docker can use Cypress's official images. They ship with Node.js, Cypress, Chrome, Firefox, and Edge pre-installed. No browser provisioning, no apt-get install, no surprise binary mismatches.

  • cypress/included:<version> — Cypress + browsers + Node.js. Run cypress run directly. Best for CI.
  • cypress/browsers:<version> — browsers + Node.js, no Cypress. Use when you install Cypress yourself.
  • cypress/base:<version> — Node.js + system deps, no browsers. Smallest; rarely the right choice for CI.

For most CI configs, cypress/included is the only image you need. Pin the tag to a version (cypress/included:13.6.0) so an upstream Cypress release doesn't silently change your CI image overnight.

Jenkins — the canonical pipeline

Jenkins uses a Jenkinsfile checked into the repo. Declarative pipeline syntax is the modern default:

pipeline {
  agent {
    docker {
      image 'cypress/included:13.6.0'
      args '-u root'   // run as root so npm install can write to node_modules
    }
  }
 
  environment {
    CYPRESS_BASE_URL       = credentials('staging-base-url')
    CYPRESS_ADMIN_PASSWORD = credentials('staging-admin-password')
  }
 
  stages {
    stage('Install') {
      steps { sh 'npm ci' }
    }
    stage('Test') {
      steps { sh 'npx cypress run --browser chrome' }
    }
    stage('Report') {
      steps { sh 'npm run cy:report' }
    }
  }
 
  post {
    always {
      archiveArtifacts(
        artifacts: 'cypress/screenshots/**, cypress/videos/**, cypress/reports/html/**',
        allowEmptyArchive: true
      )
      junit allowEmptyResults: true, testResults: 'cypress/reports/junit/*.xml'
    }
  }
}

The structure mirrors the GitHub Actions workflow:

  • agent { docker { image: '...' } } — every stage runs inside the Cypress image. No Jenkins-side browser install.
  • environment { CYPRESS_X = credentials(...) } — Jenkins's credentials plugin injects secrets. Same pattern as GitHub's secrets context.
  • stages — install, test, report, in order.
  • post { always { ... } } — runs whether the pipeline succeeded or failed. archiveArtifacts is Jenkins's equivalent of GitHub's upload-artifact. junit parses Mocha JUnit XML and feeds the Jenkins test-results dashboard.

If your team uses a multi-reporter setup that emits both Mochawesome JSON and JUnit XML (chapter 7), Jenkins gives you a native test-results page and a downloadable HTML report.

GitLab CI — concise YAML

GitLab CI uses .gitlab-ci.yml. Same structure, lighter syntax:

stages:
  - test
 
cypress:
  stage: test
  image: cypress/included:13.6.0
  variables:
    CYPRESS_BASE_URL: $STAGING_URL
    CYPRESS_ADMIN_PASSWORD: $STAGING_ADMIN_PASSWORD
  script:
    - npm ci
    - npx cypress run --browser chrome
    - npm run cy:report
  artifacts:
    when: always
    paths:
      - cypress/screenshots
      - cypress/videos
      - cypress/reports/html
    reports:
      junit: cypress/reports/junit/*.xml
    expire_in: 1 week

GitLab's variables block reads from project-level CI/CD settings (the equivalent of GitHub Actions secrets). artifacts.reports.junit integrates with GitLab's own test-result dashboard. Everything compresses into roughly half the lines of the equivalent Jenkinsfile.

GitLab has the smoothest first-party container support of any CI — image: cypress/included is all you need; the runner pulls and caches it automatically.

The three platforms side by side

GitHub Actions vs Jenkins vs GitLab CI for Cypress

GitHub Actions

  • YAML in .github/workflows/

  • First-party cypress-io/github-action handles caching, browser, wait-on

  • Hosted runners with browsers preinstalled — no Docker needed

  • Best for: GitHub-hosted projects of any size

Jenkins

  • Groovy Jenkinsfile in repo root

  • Run inside cypress/included Docker image for clean browser env

  • credentials() plugin for secrets; junit step for test reports

  • Best for: enterprise environments with existing Jenkins infra

GitLab CI

  • YAML in .gitlab-ci.yml

  • Native image: cypress/included support

  • artifacts.reports.junit feeds the GitLab test dashboard

  • Best for: GitLab-hosted projects, especially self-managed instances

The trade-off across all three: GitHub Actions gives you the slickest first-party Cypress integration; Jenkins gives you the most flexibility (and the most YAML); GitLab sits in the middle on both axes. Pick whichever your repo is already on — none of the three is dramatically better than the others for Cypress specifically.

Common cross-platform issues

A few problems that come up on every non-GitHub platform until you've seen them once:

  • "Missing browsers." The CI runner doesn't have Chrome installed. Fix: use cypress/included as the image. Don't try to apt-get install chrome on a base Linux image — the Cypress images encode the right Chrome version for the Cypress version.
  • "Cannot find display / X server." Cypress runs headless inside the Docker images via Xvfb (a virtual display). If you bypass the official images and run on a custom Linux box, you may need to install and start Xvfb manually. Stick with the official images and skip the headache.
  • Out-of-memory crashes on long runs. Cypress is memory-hungry; the CI runner's default Node memory cap (~512 MB on some platforms) isn't enough. Increase with NODE_OPTIONS: "--max-old-space-size=4096" in the env block.
  • Volume permission errors when running as root. Some Docker setups complain about file ownership when the container writes to a mounted volume. The Jenkinsfile above passes -u root to sidestep this; on GitLab, pinning a specific user in the image works similarly.

A real-world dual-platform setup

Some teams run both GitHub Actions and Jenkins — Actions for fast PR checks, Jenkins for nightly heavy regression on a self-hosted runner with database access. The Cypress side stays identical; only the orchestration changes. Keep cypress.config.ts, cypress.env.json, and your test code platform-agnostic. Each platform's config file is just a different way of saying "install, build, run, archive."

This is the single most useful framing for thinking about CI: the platform is wiring; the work is the same. Once you've set up Cypress on one platform, the second one takes 20 minutes.

⚠️ Common mistakes

  • Installing Chrome manually instead of using a Cypress Docker image. Versions drift, missing system libs cause cryptic crashes, and the maintenance burden is yours forever. The Cypress team publishes new images for every release — let them carry the integration cost.
  • Hardcoding secrets in the Jenkinsfile or .gitlab-ci.yml. Anything checked in is leaked. Use the platform's secret store: credentials(...) in Jenkins, project-level CI/CD variables in GitLab.
  • Saving artifacts only on success or only on failure. The most useful artifact is the one from the failing run, but when: always ensures the report exists for both. Stakeholders expect the same green dashboard URL whether the build passed or failed.

🎯 Practice task

Set up a Cypress CI run on a non-GitHub platform. 30-45 minutes.

  1. Pick GitLab CI (free public projects on gitlab.com) or a local Jenkins (a Docker jenkins/jenkins:lts instance is a 5-minute setup).
  2. GitLab path: push your project to a GitLab repo. Create .gitlab-ci.yml from the lesson template. Define STAGING_URL and STAGING_ADMIN_PASSWORD as project CI variables. Push and watch the pipeline run.
  3. Jenkins path: create a multibranch pipeline pointed at your repo. Configure the same two credentials in the Jenkins credentials store. Add the Jenkinsfile from the lesson. Trigger a build.
  4. Force a failure by breaking one assertion. Confirm the artifacts (screenshots, videos, HTML report) are archived on the failure step.
  5. JUnit integration — install mocha-junit-reporter (chapter 7). Configure Cypress to emit JUnit XML alongside Mochawesome JSON. Confirm the Jenkins or GitLab test-result dashboard picks up the XML and shows per-test status.
  6. Nightly job — schedule the same pipeline to run at 03:00 UTC. In GitLab, set up a pipeline schedule. In Jenkins, add triggers { cron('H 3 * * *') } to the pipeline.
  7. Stretch: if you have access to both platforms, set up the same project on both. Compare run times, artifact UX, and dashboard quality. Pick the one your team will actually look at — that's the right tool for you.

The next lesson goes from "one machine running tests serially" to "ten machines running tests in parallel" — the load-balancing strategies that turn 30-minute suites into 5-minute suites.

// tip to track lessons you complete and pick up where you left off across devices.