LHCI Server: Self-Host Lighthouse CI Dashboard

Deploy your own Lighthouse CI server for historical tracking, build comparisons, and team dashboards. Covers Docker, Heroku, and database setup.
Harlan WiltonHarlan Wilton Published

LHCI Server provides a hosted dashboard for your Lighthouse CI results. While the CLI can run locally and output to the console, the server component gives you persistent historical data, trend analysis, and a web UI for comparing builds over time.

What LHCI Server Provides

Historical Tracking

  • Store Lighthouse reports across all builds
  • Track performance trends over weeks and months
  • Compare any two builds side-by-side
  • View metric changes across deployments

Team Dashboards

  • Centralized view of all projects
  • Multiple projects per server instance
  • Per-project build tokens for security
  • Web UI accessible to entire team

Build Comparison

  • Visual diff between baseline and current build
  • Identify regressions before they ship
  • Track improvement over time
  • Export comparison data

Requirements

Runtime

  • Node.js 18+ (20+ recommended)
  • Database: SQLite, MySQL, or PostgreSQL
  • 512MB RAM minimum (1GB+ recommended)
  • Storage scales with report retention

Network

  • HTTP server accessible to CI runners
  • Optional: reverse proxy for HTTPS
  • Optional: basic auth for protection

Local Deployment

Quickest way to test LHCI Server locally.

Using npx

# SQLite database (easiest for testing)
npx @lhci/server --storage.sqlDatabasePath=./lhci.db

# Custom port
npx @lhci/server --port=9001 --storage.sqlDatabasePath=./lhci.db

Using npm

# Install globally
npm install -g @lhci/server

# Run with SQLite
lhci server --storage.sqlDatabasePath=./lhci.db

# Run with MySQL
lhci server \
  --storage.sqlDialect=mysql \
  --storage.sqlConnectionUrl=mysql://user:pass@localhost/lhci

Server runs on http://localhost:9001 by default.

Docker Deployment

Pre-built Docker image makes deployment straightforward.

Docker Run

# SQLite (data persists in volume)
docker run -d \
  -p 9001:9001 \
  -v lhci-data:/data \
  --name lhci-server \
  patrickhulce/lhci-server

# With custom port
docker run -d \
  -p 8080:9001 \
  -v lhci-data:/data \
  --name lhci-server \
  patrickhulce/lhci-server

Docker Compose

version: '3'
services:
  lhci-server:
    image: patrickhulce/lhci-server
    ports:
      - '9001:9001'
    volumes:
      - lhci-data:/data
    environment:
      - LHCI_STORAGE__SQL_DATABASE_PATH=/data/lhci.db
    restart: unless-stopped

volumes:
  lhci-data:

With PostgreSQL

version: '3'
services:
  lhci-server:
    image: patrickhulce/lhci-server
    ports:
      - '9001:9001'
    environment:
      - LHCI_STORAGE__SQL_DIALECT=postgres
      - LHCI_STORAGE__SQL_CONNECTION_URL=postgres://lhci:password@postgres:5432/lhci
    depends_on:
      - postgres
    restart: unless-stopped

  postgres:
    image: postgres:15-alpine
    environment:
      - POSTGRES_USER=lhci
      - POSTGRES_PASSWORD=password
      - POSTGRES_DB=lhci
    volumes:
      - postgres-data:/var/lib/postgresql/data
    restart: unless-stopped

volumes:
  postgres-data:

Heroku Deployment

Heroku provides managed hosting with automatic HTTPS. Note that Heroku discontinued its free tier in November 2022—all deployments now require paid plans.

One-Click Deploy

Use the official Heroku button:

https://heroku.com/deploy?template=https://github.com/GoogleChrome/lighthouse-ci/tree/main/docs/recipes/heroku-server

Manual Deploy

# Install Heroku CLI
npm install -g heroku

# Create app
heroku create your-lhci-server

# Add PostgreSQL (Essential tier, starts at $5/month)
heroku addons:create heroku-postgresql:essential-0

# Set buildpack
heroku buildpacks:set heroku/nodejs

# Deploy
git clone https://github.com/GoogleChrome/lighthouse-ci.git
cd lighthouse-ci
git subtree push --prefix packages/server heroku main

# Open dashboard
heroku open

Heroku automatically sets DATABASE_URL for PostgreSQL connection.

Database Configuration

LHCI Server supports three database backends.

SQLite (Default)

Best for single-server deployments, testing, and low traffic. Works well for small teams but gets bigger and slower over time.

lhci server --storage.sqlDatabasePath=/data/lhci.db

Environment variable:

LHCI_STORAGE__SQL_DATABASE_PATH=/data/lhci.db
SQLite limitations: Not optimized for high write volumes. Azure storage file shares are incompatible with SQLite — use PostgreSQL for cloud deployments.

MySQL

Better for multi-server deployments and higher traffic.

