GitLab CI pipelines for Playwright require one design decision upfront: whether to install browsers during the pipeline or use the official Playwright Docker image. The Docker image approach is faster and avoids the most common failure mode, where a version mismatch between the image tag and @playwright/test in package.json breaks the pipeline with an executable doesn't exist error. This guide covers the minimal working .gitlab-ci.yml, artifact uploads, secrets, caching, parallel sharding, and a multi-environment setup for staging and production.
Prerequisites
- Playwright project working locally with
npx playwright test - GitLab repository (gitlab.com or self-hosted)
.gitlab-ci.ymldoesn't exist yet (or you're adding to it)
The Minimal Working Pipeline
Create .gitlab-ci.yml in your project root:
image: mcr.microsoft.com/playwright:v1.44.0-jammy
stages:
- test
playwright-tests:
stage: test
script:
- npm ci
- npx playwright test
artifacts:
when: always
paths:
- playwright-report/
expire_in: 7 daysThat's it. Push this file and GitLab CI will run your tests on every commit.
Let's understand what each part does.
Line-by-Line Explanation
image: mcr.microsoft.com/playwright:v1.44.0-jammy
This is the Docker container your pipeline runs inside. Playwright's official Docker image comes with:
- Node.js
- All Playwright browser dependencies pre-installed
- Linux environment
This is the recommended approach — no browser installation needed in the pipeline.
Pin the version (v1.44.0, not latest) so your pipeline doesn't break when Playwright releases a new version. Update it manually when you upgrade Playwright locally.
npm ci vs. npm install
In CI, always use npm ci instead of npm install:
npm ciinstalls exactly what's inpackage-lock.json(deterministic)npm ciis faster in CI environmentsnpm cifails ifpackage-lock.jsonis missing or inconsistent
artifacts
Playwright generates an HTML report in playwright-report/ after each run. The artifacts section tells GitLab to save this folder so you can download and view it.
when: always— save artifacts even if tests fail (you need the report most when tests fail!)expire_in: 7 days— GitLab automatically deletes artifacts after 7 days
To view the report: in GitLab CI → the pipeline → the job → "Download artifacts" → open playwright-report/index.html.
A More Complete Pipeline
For real projects, you'll want more structure:
image: mcr.microsoft.com/playwright:v1.44.0-jammy
stages:
- test
variables:
# Cache node_modules between runs for speed
npm_config_cache: "$CI_PROJECT_DIR/.npm"
cache:
key: "$CI_COMMIT_REF_SLUG"
paths:
- .npm/
- node_modules/
playwright-tests:
stage: test
# Run on merge requests and main branch
rules:
- if: $CI_PIPELINE_SOURCE == "merge_request_event"
- if: $CI_COMMIT_BRANCH == "main"
script:
- npm ci
- npx playwright test --reporter=html,junit
artifacts:
when: always
paths:
- playwright-report/
- test-results/
reports:
junit: test-results/results.xml
expire_in: 7 days
# Timeout for the whole job
timeout: 20 minutesWhat's new here:
variables: Set environment variables for the job. npm_config_cache tells npm to use a specific cache directory.
cache: Node modules are cached between pipeline runs. First run: slow (downloads everything). Subsequent runs: fast (reads from cache). Key is the branch name so different branches have separate caches.
rules: Control when this job runs. The config above runs on:
- Merge requests (so you see test results before merging)
- Pushes to
main(after merging)
--reporter=html,junit: Generate both HTML report and JUnit XML. JUnit format is understood by GitLab and shows test results directly in the pipeline UI.
reports: junit: This tells GitLab where the JUnit XML is. GitLab then shows a "Tests" tab in the pipeline with pass/fail per test.
Using Environment Variables (Secrets)
Never put passwords or API keys in .gitlab-ci.yml. Use GitLab CI/CD variables:
1. Project → Settings → CI/CD → Variables
2. Add: BASE_URL, ADMIN_EMAIL, ADMIN_PASSWORD, API_KEY
3. Mark sensitive ones as "Masked" (hidden in logs)
4. Mark environment-specific ones as "Protected" (only runs on protected branches)
Using in your pipeline:playwright-tests:
script:
- npm ci
- npx playwright test
variables:
BASE_URL: $BASE_URL # From GitLab CI variables
ADMIN_EMAIL: $ADMIN_EMAIL # From GitLab CI variables// playwright.config.ts
import dotenv from 'dotenv';
dotenv.config();
export default defineConfig({
use: {
baseURL: process.env.BASE_URL || 'http://localhost:3000',
},
});GitLab CI variables are available as environment variables in the job — process.env.BASE_URL works.
Parallel Execution and Sharding
For large test suites, run tests in parallel:
playwright-tests:
stage: test
parallel:
matrix:
- SHARD: ["1/4", "2/4", "3/4", "4/4"]
script:
- npm ci
- npx playwright test --shard=$SHARD
artifacts:
when: always
paths:
- playwright-report/
expire_in: 7 daysThis creates 4 parallel jobs, each running 25% of the tests. Total test time is roughly divided by 4.
Note: Merging the split HTML reports requires additional steps (Playwright's merge-reports command).
Running in Headed Mode (Debugging)
By default, Playwright runs headless in CI. If you need to debug visually:
playwright-debug:
stage: test
script:
- npm ci
- npx playwright test --headed --video=on
when: manual # Only run when manually triggered
artifacts:
when: always
paths:
- test-results/ # Videos are saved here
expire_in: 1 daywhen: manual means this job doesn't run automatically — you trigger it manually from the GitLab UI when you need to debug.
Handling Flaky Tests
Playwright has built-in retry logic. Configure it for CI:
// playwright.config.ts
export default defineConfig({
retries: process.env.CI ? 2 : 0, // Retry 2 times in CI only
});Or in .gitlab-ci.yml, allow the job itself to retry on failure:
playwright-tests:
retry:
max: 1 # Retry the whole job once if it fails
when:
- runner_system_failure
- stuck_or_timeout_failureNote: Job-level retry (GitLab) is for infrastructure failures. Test-level retry (Playwright config) is for flaky tests. Use both.
Full Example with Multiple Environments
image: mcr.microsoft.com/playwright:v1.44.0-jammy
stages:
- test
.playwright-base:
script:
- npm ci
- npx playwright test --reporter=html,junit
artifacts:
when: always
paths:
- playwright-report/
reports:
junit: test-results/results.xml
expire_in: 7 days
timeout: 20 minutes
playwright-staging:
extends: .playwright-base
variables:
BASE_URL: https://staging.yourapp.com
rules:
- if: $CI_COMMIT_BRANCH == "develop"
- if: $CI_PIPELINE_SOURCE == "merge_request_event"
playwright-production:
extends: .playwright-base
variables:
BASE_URL: https://yourapp.com
rules:
- if: $CI_COMMIT_BRANCH == "main"
environment:
name: productionThis runs staging tests on develop branch and PRs, and production tests only when merging to main.
Troubleshooting Common Issues
"Executable doesn't exist" error:You're not using the Playwright Docker image, or it's the wrong version. Stick with mcr.microsoft.com/playwright:v1.44.0-jammy and the version must match your @playwright/test version exactly.
CI machines are slower. Increase timeouts in playwright.config.ts:
export default defineConfig({
timeout: 60000, // 60s per test (up from 30s default)
actionTimeout: 15000, // 15s per action
});You have a dependency that's not in package.json. Run npm install locally and commit the updated package.json and package-lock.json.
Add caching (shown in the complete example above). First run will be slow; subsequent runs will be fast.
Quick Checklist
Before pushing your first pipeline:
- [ ]
.gitlab-ci.ymlis in the project root - [ ] Using the Playwright Docker image with pinned version
- [ ]
npm ci(notnpm install) - [ ] Artifacts configured to save
playwright-report/ - [ ] Secrets in GitLab CI variables, not in the YAML file
- [ ] Playwright
retriesconfigured for CI environment - [ ]
timeoutset inplaywright.config.ts(CI machines are slower)
With this setup, every push to your repository will automatically run your test suite and make results available in the GitLab pipeline view.