Run ModPageSpeed 2.0 with Docker Compose
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, notdocker-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-datais mounted at/datain both containers. That is where the Cyclone cache file lives. Both processes open it viammap, 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-gatewayis 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_SIZEto ~10% of the variants you expect (rule of thumb: 1 GiB per 5000 unique image URLs). - Add a logging driver.
json-filewith 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 innginx.conf. - Wire
/healthinto your uptime monitor. - Consider the full installation walkthrough for production deploy detail.
What next
- Install & run: it optimizes out of the box, and it is licensed under Apache-2.0 and free to run in development and in production — see the license.
- Getting started — full installation overview including non-Docker paths.
- Installation: Docker — production-grade Docker setup with volume sizing and tuning.
- ASP.NET Core middleware if you’d rather embed the optimizer than run nginx as a reverse proxy.
- Critical CSS without a headless browser for how the worker decides which CSS to inline.
- mod_pagespeed alternatives — short form on what changed between 1.x and 2.0.
- vs imgproxy — comparison if you’re evaluating image pipelines.
Read next
-
Pay-per-crawl at the origin: an experimental RSL-CAP access gate
mod_pagespeed 2.1 ships an experimental, off-by-default RSL-CAP gate for nginx: validate Authorization: License capability tokens and refuse unauthorized AI-crawler access at your origin. Preview, not GA.
-
Web Bot Auth: verify AI crawlers at your origin
mod_pagespeed 2.1 ships an nginx Web Bot Auth verifier that checks RFC 9421 signatures and labels each request in $x_verified_bot. Observe-only, default off.
-
Server-injected resource hints: Speculation Rules and preconnect from real traffic
How a proxy generates server-injected resource hints: speculation-rules prefetch derived from real traffic, priority-bucketed preconnect, font preload.