lhci server \
  --storage.sqlDialect=mysql \
  --storage.sqlConnectionUrl=mysql://user:password@host:3306/lhci

Environment variables:

LHCI_STORAGE__SQL_DIALECT=mysql
LHCI_STORAGE__SQL_CONNECTION_URL=mysql://user:password@host:3306/lhci

PostgreSQL

Recommended for production deployments. Better query performance and handles concurrent writes from multiple CI pipelines.

lhci server \
  --storage.sqlDialect=postgres \
  --storage.sqlConnectionUrl=postgres://user:password@host:5432/lhci

Environment variables:

LHCI_STORAGE__SQL_DIALECT=postgres
LHCI_STORAGE__SQL_CONNECTION_URL=postgres://user:password@host:5432/lhci

Connection string format:

postgres://username:password@hostname:port/database?ssl=true

Creating Projects

Use the wizard to create projects and generate tokens.

Interactive Wizard

lhci wizard

Follow prompts:

  1. Enter server URL (e.g., https://lhci.yourdomain.com)
  2. Create new project or select existing
  3. Project name (e.g., "Marketing Site")
  4. Receive build token and admin token

Output Example

Created project Marketing Site (id: abc123)!

Build Token: def456789...
Admin Token: ghi012345...

Use build token in your CI configuration:
LHCI_TOKEN=def456789...

Manual Project Creation

Via API after server is running:

curl -X POST https://lhci.yourdomain.com/v1/projects \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Marketing Site",
    "externalUrl": "https://marketing.example.com",
    "slug": "marketing-site"
  }'

Returns project ID and tokens in JSON response.

Token Security Model

LHCI uses two types of tokens with different permissions.

Build Token

  • Used by CI to upload Lighthouse reports
  • Can only write data, cannot read
  • Safe to use in CI environment variables
  • One per project

Example in GitHub Actions:

env:
  LHCI_TOKEN: ${{ secrets.LHCI_BUILD_TOKEN }}

Admin Token

  • Full read/write access to project
  • Can create/delete builds
  • Manage project settings
  • Keep secure, never commit

Use admin token sparingly:

# Delete old builds (use admin token)
curl -X DELETE https://lhci.yourdomain.com/v1/projects/abc123/builds/789 \
  -H "Authorization: Bearer $ADMIN_TOKEN"

Store tokens in:

  • CI secrets (GitHub Secrets, GitLab Variables)
  • Environment variables
  • Secret management tools (Vault, AWS Secrets Manager)

Never hardcode tokens in lighthouserc.js.

Configuring Upload Target

Point your CI configuration to your LHCI Server.

lighthouserc.js

module.exports = {
  ci: {
    collect: {
      url: ['http://localhost:3000'],
      numberOfRuns: 3
    },
    upload: {
      target: 'lhci',
      serverBaseUrl: 'https://lhci.yourdomain.com',
      token: process.env.LHCI_TOKEN // Build token from env
    },
    assert: {
      preset: 'lighthouse:recommended',
      assertions: {
        'categories:performance': ['error', { minScore: 0.9 }]
      }
    }
  }
}

Upload Options

upload: {
  target: 'lhci',
  serverBaseUrl: 'https://lhci.yourdomain.com',
  token: process.env.LHCI_TOKEN,

  // Optional: GitHub App integration
  githubAppToken: process.env.LHCI_GITHUB_APP_TOKEN,

  // Optional: ignore HTTPS certificate errors (not recommended)
  ignoreDuplicateBuildFailure: true
}

See configuration guide for all upload options.

Verify Upload

After CI runs:

lhci upload \
  --serverBaseUrl=https://lhci.yourdomain.com \
  --token=$LHCI_TOKEN

Check logs for:

Saving CI project Marketing Site (abc123)
Saved LHR to https://lhci.yourdomain.com (build: def456)
Done saving build results to Lighthouse CI

Basic Auth Protection

Add basic authentication to protect your dashboard.

Environment Variables

LHCI_BASIC_AUTH__USERNAME=admin
LHCI_BASIC_AUTH__PASSWORD=secure-password-here

Docker Compose

services:
  lhci-server:
    image: patrickhulce/lhci-server
    environment:
      - LHCI_BASIC_AUTH__USERNAME=admin
      - LHCI_BASIC_AUTH__PASSWORD=${LHCI_PASSWORD}

CLI Arguments

lhci server \
  --basicAuth.username=admin \
  --basicAuth.password=secure-password \
  --storage.sqlDatabasePath=./lhci.db

Basic auth applies to web UI only. API token authentication still works for uploads.

Uploading with Basic Auth

If your server has basic auth, CI uploads use token authentication and bypass basic auth automatically. No additional configuration needed.

Reverse Proxy Setup

Run LHCI Server behind nginx or Caddy for HTTPS.

Nginx

