Skip to main content
ModPageSpeed 2.0: AVIF, WebP, and critical CSS — up to 69% less page weight on the live demo
2.0 1.1

Install with Docker

Deploy ModPageSpeed 2.0 using Docker containers.

On this page

This guide walks you through deploying ModPageSpeed 2.0 with Docker Compose. You’ll run three containers (nginx with the pagespeed module, the worker, and your origin server) sharing a Cyclone cache volume.

Quick try (one container)

The fastest way to see ModPageSpeed against your own site is the combined image, which runs the worker and nginx together in a single container. Point it at your origin and publish port 80:

docker run --rm -p 80:80 \
  -e BACKEND_HOST=host.docker.internal -e BACKEND_PORT=8081 \
  -e ACCEPT_EULA=Y \
  ghcr.io/we-amp/pagespeed-combined:latest

Replace BACKEND_HOST/BACKEND_PORT with your origin. The combined image suits evaluation and small single-host deployments; for production, run the worker and nginx as separate services (below) so you can scale and update them independently. :latest is published only on the combined image — the worker and nginx images ship immutable version tags (for example :2.0.29).

ACCEPT_EULA=Y acknowledges the Terms of Service, which govern your use of the image whether licensed or running unlicensed to evaluate. The container prints the terms URL on startup if you omit the flag.

Prerequisites

  • Docker Engine 20.10+
  • Docker Compose v2
  • A ModPageSpeed license key for production use. You can install and run unlicensed to evaluate — the module fully optimizes and adds an X-PageSpeed-Warn: unlicensed response header. When you’re ready for production, buy a license. Licensing is per site (registrable domain): every container and replica serving the same site is covered by one license.

Directory Structure

Create a project directory with the following layout:

modpagespeed/
├── docker-compose.yml
├── nginx.conf
└── entrypoint-worker.sh

Docker Compose Configuration

Create docker-compose.yml:

services:
  # Your origin server — replace with your actual upstream
  origin:
    image: nginx:stable
    volumes:
      - ./your-site:/usr/share/nginx/html:ro
    expose:
      - '8081'

  # Factory Worker — optimizes cached content
  worker:
    image: ghcr.io/we-amp/pagespeed-worker:2.0.29
    entrypoint: /entrypoint-worker.sh
    environment:
      # Acknowledges the Terms of Service: https://modpagespeed.com/terms/
      ACCEPT_EULA: 'Y'
    volumes:
      - shared:/shared
      - ./entrypoint-worker.sh:/entrypoint-worker.sh:ro
    depends_on:
      - origin

  # Nginx with PageSpeed module
  nginx:
    image: ghcr.io/we-amp/pagespeed-nginx:2.0.29
    ports:
      - '8080:8080'
    volumes:
      - shared:/shared
      - ./nginx.conf:/etc/nginx/nginx.conf:ro
    depends_on:
      - worker

volumes:
  shared:

The shared volume is where the Cyclone cache file and Unix socket live. Both the nginx and worker containers mount it at /shared.

Nginx Configuration

Create nginx.conf:

worker_processes auto;
error_log /var/log/nginx/error.log warn;
pid /var/run/nginx.pid;

load_module /usr/lib/nginx/modules/ngx_pagespeed_module.so;

events {
    worker_connections 1024;
}

http {
    include       /etc/nginx/mime.types;
    default_type  application/octet-stream;
    access_log    /var/log/nginx/access.log;

    server {
        listen 8080;
        server_name _;

        # Enable PageSpeed
        pagespeed on;
        pagespeed_cache_path /shared/cache.vol;
        # Worker socket path is read from pagespeed-shared.conf automatically

        location / {
            proxy_pass http://origin:8081;
            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;
        }
    }
}

The two pagespeed_* directives are all you need:

  • pagespeed on — enables the module for this server block
  • pagespeed_cache_path — path to the shared Cyclone cache file

The worker socket path, license key, and other shared settings are read automatically from pagespeed-shared.conf, which the worker writes next to the cache file.

Worker Entrypoint

