Skip to main content
mod_pagespeed 2.1 is here — open source (Apache-2.0): the module you know, plus the optimizer worker

Run ModPageSpeed 2.0 with Docker Compose

By Otto van der Schaaf

installation docker nginx

mod_pagespeed 2.1 ships as two cooperating containers. One is a worker that transcodes images and minifies assets; the other is an nginx interceptor that serves the optimized variants. Both processes share a single Cyclone cache volume via memory-mapped I/O. What follows is the shortest path from a fresh host to a working install.

Prerequisites

  • Docker 24+ with Compose v2 (docker compose, not docker-compose).
  • An origin (your existing app) reachable from the nginx container. By default the compose file assumes host.docker.internal:8081.

docker-compose.yml

This is the production compose file from the mod_pagespeed repo, trimmed for readability:

services:
  worker:
    image: ghcr.io/we-amp/pagespeed-worker:${PAGESPEED_VERSION:-2.0.21}
    volumes:
      - pagespeed-data:/data
    environment:
      - CACHE_SIZE=${CACHE_SIZE:-1073741824} # 1 GiB
    restart: unless-stopped
    healthcheck:
      test:
        ['CMD-SHELL', "echo '' | socat - UNIX-CONNECT:/data/pagespeed.sock.health | grep -q '^OK'"]
      interval: 10s
      timeout: 5s
      retries: 3
      start_period: 5s

  nginx:
    image: ghcr.io/we-amp/pagespeed-nginx:${PAGESPEED_VERSION:-2.0.21}
    ports:
      - '${NGINX_PORT:-80}:80'
      - '${NGINX_SSL_PORT:-443}:443'
    volumes:
      - pagespeed-data:/data
      # Custom config: - ./nginx.conf:/etc/nginx/nginx.conf:ro
      # TLS:           - ./ssl/cert.pem:/etc/nginx/ssl/cert.pem:ro
      #                - ./ssl/key.pem:/etc/nginx/ssl/key.pem:ro
    environment:
      - BACKEND_HOST=${BACKEND_HOST:-host.docker.internal}
      - BACKEND_PORT=${BACKEND_PORT:-8081}
    extra_hosts:
      - 'host.docker.internal:host-gateway'
    depends_on:
      worker:
        condition: service_healthy
    restart: unless-stopped
    healthcheck:
      test: ['CMD', 'curl', '-sf', 'http://localhost/health']
      interval: 10s
      timeout: 5s
      retries: 3
      start_period: 3s

volumes:
  pagespeed-data:
    driver: local

Note:

  • The shared named volume pagespeed-data is mounted at /data in both containers. That is where the Cyclone cache file lives. Both processes open it via mmap, so writes from the worker show up in the nginx interceptor’s address space immediately. No IPC, no socket round-trips for cache reads.
  • extra_hosts: host.docker.internal:host-gateway is what lets nginx in Docker reach an origin on the host on Linux. macOS and Windows Docker resolve this hostname by default; the explicit mapping is for Linux parity.

.env

Drop a .env next to the compose file:

# Origin server (your app)
BACKEND_HOST=host.docker.internal
BACKEND_PORT=8081

# Cache size in bytes (1 GiB by default)
CACHE_SIZE=1073741824

# Image tag (pin in production)
PAGESPEED_VERSION=latest

Start it

$ docker compose up -d
[+] Running 3/3
 ✔ Network modpagespeed_default  Created
 ✔ Container modpagespeed-worker-1  Healthy
 ✔ Container modpagespeed-nginx-1   Started

$ docker compose ps
NAME                       STATUS                    PORTS
modpagespeed-nginx-1       Up 12s (healthy)          0.0.0.0:80->80/tcp, 0.0.0.0:443->443/tcp
modpagespeed-worker-1      Up 22s (healthy)

The depends_on: condition: service_healthy clause means nginx will not start until the worker reports OK on its Unix-domain health socket. If nginx never starts, check docker compose logs worker first.

Verify

The fastest check is the response header. mod_pagespeed emits X-PageSpeed: on optimized responses (the 1.x lineage uses X-Mod-Pagespeed:, same project family, different header):

$ curl -sI http://localhost/ | grep -i pagespeed
x-pagespeed: HIT

For more diagnostic depth, hit the nginx health endpoint and read the worker socket:

$ curl -s http://localhost/health
OK

$ docker compose exec worker socat - UNIX-CONNECT:/data/pagespeed.sock.health
OK 0/128

0/128 is queue_depth/queue_capacity. A worker that is constantly at 128/128 is bottlenecked on transcoding, usually a sign the cache volume is too small and variants are being evicted faster than they’re being generated.

Custom nginx.conf

The default nginx.conf baked into the image proxies everything to $BACKEND_HOST:$BACKEND_PORT. To override it (multiple vhosts, TLS, more specific pagespeed_disallow rules), mount your own:

services:
  nginx:
    volumes:
      - pagespeed-data:/data
      - ./nginx.conf:/etc/nginx/nginx.conf:ro

A minimal nginx.conf with PageSpeed enabled:

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

events { worker_connections 1024; }

http {
    include /etc/nginx/mime.types;

    upstream backend {
        server host.docker.internal:8081;
        keepalive 32;
    }

    server {
        listen 80;

        pagespeed on;
        pagespeed_cache_path /data/cache.vol;
        pagespeed_enable_mmap_directory on;

        # Don't optimize these
        pagespeed_disallow /api/;
        pagespeed_disallow /admin/;
        pagespeed_disallow "*.woff2";

        location / {
            proxy_pass http://backend;
            proxy_set_header Host $host;
            proxy_set_header Accept-Encoding "";  # PageSpeed needs raw bytes
            proxy_http_version 1.1;
        }

        location /health {
            access_log off;
            return 200 "OK\n";
        }
    }
}

Two non-obvious config items worth flagging:

  • proxy_set_header Accept-Encoding ""; (the origin must return uncompressed bytes). PageSpeed caches raw HTML/CSS/JS and applies brotli/gzip at serve time. If your origin gzips eagerly, PageSpeed sees compressed bytes and the HTML pipeline silently skips.
  • pagespeed_enable_mmap_directory on; (required for shared-cache mode). Without it, the nginx interceptor falls back to socket round-trips for every read, which is functional but slow.

Production checklist

Before pointing real traffic at this:

  • Pin the image tag (ghcr.io/we-amp/pagespeed-worker:2.0.21, not :latest).
  • Set CACHE_SIZE to ~10% of the variants you expect (rule of thumb: 1 GiB per 5000 unique image URLs).
  • Add a logging driver. json-file with rotation is the compose-file default but consider shipping to your aggregator.
  • Set up TLS termination. The compose file leaves it off; mount your certs and uncomment the ssl_* directives in nginx.conf.
  • Wire /health into your uptime monitor.
  • Consider the full installation walkthrough for production deploy detail.

What next

Like this kind of writeup?

We write about how mod_pagespeed actually works, and what we learn shipping it. Get the next post by email.

Read next