Re-provisioning to enable fastCGI Caching

I decided to enable FastCGI Caching after having provisioned the server already once so updated trellis/group_vars/production/wordpress_sites.yml:

# Created by trellis-cli v1.13.0
# Documentation: https://roots.io/trellis/docs/wordpress-sites/

wordpress_sites:
  domain.com:
    site_hosts:
    - canonical: domain.com
      redirects:
      - www.domain.com
    local_path: ../site
    branch: main
    repo: git@github.com:domain/domain.com.git
    repo_subtree_path: site
    multisite:
      enabled: false
    ssl:
      enabled: true
      provider: letsencrypt
    cache:
      enabled: true
      skip_cache_uri: /wp-admin/|/wp-json/|/xmlrpc.php|wp-.*.php|/feed/|index.php|sitemap(_index)?.xml|/store.*|/cart.*|/my-account.*|/checkout.*|/addons.*
      skip_cache_cookie: comment_author|wordpress_[a-f0-9]+|wp-postpass|wordpress_no_cache|wordpress_logged_in|woocommerce_cart_hash|woocommerce_items_in_cart|wp_woocommerce_session_

so once I updated main branch I did a provisioning with tags needed:

trellis provision --tags "nginx wordpress-setup" production
Starting galaxy role install process
- composer (1.9.2) is already installed, skipping.
- ntp (2.5.0) is already installed, skipping.
- logrotate (v0.0.5) is already installed, skipping.
- swapfile (v2.0.38) is already installed, skipping.
- mailpit (v1.0.0) is already installed, skipping.

Running command => ansible-playbook server.yml --tags=nginx wordpress-setup -e env=production

PLAY [Ensure necessary variables are defined] **********************************

PLAY [Test Connection and Determine Remote User] *******************************

TASK [connection : Require manual definition of remote-user] *******************
skipping: [XXX.XXX.XXX.XXX]

TASK [connection : Check whether Ansible can connect as root] ******************
ok: [XXX.XXX.XXX.XXX -> localhost]

TASK [connection : Warn about change in host keys] *****************************
skipping: [XXX.XXX.XXX.XXX]

TASK [connection : Set remote user for each host] ******************************
ok: [XXX.XXX.XXX.XXX]

TASK [connection : Announce which user was selected] ***************************
ok: [XXX.XXX.XXX.XXX] => {
    "msg": "Note: Ansible will attempt connections as user = root\n"
}

TASK [connection : Load become password] ***************************************
skipping: [XXX.XXX.XXX.XXX]

PLAY [WordPress Server - Install LEMP Stack with PHP and MariaDB MySQL] ********

TASK [Gathering Facts] *********************************************************
[WARNING]: Platform linux on host XXX.XXX.XXX.XXX is using the discovered
Python interpreter at /usr/bin/python3.12, but future installation of another
Python interpreter could change the meaning of that path. See
https://docs.ansible.com/ansible-
core/2.17/reference_appendices/interpreter_discovery.html for more information.
ok: [XXX.XXX.XXX.XXX]

PLAY RECAP *********************************************************************
XXX.XXX.XXX.XXX            : ok=4    changed=0    unreachable=0    failed=0    skipped=3    rescued=0    ignored=0   

And saw no changes added oddly. So I checked server:

cat /etc/nginx/sites-enabled/domain.com.conf
# Ansible managed


