sidekick

package module
v0.0.7 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Dec 23, 2025 License: MIT Imports: 37 Imported by: 0

README

Caddy Sidekick

Lightning-fast server side caching Caddy module for PHP applications, with an emphasis on WordPress

Features

Performance Optimizations
  • Buffer Pooling: Uses sync.Pool for efficient memory management
  • Automatic Compression: Stores compressed versions (gzip, brotli, zstd) when beneficial
  • Streaming to Disk: Large responses stream directly to disk instead of buffering in memory
  • 304 Not Modified Support: Handles conditional requests with ETag and Last-Modified headers
  • Pre-compiled Regex: Patterns compiled once during initialization
Advanced Cache Management
  • Configurable Cache Keys: Include query parameters, headers, and cookies in cache key generation
  • Size Management: Fine-grained control over memory usage and cache limits
  • Selective Caching: Path prefixes, regex patterns, and response codes
  • Purge API: Secure cache invalidation endpoint
WordPress Integration
  • Automatic mu-plugin Deployment: Manages WordPress must-use plugins for cache purging and URL rewriting
  • Checksum Verification: Ensures mu-plugin integrity with SHA-256 checksums
  • Smart Directory Management: Creates directories as needed with parent directory validation

Installation

Building with xcaddy
xcaddy build --with github.com/honest-hosting/caddy-sidekick

Configuration

Environment Variables

All environment variables use the SIDEKICK_ prefix for namespace isolation:

