---
title: "Lighthouse CI with GitLab CI/CD: Setup Guide"
description: "Configure Lighthouse CI for GitLab pipelines. Includes .gitlab-ci.yml examples, merge request integration, and artifact storage."
canonical_url: "https://unlighthouse.dev/learn-lighthouse/lighthouse-ci/gitlab-ci"
last_updated: "2026-07-05T10:28:35.131Z"
---

Lighthouse CI integrates smoothly with GitLab CI/CD to run performance audits on every merge request. This guide covers setup, configuration, and best practices for [GitLab](https://gitlab.com) pipelines.

## Quick Start

Basic `.gitlab-ci.yml` to audit a static site:

```yaml
lighthouse:
  image: cypress/browsers:node-22.12.0-chrome-131.0.6778.204-1-ff-134.0-edge-131.0.2903.112-1
  stage: test
  script:
    - npm install -g @lhci/cli@0.15.x
    - lhci autorun
  artifacts:
    when: always
    paths:
      - .lighthouseci/
    expire_in: 7 days
```

Create `lighthouserc.js`:

```js
export default {
  ci: {
    collect: {
      staticDistDir: './dist',
      chromePath: '/usr/bin/google-chrome',
      chromeFlags: '--no-sandbox'
    },
    upload: {
      target: 'temporary-public-storage'
    }
  }
}
```

## Why --no-sandbox?

GitLab CI runners require Chrome's `--no-sandbox` flag because [Docker's default seccomp profile blocks the syscalls](https://chromium.googlesource.com/chromium/src/+/0e94f26e8/docs/linux_sandboxing.md) Chrome needs to create its own namespace sandbox. The container itself provides isolation, creating a conflict.

Without it, Chrome fails to launch:

```text
Failed to launch Chrome: spawn EACCES
```

Always include `chromeFlags: '--no-sandbox'` in your configuration.

<warning>

**Security note**: Only use `--no-sandbox` in trusted CI environments running your own code. If auditing untrusted websites, [malicious pages could escape the container](https://googlechrome.github.io/lighthouse-ci/docs/recipes/docker-client/). For maximum security, use [Jess Frazelle's Chrome seccomp profile](https://github.com/jessfraz/dotfiles/blob/main/etc/docker/seccomp/chrome.json) which allows only the specific syscalls Chrome needs.

</warning>

## Testing Different Targets

### Static Build Output

Audit files in `dist/` or `build/`:

```yaml
lighthouse:
  image: cypress/browsers:node-22.12.0-chrome-131.0.6778.204-1-ff-134.0-edge-131.0.2903.112-1
  stage: test
  script:
    - npm ci
    - npm run build
    - npm install -g @lhci/cli@0.15.x
    - lhci autorun
  artifacts:
    paths:
      - .lighthouseci/
```

```js
// lighthouserc.js
const config2 = {
  ci: {
    collect: {
      staticDistDir: './dist',
      chromePath: '/usr/bin/google-chrome',
      chromeFlags: '--no-sandbox'
    }
  }
}
```

### Live URLs

Test production or staging URLs:

```js
const config3 = {
  ci: {
    collect: {
      url: [
        'https://example.com',
        'https://example.com/about',
        'https://example.com/products'
      ],
      chromePath: '/usr/bin/google-chrome',
      chromeFlags: '--no-sandbox'
    }
  }
}
```

### Local Development Server

Start server, run audits, shut down:

```js
const config4 = {
  ci: {
    collect: {
      startServerCommand: 'npm run serve',
      startServerReadyPattern: 'Server listening on',
      url: ['http://localhost:8080'],
      chromePath: '/usr/bin/google-chrome',
      chromeFlags: '--no-sandbox'
    }
  }
}
```

The CLI automatically kills the server after audits complete.

## Adding Assertions

Fail the pipeline if performance budgets aren't met:

```js
const config5 = {
  ci: {
    collect: {
      staticDistDir: './dist',
      chromePath: '/usr/bin/google-chrome',
      chromeFlags: '--no-sandbox'
    },
    assert: {
      assertions: {
        'categories:performance': ['error', { minScore: 0.9 }],
        'categories:accessibility': ['error', { minScore: 0.9 }],
        'categories:best-practices': ['error', { minScore: 0.9 }],
        'categories:seo': ['error', { minScore: 0.9 }],
        'first-contentful-paint': ['error', { maxNumericValue: 2000 }],
        'largest-contentful-paint': ['error', { maxNumericValue: 2500 }],
        'cumulative-layout-shift': ['error', { maxNumericValue: 0.1 }],
        'total-blocking-time': ['error', { maxNumericValue: 300 }]
      }
    },
    upload: {
      target: 'temporary-public-storage'
    }
  }
}
```

Pipeline fails if any assertion fails. See [budgets](/learn-lighthouse/lighthouse-ci/budgets) for assertion syntax.

## Storing Reports as Artifacts

Save HTML reports for download:

```yaml
lighthouse:
  image: cypress/browsers:node-22.12.0-chrome-131.0.6778.204-1-ff-134.0-edge-131.0.2903.112-1
  stage: test
  script:
    - npm install -g @lhci/cli@0.15.x
    - lhci autorun --upload.target=filesystem --upload.outputDir=./lhci-reports
  artifacts:
    when: always
    paths:
      - lhci-reports/
      - .lighthouseci/
    expire_in: 30 days
```

Reports appear in GitLab's merge request artifacts.

## Testing Review Apps

Audit ephemeral Review Apps using GitLab environment variables:

```yaml
lighthouse:
  image: cypress/browsers:node-22.12.0-chrome-131.0.6778.204-1-ff-134.0-edge-131.0.2903.112-1
  stage: test
  rules:
    - if: '$CI_PIPELINE_SOURCE == "merge_request_event"'
  script:
    - npm install -g @lhci/cli@0.15.x
    - lhci autorun --collect.url=$CI_ENVIRONMENT_URL
  environment:
    name: review/$CI_COMMIT_REF_NAME
    url: https://$CI_COMMIT_REF_SLUG.review.example.com
```

Or configure in `lighthouserc.js`:

```js
const config6 = {
  ci: {
    collect: {
      url: [process.env.CI_ENVIRONMENT_URL],
      chromePath: '/usr/bin/google-chrome',
      chromeFlags: '--no-sandbox'
    }
  }
}
```

## Reducing Variance with Multiple Runs

Run Lighthouse multiple times and use median values:

```js
const config7 = {
  ci: {
    collect: {
      numberOfRuns: 5,
      staticDistDir: './dist',
      chromePath: '/usr/bin/google-chrome',
      chromeFlags: '--no-sandbox'
    }
  }
}
```

Increases job duration but reduces false negatives from variance. [Google's research](https://developers.google.com/web/tools/lighthouse/variability) shows 5 runs is twice as stable as 1 run. GitLab runners are generally stable, so 3 runs ([37% variance reduction](https://github.com/GoogleChrome/lighthouse/blob/main/docs/variability.md)) is usually sufficient.

<note type="info">

**Hardware requirements**: LHCI needs minimum [2 cores and 4GB RAM](https://googlechrome.github.io/lighthouse-ci/docs/troubleshooting.html). Avoid burstable instances (AWS t-series) as CPU throttling causes score inconsistency.

</note>

## Full Production Pipeline

Complete example with build, test, and artifact storage:

```yaml
stages:
  - build
  - test

variables:
  NODE_ENV: production

build:
  stage: build
  image: node:20
  script:
    - npm ci
    - npm run build
  artifacts:
    paths:
      - dist/
    expire_in: 1 hour

lighthouse:
  stage: test
  image: cypress/browsers:node-22.12.0-chrome-131.0.6778.204-1-ff-134.0-edge-131.0.2903.112-1
  dependencies:
    - build
  rules:
    - if: '$CI_PIPELINE_SOURCE == "merge_request_event"'
    - if: '$CI_COMMIT_BRANCH == "main"'
  script:
    - npm install -g @lhci/cli@0.15.x
    - lhci autorun
  artifacts:
    when: always
    paths:
      - .lighthouseci/
      - lhci-reports/
    expire_in: 30 days
```

```js
// lighthouserc.js
const config8 = {
  ci: {
    collect: {
      staticDistDir: './dist',
      numberOfRuns: 3,
      chromePath: '/usr/bin/google-chrome',
      chromeFlags: '--no-sandbox'
    },
    assert: {
      preset: 'lighthouse:recommended',
      assertions: {
        'categories:performance': ['error', { minScore: 0.9 }],
        'uses-responsive-images': 'off',
        'offscreen-images': 'off'
      }
    },
    upload: {
      target: 'filesystem',
      outputDir: './lhci-reports'
    }
  }
}
```

## Lighthouse CI Server Integration

Upload to a persistent Lighthouse CI server for historical tracking:

```yaml
lighthouse:
  image: cypress/browsers:node-22.12.0-chrome-131.0.6778.204-1-ff-134.0-edge-131.0.2903.112-1
  stage: test
  script:
    - npm install -g @lhci/cli@0.15.x
    - lhci autorun --upload.serverBaseUrl=$LHCI_SERVER_URL --upload.token=$LHCI_BUILD_TOKEN
```

Set `LHCI_SERVER_URL` and `LHCI_BUILD_TOKEN` as CI/CD variables in GitLab project settings. See [server setup](/learn-lighthouse/lighthouse-ci/server) for details.

## Troubleshooting

### Chrome Won't Launch

**Error:**

```text
Failed to launch Chrome: spawn /usr/bin/google-chrome ENOENT
```

**Fix:** Use `cypress/browsers` image which includes Chrome, or install Chrome manually:

```yaml
script:
  - apt-get update
  - apt-get install -y wget gnupg
  - wget -q -O - https://dl-ssl.google.com/linux/linux_signing_key.pub | gpg --dearmor -o /usr/share/keyrings/google-chrome.gpg
  - echo "deb [signed-by=/usr/share/keyrings/google-chrome.gpg] http://dl.google.com/linux/chrome/deb/ stable main" > /etc/apt/sources.list.d/google.list
  - apt-get update
  - apt-get install -y google-chrome-stable
  - npm install -g @lhci/cli@0.15.x
  - lhci autorun
```

### Sandbox Errors

**Error:**

```text
Failed to move to new namespace: PID namespaces supported, Network namespace supported, but failed: errno = Operation not permitted
```

**Fix:** Add `--no-sandbox` to chrome flags:

```js
chromeFlags: '--no-sandbox'
```

### Out of Memory

**Error:**

```text
FATAL ERROR: Ineffective mark-compacts near heap limit Allocation failed - JavaScript heap out of memory
```

**Fix:** Increase memory in `.gitlab-ci.yml`:

```yaml
variables:
  NODE_OPTIONS: --max-old-space-size=4096
```

Or reduce `numberOfRuns`:

```js
const config9 = {
  collect: {
    numberOfRuns: 1
  }
}
```

### Shared Memory Issues

**Error:**

```text
Browser tab has unexpectedly crashed
```

**Fix:** [Docker's default 64MB /dev/shm](https://bugs.chromium.org/p/chromium/issues/detail?id=715363) is insufficient for Chrome. Either use `--disable-dev-shm-usage` flag (writes to disk, slower) or increase shared memory:

```yaml
services:
  - name: docker:dind
    command: [--shm-size=2g]
```

### Job Timeout

GitLab default timeout is 1 hour. For `numberOfRuns: 5`, jobs may timeout. Reduce runs or increase timeout:

```yaml
lighthouse:
  timeout: 2 hours
```

### Reports Not Generated

Check that `upload.target` is set:

```js
const config10 = {
  upload: {
    target: 'filesystem',
    outputDir: './lhci-reports'
  }
}
```

And artifacts path matches:

```yaml
artifacts:
  paths:
    - lhci-reports/
```

## Next Steps

- [Configuration reference](/learn-lighthouse/lighthouse-ci/configuration) - All `lighthouserc.js` options
- [Performance budgets](/learn-lighthouse/lighthouse-ci/budgets) - Assertion syntax and examples
- [LHCI Server](/learn-lighthouse/lighthouse-ci/server) - Historical tracking and comparison
- [Troubleshooting](/learn-lighthouse/lighthouse-ci/troubleshooting) - Common issues and fixes

## When Budgets Fail

If your assertions fail, use these guides to fix the basic issues:

- [Fix LCP](/learn-lighthouse/lcp) - Largest Contentful Paint optimization
- [Fix CLS](/learn-lighthouse/cls) - Cumulative Layout Shift fixes
- [Fix INP](/learn-lighthouse/inp) - Interaction to Next Paint improvements
- [Core Web Vitals Overview](/learn-lighthouse/core-web-vitals) - Understanding all metrics