Create entrypoint-worker.sh and make it executable (chmod +x):

#!/bin/bash
set -e

# Ensure shared directory is accessible by both nginx (nobody) and worker (root)
chmod 777 /shared
umask 0000

# Pre-create cache file with open permissions
touch /shared/cache.vol
chmod 666 /shared/cache.vol

exec factory_worker \
  --socket /shared/pagespeed.sock \
  --cache-path /shared/cache.vol

The permission setup is important: nginx worker processes run as nobody while the worker runs as root. Both need read/write access to the cache file and Unix socket.

Start the Stack

docker compose up -d

Check that all three containers are running:

docker compose ps

You should see origin, worker, and nginx all in a running state.

Verify It Works

Test with a simple request:

# First request — cache miss, proxied to origin
curl -I http://localhost:8080/

Look for the X-PageSpeed: MISS header. This means the module is active and the response was proxied to your origin and cached.

# Second request — cache hit, served from cache
curl -I http://localhost:8080/

You should now see X-PageSpeed: HIT.

View Logs

# All services
docker compose logs -f

# Just the worker
docker compose logs -f worker

# Just nginx
docker compose logs -f nginx

The worker logs show optimization activity — you’ll see messages when it processes images, CSS, and JavaScript files.

Cache Size

By default, the cache size is 1 GB. To increase it, pass the --cache-size flag to the worker (in bytes):

exec factory_worker \
  --socket /shared/pagespeed.sock \
  --cache-path /shared/cache.vol \
  --cache-size 536870912  # 512 MB

Stopping and Restarting

# Stop all containers
docker compose down

# Stop and remove the cache volume (fresh start)
docker compose down -v

The cache is stored in a named Docker volume. Stopping containers preserves the cache — optimized content is still available on restart. Use down -v only if you want to clear the cache completely.

Kubernetes

For Kubernetes deployments, run nginx and the worker as separate containers in the same pod, sharing an emptyDir volume:

apiVersion: v1
kind: Pod
metadata:
  name: pagespeed
spec:
  containers:
    - name: nginx
      image: ghcr.io/we-amp/pagespeed-nginx:2.0.29
      ports:
        - containerPort: 8080
      volumeMounts:
        - name: shared
          mountPath: /shared

    - name: worker
      image: ghcr.io/we-amp/pagespeed-worker:2.0.29
      command: ['/entrypoint-worker.sh']
      env:
        # Acknowledges the Terms of Service: https://modpagespeed.com/terms/
        - name: ACCEPT_EULA
          value: 'Y'
      volumeMounts:
        - name: shared
          mountPath: /shared

  volumes:
    - name: shared
      emptyDir:
        sizeLimit: 512Mi

Both containers in the same pod share the same network namespace, so the Unix socket is accessible without additional configuration.

Verifying the images

The images are signed with keyless cosign and carry an SBOM attestation and SLSA build provenance. The signing identity is the We-Amp/modpagespeed-images publish workflow. To verify a pull:

# Signature (keyless — no public key to manage)
cosign verify \
  --certificate-identity-regexp '^https://github.com/We-Amp/modpagespeed-images/' \
  --certificate-oidc-issuer https://token.actions.githubusercontent.com \
  ghcr.io/we-amp/pagespeed-combined:latest

# SBOM + build provenance
gh attestation verify oci://ghcr.io/we-amp/pagespeed-combined:latest \
  --repo We-Amp/modpagespeed-images

The same commands work for pagespeed-worker and pagespeed-nginx (use a pinned tag such as :2.0.29).

Troubleshooting

No X-PageSpeed header: Check that pagespeed on; is set in your nginx config and the module is loaded. Verify with docker compose logs nginx.

X-PageSpeed: MISS on every request: The cache file may not be shared correctly. Ensure both containers mount the same volume at /shared and that permissions are set (cache file 666, socket world-writable).

Worker not processing content: Check worker logs with docker compose logs worker. Verify the worker is writing pagespeed-shared.conf next to the cache file (nginx reads the socket path from this file automatically).

Next Steps