server {
  listen 443 ssl http2;
  server_name lhci.yourdomain.com;

  ssl_certificate /path/to/cert.pem;
  ssl_certificate_key /path/to/key.pem;

  location / {
    proxy_pass http://localhost:9001;
    proxy_http_version 1.1;
    proxy_set_header Upgrade $http_upgrade;
    proxy_set_header Connection 'upgrade';
    proxy_set_header Host $host;
    proxy_set_header X-Real-IP $remote_addr;
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    proxy_set_header X-Forwarded-Proto $scheme;
    proxy_cache_bypass $http_upgrade;
  }
}

Caddy

lhci.yourdomain.com {
  reverse_proxy localhost:9001
}

Caddy handles HTTPS automatically with Let's Encrypt.

Docker with Traefik

services:
  lhci-server:
    image: patrickhulce/lhci-server
    labels:
      - traefik.enable=true
      - traefik.http.routers.lhci.rule=Host(`lhci.yourdomain.com`)
      - traefik.http.routers.lhci.entrypoints=websecure
      - traefik.http.routers.lhci.tls.certresolver=letsencrypt

Storage Considerations

LHCI Server storage grows with report retention.

Storage Estimates

Per build (3 runs):

  • Raw Lighthouse reports: ~2-3 MB
  • Screenshots: ~500 KB - 1 MB
  • Metadata: ~50 KB

Example: 100 builds/month = ~300-400 MB/month

Database Maintenance

SQLite:

# Vacuum database to reclaim space
sqlite3 /data/lhci.db "VACUUM;"

PostgreSQL:

-- Vacuum and analyze
VACUUM ANALYZE;

Manual Cleanup

Delete old builds via API:

# List builds
curl https://lhci.yourdomain.com/v1/projects/abc123/builds \
  -H "Authorization: Bearer $ADMIN_TOKEN"

# Delete specific build
curl -X DELETE https://lhci.yourdomain.com/v1/projects/abc123/builds/789 \
  -H "Authorization: Bearer $ADMIN_TOKEN"

Retention Policy

Industry standard for performance monitoring: raw metrics for 30 days, aggregated data for 13 months. Disk space is a common issue — plan for automated cleanup.

Set up automated cleanup with cron:

#!/bin/bash
# cleanup-old-builds.sh
# Delete builds older than 90 days

ADMIN_TOKEN="your-admin-token"
SERVER="https://lhci.yourdomain.com"
PROJECT_ID="abc123"
CUTOFF_DATE=$(date -d '90 days ago' +%s)

# Fetch builds and filter by date
curl "$SERVER/v1/projects/$PROJECT_ID/builds" \
  -H "Authorization: Bearer $ADMIN_TOKEN" | \
  jq -r ".[] | select(.createdAt < $CUTOFF_DATE) | .id" | \
  while read BUILD_ID; do
    curl -X DELETE "$SERVER/v1/projects/$PROJECT_ID/builds/$BUILD_ID" \
      -H "Authorization: Bearer $ADMIN_TOKEN"
    echo "Deleted build $BUILD_ID"
  done

Cron entry:

0 2 * * 0 /path/to/cleanup-old-builds.sh

Updating the Server

Keep LHCI Server up to date for bug fixes and features.

Docker

# Pull latest image
docker pull patrickhulce/lhci-server:latest

# Restart container
docker compose down
docker compose up -d

npm/npx

# Update global installation
npm update -g @lhci/server

# Or update in package.json
npm install @lhci/server@latest

Heroku

# Pull latest code
git pull origin main

# Deploy
git push heroku main

Database Migrations

LHCI Server handles schema migrations automatically on startup. Backup your database before major version upgrades:

# SQLite backup
cp /data/lhci.db /data/lhci.db.backup

# PostgreSQL backup
pg_dump lhci > lhci-backup.sql

Production Checklist

Before deploying to production:

  • Use PostgreSQL or MySQL (not SQLite for multi-server)
  • Enable basic auth or place behind VPN
  • Set up HTTPS via reverse proxy
  • Configure database backups
  • Store tokens in secrets manager
  • Set up monitoring (uptime, disk space)
  • Plan retention policy and cleanup automation
  • Document server URL and project IDs for team
  • Test upload from CI environment
  • Configure GitHub App integration (optional)

Common Deployment Issues

Database Connection Fails

Check connection string format:

# Wrong
postgres://localhost/lhci

# Right
postgres://username:password@localhost:5432/lhci

Uploads Timeout

Increase client timeout in lighthouserc.js:

upload: {
  target: 'lhci',
  serverBaseUrl: 'https://lhci.yourdomain.com',
  token: process.env.LHCI_TOKEN,
  uploadUrlTimeout: 120000 // 2 minutes
}

Server Crashes on Start

Check Node version (16+ required):

node --version

Verify database permissions and disk space.

Port Already in Use

Change port:

lhci server --port=9002

Or find and kill process on port 9001:

lsof -ti:9001 | xargs kill -9

Next Steps

Resources