Running Lighthouse CI in a Lightweight Docker Container
This guide demonstrates how to run Lighthouse CI performance tests for www.pradappandiyan.com using a lightweight Docker container. By containerizing our Lighthouse tests, we ensure consistent, reproducible results across different environments while maintaining a minimal footprint.
Why Docker for Lighthouse CI?
Benefits
- Consistency — Same environment everywhere (local, CI/CD, production)
- Isolation — No conflicts with your system’s Chrome or Node versions
- Portability — Run anywhere Docker is installed
- Reproducibility — Same results across different machines
- Clean Setup — No need to install Chrome/Chromium locally
Why Lightweight Matters
Performance testing should be fast and efficient. A bloated Docker image means:
- ❌ Longer build times
- ❌ More bandwidth for CI/CD pipelines
- ❌ Higher storage costs
- ❌ Slower deployments
Our optimized image addresses all these concerns.
Architecture: How We Built a Lightweight Image
Image Size Breakdown
Final Image Size: ~913MB
┌─────────────────────────────────────────┐
│ Component Size % │
├─────────────────────────────────────────┤
│ Chromium Browser ~650MB 71% │
│ Node.js 18 Alpine ~50MB 5% │
│ Lighthouse CI ~150MB 16% │
│ System Libraries ~63MB 7% │
└─────────────────────────────────────────┘Why ~900MB is Actually Lightweight
Comparison with alternatives:
SolutionSizeNotesOur Alpine Build~913MB✅ OptimizedNode:18 + Chrome (Debian)~1.8GB2x largerPuppeteer Official~1.2GB30% largerFull Ubuntu + Chrome~2.5GB2.7x larger
The bulk of the size (~650MB) comes from Chromium browser, which is unavoidable for real browser testing. You cannot test a website without an actual browser engine!
Optimization Techniques Applied
1. Multi-Stage Build
We use a two-stage Docker build to separate build dependencies from runtime:
# Stage 1: Build (discarded after)
FROM node:18-alpine AS builder
RUN npm ci --omit=dev --ignore-scripts
# Stage 2: Runtime (final image)
FROM node:18-alpine
COPY --from=builder /app/node_modules ./node_modulesResult: Build tools and npm cache don’t bloat the final image.
2. Alpine Linux Base
Alpine Linux is a security-oriented, lightweight distribution:
- Base image: ~5MB
- Full Node.js Alpine: ~50MB
- Standard Node.js: ~900MB
Savings: ~850MB from base image alone
3. Minimal Dependencies
Only essential Chromium dependencies are installed:
RUN apk add --no-cache \
chromium \
freetype \
harfbuzz \
ca-certificatesAvoided: Unnecessary fonts, documentation, development tools
4. Node Modules Cleanup
Aggressive cleanup of unnecessary files:
# Remove documentation
find node_modules -type f -name "*.md" -delete
# Remove license files
find node_modules -type f -name "LICENSE*" -delete
# Remove source maps
find node_modules -type f -name "*.map" -delete
# Remove test directories
find node_modules -type d -name "test" -exec rm -rf {} +Savings: ~35–50MB
5. Cache Cleanup
Remove all temporary files and caches:
RUN rm -rf /var/cache/apk/* \
/tmp/* \
/usr/share/man \
/usr/share/docSavings: ~10–20MB
6. Production-Only Dependencies
Using npm ci --omit=dev ensures no development dependencies:
{
"dependencies": {
"@lhci/cli": "^0.13.0"
}
}Savings: ~100MB of dev dependencies
7. Security Best Practices
Running as non-root user (doesn’t reduce size, but improves security):
RUN addgroup -g 1001 -S nodejs && \
adduser -S lighthouse -u 1001
USER lighthouseQuick Start: Running the Test Locally
Prerequisites
- Docker installed (Get Docker)
- 2GB RAM available
- Internet connection (to test the website)
Docker File
# Ultra-lightweight multi-stage build
# Stage 1: Build stage
FROM node:18-alpine AS builder
WORKDIR /app
# Copy only package files first (better caching)
COPY package*.json ./
# Install dependencies with optimizations
RUN npm ci --omit=dev --ignore-scripts && \
npm cache clean --force && \
# Remove unnecessary files from node_modules
find node_modules -type f -name "*.md" -delete && \
find node_modules -type f -name "LICENSE*" -delete && \
find node_modules -type f -name "*.map" -delete && \
find node_modules -type d -name "test" -exec rm -rf {} + 2>/dev/null || true && \
find node_modules -type d -name "tests" -exec rm -rf {} + 2>/dev/null || true && \
find node_modules -type d -name "docs" -exec rm -rf {} + 2>/dev/null || true
# Stage 2: Runtime stage (minimal)
FROM node:18-alpine
# Install ONLY critical Chromium dependencies (minimal set)
RUN apk add --no-cache \
chromium \
# Only essential libraries
freetype \
harfbuzz \
ca-certificates \
# Remove unnecessary font packages
&& \
# Remove package manager cache
rm -rf /var/cache/apk/* \
/tmp/* \
/usr/share/man \
/usr/share/doc
# Set Chrome environment variables
ENV CHROME_BIN=/usr/bin/chromium-browser \
PUPPETEER_SKIP_CHROMIUM_DOWNLOAD=true \
PUPPETEER_EXECUTABLE_PATH=/usr/bin/chromium-browser \
NODE_ENV=production \
# Disable unnecessary features
CHROME_DEVEL_SANDBOX=/usr/lib/chromium/chrome-sandbox
WORKDIR /app
# Copy only necessary files from builder
COPY --from=builder /app/node_modules ./node_modules
COPY package*.json ./
COPY lighthouserc.js ./
# Run as non-root user for security
RUN addgroup -g 1001 -S nodejs && \
adduser -S lighthouse -u 1001 && \
chown -R lighthouse:nodejs /app
USER lighthouse
# Run Lighthouse CI tests
CMD ["npm", "test"]
Step 1: Build the Docker Image
docker build -t lighthouse-ci-test .Expected output:
[+] Building 25.3s (15/15) FINISHED
=> exporting to image
=> => writing image sha256:c35cbd3762752cb...
=> => naming to docker.io/library/lighthouse-ci-testBuild time: ~20–30 seconds (first time)
Step 2: Run the Test
docker run --rm lighthouse-ci-testWhat happens:
- Container starts
- Lighthouse runs 3 times on www.pradappandiyan.com
- Results are aggregated (median values)
- Report is generated and uploaded
- Container automatically removes itself (
--rm)
Step 3: View Results
The test output shows:
✅ Configuration file found
✅ Chrome installation found
Healthcheck passed!Running Lighthouse 3 time(s) on https://www.pradappandiyan.com
Run #1...done.
Run #2...done.
Run #3...done.2 result(s) for https://www.pradappandiyan.com/ : ⚠️ categories.accessibility warning for minScore assertion
expected: >=0.8
found: 0.75 ⚠️ largest-contentful-paint warning for maxNumericValue assertion
expected: <=2500
found: 4039.77Open the report at https://storage.googleapis.com/...
Using NPM Scripts (Easier)
We’ve configured convenient npm scripts:
# Build the image
npm run docker:build
# Run the test
npm run docker:runAdvanced Usage
Save Results Locally
Mount a volume to persist results:
docker run --rm \
-v $(pwd)/.lighthouseci:/app/.lighthouseci \
lighthouse-ci-testResults are saved to .lighthouseci/ directory.
Custom Configuration
Edit lighthouserc.js to change test parameters:
module.exports = {
ci: {
collect: {
url: ['https://www.pradappandiyan.com'],
numberOfRuns: 3, // Change number of runs
settings: {
chromeFlags: '--no-sandbox --disable-dev-shm-usage --headless',
preset: 'desktop', // or 'mobile'
},
},
assert: {
assertions: {
'categories:performance': ['error', { minScore: 0.7 }],
'largest-contentful-paint': ['warn', { maxNumericValue: 2500 }],
// Add more assertions
},
},
},
};Test Multiple URLs
# Modify lighthouserc.js
url: [
'https://www.pradappandiyan.com',
'https://www.pradappandiyan.com/about',
'https://www.pradappandiyan.com/contact'
]Run in CI/CD
GitHub Actions Example:
name: Lighthouse CI
on: [push, pull_request]
jobs:
lighthouse:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Build Lighthouse Docker
run: docker build -t lighthouse-ci-test .
- name: Run Lighthouse Tests
run: docker run --rm lighthouse-ci-testGitLab CI Example:
lighthouse-test:
image: docker:latest
services:
- docker:dind
script:
- docker build -t lighthouse-ci-test .
- docker run --rm lighthouse-ci-test
only:
- main
- merge_requestsUnderstanding the Test Results
Performance Metrics Tested
MetricThresholdWhat It MeasuresPerformance Score≥70%Overall page performanceFirst Contentful Paint (FCP)<2000msTime to first visible contentLargest Contentful Paint (LCP)<2500msTime to main content loadTime to Interactive (TTI)❤500msWhen page becomes fully interactiveSpeed Index❤000msHow quickly content is visually displayedTotal Blocking Time (TBT)❤00msTime page is blocked from user inputCumulative Layout Shift (CLS)<0.1Visual stability (no jumping content)
Other Categories
- Accessibility: ≥80% — WCAG compliance
- Best Practices: ≥80% — Security and modern standards
- SEO: ≥80% — Search engine optimization
Reading the Report
Click the generated report URL to see:
- 📊 Detailed performance metrics
- 🎯 Specific recommendations
- 📈 Performance timeline
- 🔍 Network analysis
- 📱 Mobile vs Desktop comparison
Troubleshooting
Docker Build Fails
# Clear Docker cache and rebuild
docker system prune -a
docker build --no-cache -t lighthouse-ci-test .Out of Memory Errors
Increase Docker memory limit:
- Docker Desktop → Settings → Resources → Memory → 4GB
Network Timeout
Website might be slow or unreachable:
// Increase timeout in lighthouserc.js
settings: {
maxWaitForLoad: 60000, // 60 seconds
}Permission Denied
Ensure Docker daemon is running:
sudo systemctl start docker # Linux
# Or restart Docker Desktop (Mac/Windows)Performance Tips
Speed Up Builds
- Layer Caching: Don’t change
package.jsonunless necessary - Use Docker BuildKit:
DOCKER_BUILDKIT=1 docker build -t lighthouse-ci-test .- Pre-pull Base Image:
docker pull node:18-alpineReduce Test Time
// Run fewer iterations
numberOfRuns: 1, // Instead of 3
// Test only performance
categories: ['performance']Technical Deep Dive
Why Alpine Linux?
Alpine uses musl libc instead of glibc, and BusyBox instead of GNU utilities:
Size Comparison:
- Alpine: 5MB base
- Debian Slim: 124MB base
- Ubuntu: 77MB baseTrade-off: Some compatibility issues with native modules (rare)
Chromium vs Chrome
We use Chromium from Alpine repos:
- ✅ Optimized for Alpine
- ✅ Regularly updated
- ✅ Smaller than Google Chrome
- ✅ Same rendering engine (Blink)
Multi-Stage Build Efficiency
Traditional Build: Multi-Stage Build:
┌─────────────┐ ┌─────────────┐
│ Build │ │ Build │ ← Discarded
│ Tools │ │ Tools │
│ npm cache │ │ npm cache │
│ Source │ └─────────────┘
│ Runtime │ ↓
└─────────────┘ ┌─────────────┐
~1.5GB │ Runtime │
│ Only │
└─────────────┘
~913MBComparison: Could We Go Smaller?
Option 1: Remote Browser (50MB)
❌ Requires external Chrome service ❌ Network latency affects results ❌ Additional infrastructure
Option 2: Lighthouse API (10MB)
❌ Depends on Google’s servers ❌ Rate limiting ❌ No custom configurations
Option 3: Our Solution (913MB)
✅ Self-contained ✅ Consistent results ✅ Full control ✅ No external dependencies ✅ Best balance of size and functionality
Conclusion
Our Lighthouse CI Docker setup achieves an optimal balance:
- Small enough: ~913MB is lightweight for browser testing
- Fast enough: Builds in 20–30 seconds
- Complete enough: Full Lighthouse capabilities
- Secure enough: Non-root user, minimal attack surface
- Production-ready: Used in CI/CD pipelines
The ~650MB Chromium requirement is unavoidable for real browser testing. Any significantly smaller solution would sacrifice functionality or require external dependencies.
Resources
Built with ❤️ for performance testing
I have created a project on GitHub and added the code here.
Feel free to hit clap if you like the content. Happy Automation Testing :) Cheers. 👏 (www.pradappandiyan.com)
If you’d like to support my work, please leave a good rating for my Chrome plugin here and my Firefox plugin here.
I have created a test automation framework for API and UI for the QA community. If you found this helpful, leave a ⭐ on GitHub or try contributing to any feature.