server {
  listen [::]:443 ssl;
  listen 443 ssl;
  http2 on;
  http3 off;
  server_name domain.com;
  
  access_log   /srv/www/domain.com/logs/access.log main;
  error_log    /srv/www/domain.com/logs/error.log;
  
  root  /srv/www/domain.com/current/web;
  index index.php index.htm index.html;
  add_header Fastcgi-Cache $upstream_cache_status;

  # Specify a charset
  charset utf-8;

  # Set the max body size equal to PHP's max POST size.
  client_max_body_size 25m;

  # SSL configuration
  include h5bp/directive-only/ssl.conf;
  include h5bp/directive-only/ssl-stapling.conf;
  ssl_buffer_size 1400; # 1400 bytes to fit in one MTU

  add_header Strict-Transport-Security "max-age=31536000; ;";
  ssl_certificate         /etc/nginx/ssl/letsencrypt/domain.com-bundled.cert;
  ssl_certificate_key     /etc/nginx/ssl/letsencrypt/domain.com.key;

  include acme-challenge-location.conf;

  include includes.d/all/*.conf;
  include includes.d/domain.com/*.conf;

  # Prevent PHP scripts from being executed inside the uploads folder.
  location ~* /app/uploads/.*\.php$ {
    deny all;
  }
  
  # Prevent Blade and Twig templates from being accessed directly.
  location ~* \.(blade\.php|twig)$ {
    deny all;
  }
  
  # composer
  location ~* composer\.(json|lock)$ {
    deny all;
  }

  location ~* composer/installed\.json$ {
    deny all;
  }

  location ~* auth\.json$ {
    deny all;
  }

  # npm
  location ~* package(-lock)?\.json$ {
    deny all;
  }

  # yarn
  location ~* yarn\.lock$ {
    deny all;
  }

  # bundler
  location ~* Gemfile(\.lock)?$ {
    deny all;
  }

  location ~* gems\.(rb|locked)?$ {
    deny all;
  }
  
  location / {
    try_files $uri $uri/ /index.php?$args;
  }
  
    
  include h5bp/directive-only/cache-file-descriptors.conf;
  include h5bp/directive-only/extra-security.conf;
  include h5bp/directive-only/x-ua-compatible.conf;
  include h5bp/directive-only/cross-origin-requests.conf;
  include h5bp/location/protect-system-files.conf;
  
  add_header Content-Security-Policy "frame-ancestors 'self'" always;
  add_header X-Frame-Options SAMEORIGIN always;
  location ~ \.php$ {
    try_files $uri /index.php;

    include fastcgi_params;
    fastcgi_param SCRIPT_FILENAME $realpath_root$fastcgi_script_name;
    fastcgi_param DOCUMENT_ROOT $realpath_root;
    fastcgi_pass unix:/var/run/php-fpm-wordpress.sock;
  }
}

# Redirect to https
server {
  listen [::]:80;
  listen 80;
  server_name domain.com;

  include acme-challenge-location.conf;

  include includes.d/all/*.conf;
  include includes.d/domain.com/*.conf;

  location / {
    return 301 https://$host$request_uri;
  }
}

# Redirect some domains
server {
  listen [::]:443 ssl;
  listen 443 ssl;
  listen [::]:80;
  listen 80;
  http2 on;
  http3 off;
  server_name www.domain.com;

  # SSL configuration
  include h5bp/directive-only/ssl.conf;
  include h5bp/directive-only/ssl-stapling.conf;
  ssl_buffer_size 1400; # 1400 bytes to fit in one MTU

  add_header Strict-Transport-Security "max-age=31536000; ;";
  ssl_certificate         /etc/nginx/ssl/letsencrypt/domain.com-bundled.cert;
  ssl_certificate_key     /etc/nginx/ssl/letsencrypt/domain.com.key;

  include acme-challenge-location.conf;

  include includes.d/all/*.conf;
  include includes.d/domain.com/*.conf;

  location / {
    return 301 https://domain.com$request_uri;
  }
}

so no updates there and checking date

ll /etc/nginx/sites-available/domain.com.conf 
-rw-r--r-- 1 root root 3416 Feb 13 02:31 /etc/nginx/sites-available/domain.com.conf

I saw last update was two days ago and cache directory check showed it was not there:

ls -la /var/run/nginx-cache/
ls: cannot access '/var/run/nginx-cache/': No such file or directory

How can I update the server to have the FastCGI caching turned on?

did another trellis provision production without those two tags - perhaps issue doing it that way? - and then checked config:

cat /etc/nginx/sites-enabled/domain.com.conf
# Ansible managed


server {
  listen [::]:443 ssl;
  listen 443 ssl;
  http2 on;
  http3 off;
  server_name domain.com;
  
  access_log   /srv/www/domain.com/logs/access.log main;
  error_log    /srv/www/domain.com/logs/error.log;
  
  root  /srv/www/domain.com/current/web;
  index index.php index.htm index.html;
  add_header Fastcgi-Cache $upstream_cache_status;

  # Specify a charset
  charset utf-8;

  # Set the max body size equal to PHP's max POST size.
  client_max_body_size 25m;

  # Fastcgi cache conditions
  set $skip_cache 0;

  # Skip requests with HTTP methods that should not be cached: DELETE, OPTIONS, PATCH, POST, PUT
  if ($request_method !~ ^(GET|HEAD)$) {
    set $skip_cache 1;
  }

  if ($query_string != "") {
    set $skip_cache 1;
  }
  if ($request_uri ~* "/wp-admin/|/wp-json/|/xmlrpc.php|wp-.*.php|/feed/|index.php|sitemap(_index)?.xml|/store.*|/cart.*|/my-account.*|/checkout.*|/addons.*") {
    set $skip_cache 1;
  }
  if ($http_cookie ~* "comment_author|wordpress_[a-f0-9]+|wp-postpass|wordpress_no_cache|wordpress_logged_in|woocommerce_cart_hash|woocommerce_items_in_cart|wp_woocommerce_session_") {
    set $skip_cache 1;
  }

  # SSL configuration
  include h5bp/directive-only/ssl.conf;
  include h5bp/directive-only/ssl-stapling.conf;
  ssl_buffer_size 1400; # 1400 bytes to fit in one MTU

  add_header Strict-Transport-Security "max-age=31536000; ;";
  ssl_certificate         /etc/nginx/ssl/letsencrypt/domain.com-bundled.cert;
  ssl_certificate_key     /etc/nginx/ssl/letsencrypt/domain.com.key;

  include acme-challenge-location.conf;

  include includes.d/all/*.conf;
  include includes.d/domain.com/*.conf;

  # Prevent PHP scripts from being executed inside the uploads folder.
  location ~* /app/uploads/.*\.php$ {
    deny all;
  }
  
  # Prevent Blade and Twig templates from being accessed directly.
  location ~* \.(blade\.php|twig)$ {
    deny all;
  }
  
  # composer
  location ~* composer\.(json|lock)$ {
    deny all;
  }

  location ~* composer/installed\.json$ {
    deny all;
  }

  location ~* auth\.json$ {
    deny all;
  }

  # npm
  location ~* package(-lock)?\.json$ {
    deny all;
  }

  # yarn
  location ~* yarn\.lock$ {
    deny all;
  }

  # bundler
  location ~* Gemfile(\.lock)?$ {
    deny all;
  }

  location ~* gems\.(rb|locked)?$ {
    deny all;
  }
  
  location / {
    try_files $uri $uri/ /index.php?$args;
  }
  
    
  include h5bp/directive-only/cache-file-descriptors.conf;
  include h5bp/directive-only/extra-security.conf;
  include h5bp/directive-only/x-ua-compatible.conf;
  include h5bp/directive-only/cross-origin-requests.conf;
  include h5bp/location/protect-system-files.conf;
  
  add_header Content-Security-Policy "frame-ancestors 'self'" always;
  add_header X-Frame-Options SAMEORIGIN always;
  location ~ \.php$ {
    try_files $uri /index.php;

    # Fastcgi cache settings
    fastcgi_cache wordpress;
    fastcgi_cache_valid 30s;
    fastcgi_cache_bypass $skip_cache;
    fastcgi_no_cache $skip_cache;
    fastcgi_cache_background_update on;

    include fastcgi_params;
    fastcgi_param SCRIPT_FILENAME $realpath_root$fastcgi_script_name;
    fastcgi_param DOCUMENT_ROOT $realpath_root;
    fastcgi_pass unix:/var/run/php-fpm-wordpress.sock;
  }
}

# Redirect to https
server {
  listen [::]:80;
  listen 80;
  server_name domain.com;

  include acme-challenge-location.conf;

  include includes.d/all/*.conf;
  include includes.d/domain.com/*.conf;

  location / {
    return 301 https://$host$request_uri;
  }
}

# Redirect some domains
server {
  listen [::]:443 ssl;
  listen 443 ssl;
  listen [::]:80;
  listen 80;
  http2 on;
  http3 off;
  server_name www.domain.com;

  # SSL configuration
  include h5bp/directive-only/ssl.conf;
  include h5bp/directive-only/ssl-stapling.conf;
  ssl_buffer_size 1400; # 1400 bytes to fit in one MTU

  add_header Strict-Transport-Security "max-age=31536000; ;";
  ssl_certificate         /etc/nginx/ssl/letsencrypt/domain.com-bundled.cert;
  ssl_certificate_key     /etc/nginx/ssl/letsencrypt/domain.com.key;

  include acme-challenge-location.conf;

  include includes.d/all/*.conf;
  include includes.d/domain.com/*.conf;

  location / {
    return 301 https://domain.com$request_uri;
  }
}

Did find

 # Fastcgi cache settings
fastcgi_cache wordpress;
fastcgi_cache_valid 30s;
fastcgi_cache_bypass $skip_cache;
fastcgi_no_cache $skip_cache;
fastcgi_cache_background_update on;

So this way it did get added and on check I did get a fastCGI caching miss fastcgi-cache: MISS:

curl -I https://domain.com/
HTTP/2 200 
server: nginx
date: Sat, 15 Feb 2025 01:50:39 GMT
content-type: text/html; charset=UTF-8
vary: Accept-Encoding
link: <https://domain.com/wp-json/>; rel="https://api.w.org/"
link: <https://domain.com/wp-json/wp/v2/pages/6765>; rel="alternate"; title="JSON"; type="application/json"
link: <https://domain.com/>; rel=shortlink
fastcgi-cache: MISS
strict-transport-security: max-age=31536000; ;
x-content-type-options: nosniff
x-xss-protection: 1; mode=block
x-ua-compatible: IE=Edge
content-security-policy: frame-ancestors 'self'
x-frame-options: SAMEORIGIN

And on second check I saw fastcgi-cache: STALE so guess it got passed 30 seconds and got set to stale as mentioned in Roots documentation.

And on another page visit and check with curl I did get a fastcgi-cache: HIT. So all working.

And finally I see cache stored at

ll /var/cache/nginx 
total 80
drwxr-xr-x 20 www-data root     4096 Feb 15 02:16 ./
drwxr-xr-x 14 root     root     4096 Feb 13 02:27 ../
drwx------  3 www-data www-data 4096 Feb 15 01:56 0/
drwx------  4 www-data www-data 4096 Feb 15 02:18 2/
drwx------  4 www-data www-data 4096 Feb 15 02:17 3/
drwx------  5 www-data www-data 4096 Feb 15 02:24 4/
drwx------  4 www-data www-data 4096 Feb 15 02:13 5/
drwx------  4 www-data www-data 4096 Feb 15 02:10 6/
drwx------  3 www-data www-data 4096 Feb 15 01:58 7/
drwx------  4 www-data www-data 4096 Feb 15 01:50 8/
drwx------  3 www-data www-data 4096 Feb 15 01:59 a/
drwx------  3 www-data www-data 4096 Feb 15 02:16 b/
drwx------  4 www-data www-data 4096 Feb 15 01:54 c/
drwx------  2 www-data root     4096 Feb 14 03:01 client_temp/
drwx------  3 www-data www-data 4096 Feb 15 02:06 e/
drwx------  4 www-data www-data 4096 Feb 15 02:08 f/
drwx------ 12 www-data root     4096 Feb 13 05:41 fastcgi_temp/
drwx------  2 www-data root     4096 Feb 13 02:29 proxy_temp/
drwx------  2 www-data root     4096 Feb 13 02:29 scgi_temp/
drwx------  2 www-data root     4096 Feb 13 02:29 uwsgi_temp/

so that proves it even further all is well.

P.S. Still wonder how I can provision properly with multiple tags .

1 Like

Isn’t it:
trellis provision --tags=nginx,wordpress-setup production?

2 Likes

Thanks @Twansparant . I see now what I did wrong.

1 Like