Environment Variable Description Default
SIDEKICK_CACHE_DIR Cache storage directory /var/www/html/wp-content/cache
SIDEKICK_METRICS Enable metrics and set admin API path (e.g., /metrics/sidekick) (disabled)
SIDEKICK_CACHE_RESPONSE_CODES HTTP status codes to cache (comma-separated) 2XX,301,302
SIDEKICK_NOCACHE Path prefixes to bypass cache (comma-separated, uses prefix matching) /wp-admin,/wp-json
SIDEKICK_NOCACHE_HOME Skip caching home page false
SIDEKICK_NOCACHE_REGEX Regex pattern for paths to bypass \.(jpg|jpeg|png|gif|ico|css|js|svg|woff|woff2|ttf|eot|otf|mp4|webm|mp3|ogg|wav|pdf|zip|tar|gz|7z|exe|doc|docx|xls|xlsx|ppt|pptx)$
SIDEKICK_CACHE_TTL Cache time-to-live in seconds 300
SIDEKICK_PURGE_HEADER HTTP header name for purge token X-Sidekick-Purge
SIDEKICK_PURGE_PATH API endpoint for cache purging (absolute path, only a-z0-9-_/ allowed) /__sidekick/purge
SIDEKICK_PURGE_URL Optional custom URL for cache purging (e.g., https://api.example.com) (empty)
SIDEKICK_PURGE_TOKEN Secret token for purge authentication (required when cache is enabled) dead-beef
SIDEKICK_CACHE_MEMORY_ITEM_MAX_SIZE Max size for single item in memory (e.g., 4MB, 0 = disabled, -1 = unlimited) 4MB
SIDEKICK_CACHE_MEMORY_MAX_SIZE Total memory cache size limit (e.g., 128MB, 0 = disabled, -1 = unlimited) 128MB
SIDEKICK_CACHE_MEMORY_MAX_PERCENT Memory cache as % of RAM (1-100, 0 = disabled, -1 = unlimited). Mutually exclusive with SIDEKICK_CACHE_MEMORY_MAX_SIZE (none)
SIDEKICK_CACHE_MEMORY_MAX_COUNT Max number of items in memory cache 32768
SIDEKICK_CACHE_MEMORY_STREAM_TO_DISK_SIZE Size threshold for streaming to disk (e.g., 10MB, 0 = disabled) 10MB
SIDEKICK_CACHE_DISK_ITEM_MAX_SIZE Max size for any cached item on disk (e.g., 100MB, 0 = disabled, -1 = unlimited) 100MB
SIDEKICK_CACHE_DISK_MAX_SIZE Total disk cache size limit (e.g., 10GB, 0 = disabled, -1 = unlimited) 10GB
SIDEKICK_CACHE_DISK_MAX_PERCENT Disk cache as % of available space (1-100, 0 = disabled, -1 = unlimited). Mutually exclusive with SIDEKICK_CACHE_DISK_MAX_SIZE (none)
SIDEKICK_CACHE_DISK_MAX_COUNT Max number of items in disk cache (-1 = unlimited, 0 = disabled) 100000
SIDEKICK_CACHE_KEY_HEADERS Headers to include in cache key (comma-separated) Accept-Encoding
SIDEKICK_CACHE_KEY_QUERIES Query parameters to include in cache key (comma-separated, use * for all) p,page,paged,s,category,tag,author
SIDEKICK_CACHE_KEY_COOKIES Cookies to include in cache key (comma-separated, supports wildcards with *) wordpress_logged_in_*,wordpress_sec_*,wp-settings-*
SIDEKICK_WP_MU_PLUGIN_ENABLED Enable automatic WordPress mu-plugin management true
SIDEKICK_WP_MU_PLUGIN_DIR Directory for WordPress mu-plugins /var/www/html/wp-content/mu-plugins

Note: When either memory or disk cache is enabled, all purge-related options (SIDEKICK_PURGE_HEADER, SIDEKICK_PURGE_PATH, SIDEKICK_PURGE_TOKEN) are required to be set. The SIDEKICK_PURGE_URL is optional and only needed if you want to use a custom URL for cache purging instead of the WordPress site URL.

Quick Start

Minimal configuration for a WordPress site:

{
    order sidekick before rewrite
}

example.com {
    sidekick {
        # Optional: Enable metrics collection (disabled by default to save resources)
        # metrics /metrics/sidekick
        
        cache_dir /var/www/cache
        cache_ttl 3600
        
        purge_path /__sidekick/purge
        purge_header X-Sidekick-Purge
        purge_token "change-this-secret"
    }
    
    root * /var/www/html
    php_server
    file_server
}
Complete Caddyfile Example

Full configuration with all options for a production WordPress site:

{
    # Global options
    # Enable admin API for metrics (comment out to disable)
    admin localhost:2019
    
    # FrankenPHP configuration
    frankenphp
    
    # Module ordering
    order php_server before file_server
    order php before file_server
    order sidekick before rewrite
    order request_header before sidekick
}

example.com {
    # Enable Sidekick caching
    sidekick {
        # Enable metrics collection and expose on admin API
        # Metrics will be available at:
        # - Admin API: http://localhost:2019/metrics/sidekick (detailed sidekick metrics)
        # - Caddy metrics: http://localhost:2019/metrics (includes sidekick metrics in Prometheus format)
        metrics /metrics/sidekick
        
        # Cache storage location
        cache_dir /var/www/cache
        
        # Cache TTL in seconds (default: 300)
        cache_ttl 3600
        
        # HTTP status codes to cache
        cache_response_codes 200 301 302
        
        # Paths to bypass cache (uses prefix matching, so /wp-admin matches /wp-admin/*)
        nocache /wp-admin /wp-json /wp-login.php
        
        # Don't cache home page (optional)
        nocache_home false
        
        # Regex for file types to bypass
        # Exclude large media files from cache
        nocache_regex "\\.(mp4|webm|mp3|ogg|wav|pdf|zip|tar|gz|7z|exe)$"
        
        # Purge endpoint configuration (required when cache is enabled)
        purge_path /__sidekick/purge
        purge_header X-Sidekick-Purge
        purge_token "your-secret-token-here"  # CHANGE THIS!
        
        # Optional: Use custom URL for cache purging (e.g., when behind a proxy)
        # purge_url https://api.example.com
        
        # Memory cache limits
        cache_memory_item_max_size 4MB
        cache_memory_max_size 128MB
        cache_memory_max_count 32768
        cache_memory_stream_to_disk_size 10MB
        
        # Disk cache limits
        cache_disk_item_max_size 100MB
        cache_disk_max_size 10GB
        cache_disk_max_count 100000
        
        # Cache key customization (defaults shown below if omitted)
        # Note: Set to "" (empty string) to disable, but this is not recommended
        cache_key_queries page sort filter     # Default: p,page,paged,s,category,tag,author
        cache_key_headers Accept-Language      # Default: Accept-Encoding
        cache_key_cookies session_id user_*    # Default: wordpress_logged_in_*,wordpress_sec_*,wp-settings-*
                                                # Supports wildcards with * for prefix matching
        
        # WordPress mu-plugin management
        wp_mu_plugin_enabled true
        wp_mu_plugin_dir /var/www/html/wp-content/mu-plugins
    }
    
    # Set document root
    root * /var/www/html
    
    # PHP handling with FrankenPHP
    php_server
    
    # Static file serving
    file_server
    
    # Compression
    encode gzip
    
    # Optional: Add custom headers
    header {
        X-Frame-Options "SAMEORIGIN"
        X-Content-Type-Options "nosniff"
        X-XSS-Protection "1; mode=block"
    }
    
    # Optional: Logging
    log {
        output file /var/log/caddy/access.log
        format console
    }
    
    # Handle errors
    handle_errors {
        @404 expression {http.error.status_code} == 404
        handle @404 {
            header Content-Type "text/html; charset=utf-8"
            respond "<!DOCTYPE html><html><head><title>404 Not Found</title></head><body><h1>404 - Page Not Found</h1></body></html>" 404
        }
        
        respond "{http.error.status_code} {http.error.status_text}"
    }
}
JSON Configuration Example
{
  "admin": {
    "listen": "localhost:2019"
  },
  "apps": {
    "http": {
      "servers": {
        "srv0": {
          "metrics": {},
          "listen": [":443"],
          "routes": [
            {
              "match": [
                {
                  "host": ["example.com"]
                }
              ],
              "handle": [
                {
                  "handler": "subroute",
                  "routes": [
                    {
                      "handle": [
                        {
                          "handler": "sidekick",
                          "metrics": "/metrics/sidekick",
                          "cache_dir": "/var/www/cache",
                          "cache_ttl": 3600,
                          "cache_response_codes": ["200", "301", "302"],
                          "nocache": ["/wp-admin", "/wp-json", "/wp-login.php"],
                          "nocache_home": false,
                          "nocache_regex": "\\.(mp4|webm|mp3|ogg|wav|pdf|zip|tar|gz|7z|exe)$",
                          "purge_path": "/__sidekick/purge",
                          "purge_url": "",
                          "purge_header": "X-Sidekick-Purge",
                          "purge_token": "your-secret-token-here",
                          "cache_memory_item_max_size": 4194304,
                          "cache_memory_max_size": 134217728,
                          "cache_memory_max_count": 32768,
                          "cache_memory_stream_to_disk_size": 10485760,
                          "cache_disk_item_max_size": 104857600,
                          "cache_disk_max_size": 10737418240,
                          "cache_disk_max_count": 100000,
                          "cache_key_queries": ["page", "sort", "filter"],
                          "cache_key_headers": ["Accept-Language"],
                          "cache_key_cookies": ["wordpress_logged_in_*"],
                          "wp_mu_plugin_enabled": true,
                          "wp_mu_plugin_dir": "/var/www/html/wp-content/mu-plugins"
                        }
                      ]
                    },
                    {
                      "handle": [
                        {
                          "handler": "rewrite",
                          "uri": "{http.matchers.file.relative}"
                        }
                      ],
                      "match": [
                        {
                          "file": {
                            "try_files": ["{http.request.uri.path}", "{http.request.uri.path}/", "index.php"]
                          }
                        }
                      ]
                    },
                    {
                      "handle": [
                        {
                          "handler": "reverse_proxy",
                          "transport": {
                            "protocol": "fastcgi",
                            "split_path": [".php"]
                          },
                          "upstreams": [
                            {
                              "dial": "localhost:9000"
                            }
                          ]
                        }
                      ],
                      "match": [
                        {
                          "path": ["*.php"]
                        }
                      ]
                    },
                    {
                      "handle": [
                        {
                          "handler": "file_server",
                          "root": "/var/www/html"
                        }
                      ]
                    }
                  ]
                }
              ]
            }
          ],
          "errors": {
            "routes": [
              {
                "match": [
                  {
                    "expression": "{http.error.status_code} == 404"
                  }
                ],
                "handle": [
                  {
                    "handler": "headers",
                    "response": {
                      "set": {
                        "Content-Type": ["text/html; charset=utf-8"]
                      }
                    }
                  },
                  {
                    "handler": "static_response",
                    "status_code": 404,
                    "body": "<!DOCTYPE html><html><head><title>404 Not Found</title></head><body><h1>404 - Page Not Found</h1></body></html>"
                  }
                ]
              }
            ]
          },
          "logs": {
            "logger_names": {
              "*": "default"
            }
          }
        }
      }
    },
    "logging": {
      "logs": {
        "default": {
          "writer": {
            "output": "file",
            "filename": "/var/log/caddy/access.log"
          },
          "encoder": {
            "format": "console"
          }
        }
      }
    }
  }
}

Cache Management

Purging Cache

The purge API only accepts POST requests with an optional JSON body specifying paths to purge.

Purge all cache (empty body or no body):
curl -X POST https://example.com/__sidekick/purge \
  -H "X-Sidekick-Purge: your-secret-token"
Purge specific paths (JSON body):
curl -X POST https://example.com/__sidekick/purge \
  -H "X-Sidekick-Purge: your-secret-token" \
  -H "Content-Type: application/json" \
  -d '{"paths": ["/blog/post-1", "/blog/post-2", "/products/*"]}'
Purge with wildcard patterns:
curl -X POST https://example.com/__sidekick/purge \
  -H "X-Sidekick-Purge: your-secret-token" \
  -H "Content-Type: application/json" \
  -d '{"paths": ["/blog/*", "/products/category-*", "/api/v1/*"]}'
WordPress mu-plugins

Sidekick automatically manages WordPress must-use plugins when wp_mu_plugin_enabled is set to true (default). These plugins provide:

  1. Content Cache Purge: Automatically purges cache when posts are updated
    • Uses SIDEKICK_PURGE_URL if set to send purge requests to a custom URL (e.g., https://api.example.com)
    • Falls back to WordPress get_site_url() if SIDEKICK_PURGE_URL is not set
    • Constructs the full purge URL as {SIDEKICK_PURGE_URL}{SIDEKICK_PURGE_PATH} or {get_site_url()}{SIDEKICK_PURGE_PATH}
  2. Force URL Rewrite: Ensures proper URL handling for WordPress

The mu-plugins are:

  • Automatically deployed on startup if not present
  • Updated if checksums don't match (ensuring latest version)
  • Removed if the feature is disabled and files match expected checksums
  • Only deployed if the parent directory exists (with warnings otherwise)

To disable automatic mu-plugin management:

sidekick {
    wp_mu_plugin_enabled false
}

Or via environment variable:

SIDEKICK_WP_MU_PLUGIN_ENABLED=false

Cache Key Configuration

Default Cache Key Components

By default, Sidekick includes the following in cache keys:

  • Query Parameters: p, page, paged, s, category, tag, author (common WordPress parameters)
  • Headers: Accept-Encoding (to vary cache by compression support)
  • Cookies: wordpress_logged_in_*, wordpress_sec_*, wp-settings-* (WordPress session cookies, with wildcard support)
Customizing Cache Keys

You can override these defaults in your Caddyfile:

sidekick {
    # Include all query parameters in cache key
    cache_key_queries *
    
    # Include specific headers
    cache_key_headers Accept-Language User-Agent
    
    # Include cookies with wildcard patterns
    cache_key_cookies session_* user_pref_*
}

Important: Setting any of these to an empty string ("") will disable that component entirely, which is not recommended as it may cause cache pollution. If you see warnings about empty cache key options, consider if you really want to disable them.

The cache_key_cookies option supports wildcard patterns using * for prefix matching:

  • wordpress_logged_in_* matches any cookie starting with wordpress_logged_in_
  • session_* matches session_id, session_token, etc.
  • Exact names (without *) only match that specific cookie

NoCache Path Matching

The nocache option uses prefix matching for paths:

sidekick {
    # This will bypass cache for:
    # - /wp-admin
    # - /wp-admin/index.php
    # - /wp-admin/users.php
    # - /wp-json
    # - /wp-json/wp/v2/posts
    # - /wp-json-custom (any path starting with /wp-json)
    nocache /wp-admin /wp-json
}

This makes it easy to exclude entire sections of your site from caching without listing every possible path.

Size Configuration Guidelines

Memory vs Disk Trade-offs
Setting Use Case Example Value
cache_memory_item_max_size Small, frequently accessed pages 4MB
cache_memory_max_size Available RAM for caching 256MB
cache_memory_max_percent Percentage of RAM to use 10 (10% of RAM)
cache_memory_stream_to_disk_size Balance memory vs disk I/O 5MB
cache_disk_item_max_size Prevent caching huge responses 100MB
cache_disk_max_size Total disk space for cache 10GB
cache_disk_max_percent Percentage of disk to use 5 (5% of disk)
cache_disk_max_count Max items on disk (LRU eviction) 100000
Special Values
  • 0 = Feature disabled
  • -1 = Unlimited (use with caution)
  • Human-readable byte-sizes: 1KB, 10MB, 1.5GB

Performance Tuning

For Shared Hosting (Limited Resources)
sidekick {
    cache_memory_max_size 64MB
    cache_memory_max_count 10000
    cache_memory_stream_to_disk_size 2MB
    cache_disk_item_max_size 20MB
    cache_disk_max_size 1GB
    cache_disk_max_count 10000  # Limited items for small disk
}
For VPS/Dedicated Server (Abundant Resources)
sidekick {
    cache_memory_max_percent 25     # Use 25% of RAM
    cache_memory_max_count -1       # Unlimited count
    cache_memory_stream_to_disk_size 20MB
    cache_disk_item_max_size 200MB
    cache_disk_max_percent 10       # Use 10% of disk space
    cache_disk_max_count -1          # Unlimited items on disk
}

Monitoring

Response Headers

Check cache headers in responses:

  • X-Sidekick-Cache: HIT - Served from cache
  • X-Sidekick-Cache: MISS - Not in cache, response cached
  • X-Sidekick-Cache: BYPASS - Caching bypassed
Prometheus Metrics

Sidekick automatically integrates with Caddy's metrics module to provide comprehensive cache monitoring. When metrics are enabled in your Caddyfile, Sidekick exposes detailed Prometheus metrics with zero additional configuration.

Available Metrics

Cache Storage Metrics:

  • caddy_sidekick_cache_used_bytes - Current cache usage in bytes (labels: type=[memory|disk|total], server)
  • caddy_sidekick_cache_limit_bytes - Cache size limit in bytes (labels: type=[memory|disk|total], server)
  • caddy_sidekick_cache_used_percent - Cache usage as percentage of limit (labels: type=[memory|disk|total], server)

Cache Count Metrics:

  • caddy_sidekick_cache_used_count - Number of cached items (labels: type=[memory|disk|total], server)
  • caddy_sidekick_cache_limit_count - Item count limit (labels: type=[memory|disk|total], server)

Cache Operations:

  • caddy_sidekick_cache_operations_total - Total operations counter (labels: operation=[get|bypass|store|purge], status=[hit|miss|success], server)
  • caddy_sidekick_cache_rate_percent - Cache hit/miss/bypass rates as percentages (labels: type=[hit|miss|bypass], server)

Performance Metrics:

  • caddy_sidekick_response_time_ms - Response time histogram in milliseconds (labels: cache_status=[hit|miss|bypass], server)
  • caddy_sidekick_cache_size_distribution_bytes - Distribution of cached item sizes (labels: type=[memory|disk], server)

Special values: 0 = disabled, -1 = unlimited, >0 = actual limit

Enabling Metrics

Sidekick metrics can be enabled by adding the metrics option to your sidekick configuration.

Important: The admin API must be enabled in your Caddyfile for the metrics endpoints to work:

{
    # Enable admin API (required for sidekick metrics endpoints)
    admin localhost:2019
}

example.com {
    sidekick {
        # Enable metrics collection
        metrics /metrics/sidekick
        
        # Other cache configuration
        cache_dir /var/www/cache
        # ... other settings ...
    }
}

When metrics are enabled, they are available in two locations:

  1. Admin API endpoint (detailed sidekick-specific metrics):

    • URL: http://localhost:2019/metrics/sidekick (Prometheus format)
    • URL: http://localhost:2019/metrics/sidekick/stats (JSON format)
    • Note: The admin API listens on localhost:2019 by default. Replace with your configured admin address if different.
  2. Caddy's standard metrics endpoint (includes sidekick metrics):

    {
        servers {
            metrics  # Enable Caddy's metrics collection
        }
    }
    
    example.com {
        handle /metrics {
            metrics  # Expose metrics publicly
        }
    }
    
    • URL: https://example.com/metrics (all Caddy metrics including sidekick)

Note: If the metrics option is not specified in the sidekick configuration, metrics collection is disabled to save resources. The admin API endpoints will not be available, and sidekick metrics will not appear in Caddy's standard metrics endpoint.

Example Prometheus Queries

Cache Hit Rate:

sum(rate(caddy_sidekick_cache_operations_total{operation="get",status="hit"}[5m])) /
sum(rate(caddy_sidekick_cache_operations_total{operation="get"}[5m])) * 100

Memory Usage Percentage:

caddy_sidekick_cache_used_bytes{type="memory"} / 
caddy_sidekick_cache_limit_bytes{type="memory"} * 100

Average Response Time by Cache Status:

rate(caddy_sidekick_response_time_ms_sum[5m]) / 
rate(caddy_sidekick_response_time_ms_count[5m])
Monitoring Best Practices
  1. Key Metrics to Watch:

    • Cache hit rate (target: >80%)
    • Memory usage (alert: >90%)
    • Disk usage (alert: >95%)
    • Response times (P95 <1s)
  2. Recommended Alerts:

    • Low cache hit rate (<50%)
    • High memory/disk usage (>90%)
    • Slow response times (P95 >1s)
    • High error rates
  3. Prometheus Scrape Configuration:

scrape_configs:
  - job_name: 'caddy_sidekick'
    static_configs:
      - targets: ['localhost:2019']  # Replace with your admin port
    scheme: http  # Use https if admin uses TLS
    metrics_path: /metrics/sidekick

Performance impact is minimal (<1% CPU overhead, ~10KB per metric series).

Complete Example with Monitoring Stack
version: '3.8'

services:
  caddy:
    image: caddy:latest
    build:
      context: .
      dockerfile: Dockerfile
    ports:
      - "80:80"
      - "443:443"
      - "443:443/udp"
      - "2019:2019"  # Admin API for metrics
    volumes:
      - ./Caddyfile:/etc/caddy/Caddyfile
      - caddy_data:/data
      - caddy_config:/config
      - caddy_cache:/var/cache/sidekick
    environment:
      - SIDEKICK_CACHE_MEMORY_MAX_SIZE=256MB
      - SIDEKICK_CACHE_DISK_MAX_SIZE=10GB

  prometheus:
    image: prom/prometheus
    ports:
      - "9090:9090"
    volumes:
      - ./prometheus.yml:/etc/prometheus/prometheus.yml
    command:
      - '--config.file=/etc/prometheus/prometheus.yml'

  grafana:
    image: grafana/grafana
    ports:
      - "3000:3000"
    environment:
      - GF_SECURITY_ADMIN_PASSWORD=admin
    depends_on:
      - prometheus

volumes:
  caddy_data:
  caddy_config:
  caddy_cache:

Prometheus Configuration (prometheus.yml):

global:
  scrape_interval: 15s

scrape_configs:
  - job_name: 'caddy'
    static_configs:
      - targets: ['caddy:2019']  # Replace 2019 with your admin port
    metrics_path: /metrics
  
  - job_name: 'caddy_sidekick'
    static_configs:
      - targets: ['caddy:2019']  # Replace 2019 with your admin port
    metrics_path: /metrics/sidekick

Note: Metrics are only collected when Caddy's metrics module is enabled. If metrics are not enabled in Caddy, Sidekick will operate normally without collecting metrics.

Alerting Rules for Prometheus

Create an alerts.yml file:

groups:
  - name: caddy_sidekick
    rules:
      - alert: LowCacheHitRate
        expr: caddy_sidekick_cache_rate_percent{type="hit"} < 50
        for: 5m
        annotations:
          summary: "Cache hit rate below 50%"
      
      - alert: HighMemoryUsage
        expr: caddy_sidekick_cache_used_percent{type="memory"} > 90
        for: 2m
        annotations:
          summary: "Memory cache >90% full"
      
      - alert: HighDiskUsage
        expr: caddy_sidekick_cache_used_percent{type="disk"} > 95
        for: 5m
        annotations:
          summary: "Disk cache >95% full"

Troubleshooting

Cache not working?
  1. Check response headers for X-Sidekick-Cache
  2. Verify paths aren't in nocache list
  3. Ensure response codes are in cache_response_codes
  4. Check WordPress login cookies aren't set
High memory usage?
  1. Reduce cache_memory_max_size or use cache_memory_max_percent
  2. Lower cache_memory_stream_to_disk_size
  3. Decrease cache_memory_max_count
Disk space issues?
  1. Reduce cache_ttl
  2. Lower cache_disk_item_max_size
  3. Set cache_disk_max_size or cache_disk_max_percent
  4. Implement regular cache purging

License

MIT License

Acknowledgements

This project was originally inspired by FrankenWP and it's Sidekick drop-in, and is designed specifically for Caddy web server with PHP/WordPress optimization in mind.

Documentation

Index

Constants

View Source
const (
	DefaultCacheDir            = "/var/www/html/wp-content/cache"
	DefaultMemoryItemMaxSize   = 4 * 1024 * 1024   // 4MB
	DefaultMemoryCacheMaxSize  = 128 * 1024 * 1024 // 128MB
	DefaultMemoryCacheMaxCount = 32 * 1024         // 32K items
	DefaultBypassDebugQuery    = "sidekick-nocache"
	DefaultPurgePath           = "/__sidekick/purge"
	DefaultPurgeHeader         = "X-Sidekick-Purge"
	DefaultPurgeToken          = "dead-beef"
	CacheHeaderName            = "X-Sidekick-Cache" // Not configurable
	DefaultNoCacheRegex        = ``                 /* 129-byte string literal not displayed */
	DefaultTTL                 = 300
	DefaultDiskItemMaxSize     = 100 * 1024 * 1024                     // 100MB
	DefaultDiskMaxSize         = 10 * 1024 * 1024 * 1024               // 10GB
	DefaultDiskMaxCount        = 100000                                // 100K items on disk
	DefaultStreamToDiskSize    = 10 * 1024 * 1024                      // 10MB
	DefaultBufferSize          = 32 * 1024                             // 32KB buffer size
	DefaultWPMuPluginEnabled   = true                                  // Enable mu-plugin management by default
	DefaultWPMuPluginDir       = "/var/www/html/wp-content/mu-plugins" // Default mu-plugins directory
)

Constants for default values

Variables

View Source
var (
	ErrCacheExpired  = errors.New("cache expired")
	ErrCacheNotFound = errors.New("cache not found")
)

Package-level errors

View Source
var CachedContentEncoding = []string{
	"none",
	"gzip",
	"br",
	"zstd",
}

Functions

func CompressBrotli

func CompressBrotli(data []byte) ([]byte, error)

CompressBrotli compresses data using brotli

func CompressForClient added in v0.0.3

func CompressForClient(data []byte, encoding string) ([]byte, error)

CompressForClient compresses data based on the requested encoding

func CompressGzip

func CompressGzip(data []byte) ([]byte, error)

CompressGzip compresses data using gzip

func CompressZstd added in v0.0.3

func CompressZstd(data []byte) ([]byte, error)

CompressZstd compresses data using zstd

func DecompressBrotli

func DecompressBrotli(data []byte) ([]byte, error)

DecompressBrotli decompresses brotli data Used internally for disk storage decompression

func DecompressGzip

func DecompressGzip(data []byte) ([]byte, error)

DecompressGzip decompresses gzip data Used internally for disk storage decompression

Types

type AdminMetrics added in v0.0.4

type AdminMetrics struct {
	// contains filtered or unexported fields
}

AdminMetrics is a Caddy admin module that exposes sidekick metrics

func (AdminMetrics) CaddyModule added in v0.0.4

func (AdminMetrics) CaddyModule() caddy.ModuleInfo

CaddyModule returns the Caddy module information.

func (*AdminMetrics) Cleanup added in v0.0.4

func (am *AdminMetrics) Cleanup() error

Cleanup cleans up the admin metrics handler

func (*AdminMetrics) Provision added in v0.0.4

func (am *AdminMetrics) Provision(ctx caddy.Context) error

Provision sets up the admin metrics handler.

func (*AdminMetrics) Routes added in v0.0.4

func (am *AdminMetrics) Routes() []caddy.AdminRoute

Routes returns the routes for the admin API.

type CacheStats added in v0.0.4

type CacheStats struct {
	UsedBytes  int64
	LimitBytes int64 // -1 for unlimited, 0 for disabled
	UsedCount  int64
	LimitCount int64 // -1 for unlimited, 0 for disabled
}

CacheStats holds cache statistics

type DiskCache

type DiskCache struct {
	// contains filtered or unexported fields
}

DiskCache wraps the generic LRU cache with disk-specific functionality

func NewDiskCache

func NewDiskCache(basePath string, maxItemCount int, maxDiskSize int64, maxItemSize int64, logger *zap.Logger) *DiskCache

NewDiskCache creates a new disk cache instance

func (*DiskCache) Delete

func (dc *DiskCache) Delete(key string) error

Delete removes an item from disk cache

func (*DiskCache) Get

func (dc *DiskCache) Get(key string) (*DiskCacheItem, error)

Get retrieves an item from disk cache

func (*DiskCache) List

func (dc *DiskCache) List() []string

List returns all keys in the disk cache

func (*DiskCache) LoadIndex

func (dc *DiskCache) LoadIndex() error

LoadIndex loads the disk cache index from filesystem This is called on startup to rebuild the cache index

func (*DiskCache) Put

func (dc *DiskCache) Put(key string, item *DiskCacheItem) error

Put adds or updates an item in disk cache

func (*DiskCache) Stats

func (dc *DiskCache) Stats() (itemCount int64, diskUsage int64)

Stats returns current disk cache statistics

type DiskCacheItem

type DiskCacheItem struct {
	*Metadata
	Path       string    // File path on disk
	Size       int64     // Size in bytes
	AccessTime time.Time // Last access time
	ModTime    time.Time // Last modification time
}

DiskCacheItem represents an item stored in disk cache

type MemoryCache

type MemoryCache[K comparable, V any] struct {
	// contains filtered or unexported fields
}

func NewMemoryCache

func NewMemoryCache[K comparable, V any](capacityCount int, capacityCost int) *MemoryCache[K, V]

func (*MemoryCache[K, V]) Cost

func (c *MemoryCache[K, V]) Cost() int

func (*MemoryCache[K, V]) Delete

func (c *MemoryCache[K, V]) Delete(key K)

func (*MemoryCache[K, V]) Get

func (c *MemoryCache[K, V]) Get(key K) (*V, bool)

func (*MemoryCache[K, V]) GetLRUKeys

func (c *MemoryCache[K, V]) GetLRUKeys(limit int) []K

GetLRUKeys returns a slice of keys in LRU order (oldest first) This is useful for eviction strategies

func (*MemoryCache[K, V]) LoadOrCompute

func (c *MemoryCache[K, V]) LoadOrCompute(key K, valueFn func() (V, int, bool)) (actual V, loaded bool)

LoadOrCompute returns the existing value for the key if present. Otherwise, it computes the value using the provided function and returns the computed value. The loaded result is true if the value was loaded, false if stored.

func (*MemoryCache[K, V]) Peek

func (c *MemoryCache[K, V]) Peek(key K) (*V, bool)

func (*MemoryCache[K, V]) Put

func (c *MemoryCache[K, V]) Put(key K, value V, cost int) bool

func (*MemoryCache[K, V]) Range

func (c *MemoryCache[K, V]) Range(f func(key K, value V) bool)

Range calls f sequentially for each key and value present in the map. If f returns false, range stops the iteration.

Range does not necessarily correspond to any consistent snapshot of the Map's contents: no key will be visited more than once, but if the value for any key is stored or deleted concurrently, Range may reflect any mapping for that key from any point during the Range call.

func (*MemoryCache[K, V]) Size

func (c *MemoryCache[K, V]) Size() int

type MemoryCacheItem

type MemoryCacheItem struct {
	*Metadata
	// contains filtered or unexported fields
}

type Metadata

type Metadata struct {
	StateCode int        `json:"c,omitempty"`
	Header    [][]string `json:"h,omitempty"`
	Timestamp int64      `json:"t,omitempty"`
	Path      string     `json:"p,omitempty"` // Original request path
	// contains filtered or unexported fields
}

func NewMetadata

func NewMetadata(stateCode int, hdr http.Header) *Metadata

func NewMetadataWithPath

func NewMetadataWithPath(stateCode int, hdr http.Header, path string) *Metadata

NewMetadataWithPath creates metadata with path information

func (*Metadata) LoadFromFile

func (m *Metadata) LoadFromFile(fp string) error

func (*Metadata) SetHeader

func (m *Metadata) SetHeader(hdr http.Header)

func (*Metadata) WriteToFile

func (m *Metadata) WriteToFile(fp string) error

type MetricsCollector added in v0.0.4

type MetricsCollector struct {
	// contains filtered or unexported fields
}

MetricsCollector handles all metrics collection for the sidekick plugin

func GetMetrics added in v0.0.4

func GetMetrics() *MetricsCollector

GetMetrics returns the global metrics instance if it exists

func GetOrCreateGlobalMetrics added in v0.0.4

func GetOrCreateGlobalMetrics(logger *zap.Logger) *MetricsCollector

GetOrCreateGlobalMetrics returns the global metrics instance, creating it if needed

func NewMetricsCollector added in v0.0.4

func NewMetricsCollector(logger *zap.Logger) *MetricsCollector

NewMetricsCollector creates a new metrics collector with its own registry

func (*MetricsCollector) Cleanup added in v0.0.4

func (m *MetricsCollector) Cleanup()

Cleanup stops the metrics collector and cleans up resources

func (*MetricsCollector) GetRates added in v0.0.4

func (m *MetricsCollector) GetRates() map[string]map[string]float64

GetRates returns the current hit, miss, and bypass rates for each cache type

func (*MetricsCollector) MetricsMiddleware added in v0.0.4

func (m *MetricsCollector) MetricsMiddleware(next func(w http.ResponseWriter, r *http.Request) error, serverName string) func(w http.ResponseWriter, r *http.Request) error

MetricsMiddleware wraps an HTTP handler to collect metrics

func (*MetricsCollector) RecordCacheOperation added in v0.0.4

func (m *MetricsCollector) RecordCacheOperation(operation, status, serverName string)

RecordCacheOperation records a cache operation with simplified metrics

func (*MetricsCollector) RecordItemSize added in v0.0.4

func (m *MetricsCollector) RecordItemSize(itemType, serverName string, size int64)

RecordItemSize records the size of a cached item for distribution analysis

func (*MetricsCollector) RecordResponseTime added in v0.0.4

func (m *MetricsCollector) RecordResponseTime(cacheStatus, serverName string, duration time.Duration)

RecordResponseTime records the response time for a request

func (*MetricsCollector) Reset added in v0.0.4

func (m *MetricsCollector) Reset()

Reset resets all metrics (useful for testing)

func (*MetricsCollector) ServeHTTP added in v0.0.4

func (m *MetricsCollector) ServeHTTP(w http.ResponseWriter, r *http.Request)

ServeHTTP serves the Prometheus metrics endpoint

func (*MetricsCollector) StartMetricsUpdater added in v0.0.4

func (m *MetricsCollector) StartMetricsUpdater(storage *Storage, serverName string)

StartMetricsUpdater starts a goroutine that periodically updates metrics

func (*MetricsCollector) UpdateCacheStats added in v0.0.4

func (m *MetricsCollector) UpdateCacheStats(serverName string, memStats, diskStats *CacheStats)

UpdateCacheStats updates cache statistics

type NopResponseWriter

type NopResponseWriter map[string][]string

func (*NopResponseWriter) Header

func (nop *NopResponseWriter) Header() http.Header

func (*NopResponseWriter) Write

func (nop *NopResponseWriter) Write(buf []byte) (int, error)

func (*NopResponseWriter) WriteHeader

func (nop *NopResponseWriter) WriteHeader(statusCode int)

type ResponseWriter

type ResponseWriter struct {
	http.ResponseWriter
	*http.Request
	*Storage
	*zap.Logger
	// contains filtered or unexported fields
}

ResponseWriter handles the response and provide the way to cache the value

func NewResponseWriter

func NewResponseWriter(rw http.ResponseWriter, r *http.Request, storage *Storage, logger *zap.Logger, s *Sidekick, once *sync.Once, cacheKey string, buf *bytes.Buffer) *ResponseWriter

func (*ResponseWriter) Close

func (r *ResponseWriter) Close() error

Close sets cache on response end

func (*ResponseWriter) Flush

func (r *ResponseWriter) Flush()

Implement http.Flusher interface if the underlying ResponseWriter supports it

func (*ResponseWriter) Header

func (r *ResponseWriter) Header() http.Header

func (*ResponseWriter) Hijack

func (r *ResponseWriter) Hijack() (net.Conn, *bufio.ReadWriter, error)

Implement http.Hijacker interface if the underlying ResponseWriter supports it

func (*ResponseWriter) Push

func (r *ResponseWriter) Push(target string, opts *http.PushOptions) error

Implement http.Pusher interface if the underlying ResponseWriter supports it

func (*ResponseWriter) Unwrap

func (r *ResponseWriter) Unwrap() http.ResponseWriter

func (*ResponseWriter) Write

func (r *ResponseWriter) Write(b []byte) (int, error)

Write will write the response body

func (*ResponseWriter) WriteHeader

func (r *ResponseWriter) WriteHeader(status int)

type Sidekick

type Sidekick struct {
	CacheDir           string   `json:"cache_dir,omitempty"`
	PurgePath          string   `json:"purge_path,omitempty"`
	PurgeURL           string   `json:"purge_url,omitempty"`
	PurgeHeader        string   `json:"purge_header,omitempty"`
	PurgeToken         string   `json:"purge_token,omitempty"`
	Metrics            string   `json:"metrics,omitempty"`       // Admin API path to expose metrics (e.g., "/metrics/sidekick")
	NoCache            []string `json:"nocache,omitempty"`       // Path prefixes to bypass
	NoCacheRegex       string   `json:"nocache_regex,omitempty"` // Regex pattern to bypass
	NoCacheHome        bool     `json:"nocache_home,omitempty"`  // Whether to skip caching home page
	CacheResponseCodes []string `json:"cache_response_codes,omitempty"`
	CacheTTL           int      `json:"cache_ttl,omitempty"` // TTL in seconds
	Storage            *Storage

	// WordPress mu-plugin configuration
	WPMuPluginEnabled bool   `json:"wp_mu_plugin_enabled,omitempty"` // Whether to manage mu-plugins
	WPMuPluginDir     string `json:"wp_mu_plugin_dir,omitempty"`     // Directory for mu-plugins

	// Size configurations (stored as int64 for byte values)
	CacheMemoryItemMaxSize      int64 `json:"cache_memory_item_max_size,omitempty"`       // Max size for single item in memory
	CacheMemoryMaxSize          int64 `json:"cache_memory_max_size,omitempty"`            // Total memory cache size limit
	CacheMemoryMaxPercent       int   `json:"cache_memory_max_percent,omitempty"`         // Memory cache as percent of available RAM (1-100)
	CacheMemoryMaxCount         int   `json:"cache_memory_max_count,omitempty"`           // Max number of items in memory
	CacheMemoryStreamToDiskSize int64 `json:"cache_memory_stream_to_disk_size,omitempty"` // Threshold to stream to disk
	CacheDiskItemMaxSize        int64 `json:"cache_disk_item_max_size,omitempty"`         // Max size for any cached item on disk
	CacheDiskMaxSize            int64 `json:"cache_disk_max_size,omitempty"`              // Total disk cache size limit
	CacheDiskMaxPercent         int   `json:"cache_disk_max_percent,omitempty"`           // Disk cache as percent of available space (1-100)
	CacheDiskMaxCount           int   `json:"cache_disk_max_count,omitempty"`             // Max number of items on disk

	// Cache key configuration
	CacheKeyHeaders []string `json:"cache_key_headers,omitempty"`
	CacheKeyQueries []string `json:"cache_key_queries,omitempty"`
	CacheKeyCookies []string `json:"cache_key_cookies,omitempty"`
	// contains filtered or unexported fields
}

func (Sidekick) CaddyModule

func (Sidekick) CaddyModule() caddy.ModuleInfo

func (*Sidekick) Cleanup added in v0.0.4

func (s *Sidekick) Cleanup() error

Cleanup implements caddy.CleanerUpper

func (*Sidekick) Provision

func (s *Sidekick) Provision(ctx caddy.Context) error

func (*Sidekick) ServeHTTP

func (s *Sidekick) ServeHTTP(w http.ResponseWriter, r *http.Request, next caddyhttp.Handler) error

ServeHTTP implements the caddy.Handler interface.

func (*Sidekick) UnmarshalCaddyfile

func (s *Sidekick) UnmarshalCaddyfile(d *caddyfile.Dispenser) error

type Storage

type Storage struct {
	// contains filtered or unexported fields
}

Storage manages both memory and disk cache storage

func NewStorage

func NewStorage(loc string, ttl int, memItemMaxSize int, memMaxSize int, memMaxCount int, diskItemMaxSize int, diskMaxSize int, diskMaxCount int, logger *zap.Logger) *Storage

NewStorage creates a new Storage instance

func (*Storage) Flush

func (s *Storage) Flush() error

func (*Storage) Get

func (s *Storage) Get(key string) ([]byte, *Metadata, error)

func (*Storage) GetDiskCache

func (s *Storage) GetDiskCache() *DiskCache

func (*Storage) GetMemCache

func (s *Storage) GetMemCache() *MemoryCache[string, *MemoryCacheItem]

func (*Storage) List

func (s *Storage) List() map[string][]string

func (*Storage) Purge

func (s *Storage) Purge(key string) error

func (*Storage) Set

func (s *Storage) Set(url string, metadata *Metadata, data []byte) error

func (*Storage) SetWithKey

func (s *Storage) SetWithKey(key string, metadata *Metadata, data []byte) error

SetWithKey stores data with the provided key

func (*Storage) WaitForAsyncOps

func (s *Storage) WaitForAsyncOps()

WaitForAsyncOps waits for all asynchronous operations to complete This is mainly useful for testing and graceful shutdown

type SyncHandler

type SyncHandler struct {
	// contains filtered or unexported fields
}

SyncHandler manages synchronization for cache operations

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL