Skip to content

Instantly share code, notes, and snippets.

@technopagan
Created December 7, 2012 08:23
Show Gist options
  • Save technopagan/4231769 to your computer and use it in GitHub Desktop.
Save technopagan/4231769 to your computer and use it in GitHub Desktop.
High Performance Wordpress Webserver Stack with NGINX, PHP-FPM + APC and Varnish
# We only have one backend to define: NGINX
backend default {
.host = "127.0.0.1";
.port = "8080";
}
# Only allow purging from specific IPs
acl purge {
"localhost";
"127.0.0.1";
}
sub vcl_recv {
# Handle compression correctly. Different browsers send different
# "Accept-Encoding" headers, even though they mostly support the same
# compression mechanisms. By consolidating compression headers into
# a consistent format, we reduce the cache size and get more hits.
# @see: http:// varnish.projects.linpro.no/wiki/FAQ/Compression
if (req.http.Accept-Encoding) {
if (req.http.Accept-Encoding ~ "gzip") {
# If the browser supports it, we'll use gzip.
set req.http.Accept-Encoding = "gzip";
}
else if (req.http.Accept-Encoding ~ "deflate") {
# Next, try deflate if it is supported.
set req.http.Accept-Encoding = "deflate";
}
else {
# Unknown algorithm. Remove it and send unencoded.
unset req.http.Accept-Encoding;
}
}
# Set client IP
if (req.http.x-forwarded-for) {
set req.http.X-Forwarded-For = req.http.X-Forwarded-For + ", " + client.ip;
} else {
set req.http.X-Forwarded-For = client.ip;
}
# Check if we may purge (only localhost)
if (req.request == "PURGE") {
if (!client.ip ~ purge) {
error 405 "Not allowed.";
}
return(lookup);
}
if (req.request != "GET" &&
req.request != "HEAD" &&
req.request != "PUT" &&
req.request != "POST" &&
req.request != "TRACE" &&
req.request != "OPTIONS" &&
req.request != "DELETE") {
# /* Non-RFC2616 or CONNECT which is weird. */
return (pipe);
}
if (req.request != "GET" && req.request != "HEAD") {
# /* We only deal with GET and HEAD by default */
return (pass);
}
# admin users always miss the cache
if( req.url ~ "^/wp-(login|admin)" ||
req.http.Cookie ~ "wordpress_logged_in_" ){
return (pass);
}
# Remove cookies set by Google Analytics (pattern: '__utmABC')
if (req.http.Cookie) {
set req.http.Cookie = regsuball(req.http.Cookie, "(^|; ) *__utm.=[^;]+;? *", "\1");
if (req.http.Cookie == "") {
remove req.http.Cookie;
}
}
# always pass through POST requests and those with basic auth
if (req.http.Authorization || req.request == "POST") {
return (pass);
}
# Do not cache these paths
if (req.url ~ "^/wp-cron\.php$" ||
req.url ~ "^/xmlrpc\.php$" ||
req.url ~ "^/wp-admin/.*$" ||
req.url ~ "^/wp-includes/.*$" ||
req.url ~ "\?s=") {
return (pass);
}
# Define the default grace period to serve cached content
set req.grace = 30s;
# By ignoring any other cookies, it is now ok to get a page
unset req.http.Cookie;
return (lookup);
}
sub vcl_fetch {
# remove some headers we never want to see
unset beresp.http.Server;
unset beresp.http.X-Powered-By;
# only allow cookies to be set if we're in admin area
if( beresp.http.Set-Cookie && req.url !~ "^/wp-(login|admin)" ){
unset beresp.http.Set-Cookie;
}
# don't cache response to posted requests or those with basic auth
if ( req.request == "POST" || req.http.Authorization ) {
return (hit_for_pass);
}
# don't cache search results
if( req.url ~ "\?s=" ){
return (hit_for_pass);
}
# only cache status ok
if ( beresp.status != 200 ) {
return (hit_for_pass);
}
# If our backend returns 5xx status this will reset the grace time
# set in vcl_recv so that cached content will be served and
# the unhealthy backend will not be hammered by requests
if (beresp.status == 500) {
set beresp.grace = 60s;
return (restart);
}
# GZip the cached content if possible
if (beresp.http.content-type ~ "text") {
set beresp.do_gzip = true;
}
# if nothing abovce matched it is now ok to cache the response
set beresp.ttl = 24h;
return (deliver);
}
sub vcl_deliver {
# remove some headers added by varnish
unset resp.http.Via;
unset resp.http.X-Varnish;
}
sub vcl_hit {
# Set up invalidation of the cache so purging gets done properly
if (req.request == "PURGE") {
purge;
error 200 "Purged.";
}
return (deliver);
}
sub vcl_miss {
# Set up invalidation of the cache so purging gets done properly
if (req.request == "PURGE") {
purge;
error 200 "Purged.";
}
return (fetch);
}
sub vcl_error {
if (obj.status == 503) {
# set obj.http.location = req.http.Location;
set obj.status = 404;
set obj.response = "Not Found";
return (deliver);
}
}
# How to use the cachebuster function when registering styles in your theme's functions.php
wp_register_style('style',get_bloginfo('stylesheet_directory').autoVer('style.css'),false,NULL,'all');
/**
* Automated cache-buster function via filemtime
**/
function autoVer($url){
$name = explode('.',$url);
$lastext = array_pop($name);
array_push($name,filemtime($_SERVER['DOCUMENT_ROOT'].parse_url($url,
PHP_URL_PATH)),$lastext);
$fullname = implode('.',$name) ;
echo $fullname;
}
# How to use the cachebuster function in your theme's header.php
<?php
autoVer(get_bloginfo('stylesheet_url'));
?>
[...]
<?php
/*
* Circumvent @import CSS for WordPress child themes
* If we're in a child theme, build links for both parent and child CSS
* This way, we can remove the @import from the child theme's style.css
* CSS loaded via link can load simultaneously, while @import blocks loading
* See: http://www.stevesouders.com/blog/2009/04/09/dont-use-import/
*/
if(is_child_theme()) {
echo '<link rel="stylesheet" href="';
autoVer(get_bloginfo('template_url').'/style.css');
echo '" />'."\n\t\t";
echo '<link rel="stylesheet" href="';
autoVer(get_bloginfo('stylesheet_url'));
echo '" />'."\n";
} else {
echo '<link rel="stylesheet" href="';
autoVer(get_bloginfo('stylesheet_url'));
echo '" />'."\n";
}
?>
application/vnd.ms-fontobject eot;
application/x-font-ttf ttf;
font/opentype ott;
application/font-woff woff;
user www-data;
worker_processes 2;
pid /var/run/nginx.pid;
events {
worker_connections 768;
multi_accept on;
use epoll;
}
http {
# Let NGINX get the real client IP for its access logs
set_real_ip_from 127.0.0.1;
real_ip_header X-Forwarded-For;
# Basic Settings
sendfile on;
tcp_nopush on;
tcp_nodelay on;
keepalive_timeout 20;
client_max_body_size 15m;
client_body_timeout 60;
client_header_timeout 60;
client_body_buffer_size 1K;
client_header_buffer_size 1k;
large_client_header_buffers 4 8k;
send_timeout 60;
reset_timedout_connection on;
types_hash_max_size 2048;
server_tokens off;
# server_names_hash_bucket_size 64;
# server_name_in_redirect off;
include /etc/nginx/mime.types;
default_type application/octet-stream;
# Logging Settings
# access_log /var/log/nginx/access.log;
error_log /var/log/nginx/error.log;
# Log Format
log_format main '$remote_addr - $remote_user [$time_local] '
'"$request" $status $body_bytes_sent "$http_referer" '
'"$http_user_agent" "$http_x_forwarded_for"';
# Gzip Settings
gzip on;
gzip_static on;
gzip_disable "msie6";
gzip_vary on;
gzip_proxied any;
gzip_comp_level 6;
gzip_min_length 512;
gzip_buffers 16 8k;
gzip_http_version 1.1;
gzip_types text/css text/javascript text/xml text/plain text/x-component application/javascript application/x-javascript application/json application/xml application/rss+xml font/truetype application/x-font-ttf font/opentype application/vnd.ms-fontobject image/svg+xml;
# Virtual Host Configs
include /etc/nginx/conf.d/*.conf;
include /etc/nginx/sites-enabled/*;
}
pid = /var/run/php5-fpm.pid
error_log = /var/log/php5-fpm.log
emergency_restart_threshold = 5
emergency_restart_interval = 2
events.mechanism = epoll
short_open_tag = Off
ignore_user_abort = Off
post_max_size = 15M
upload_max_filesize = 15M
default_charset = "UTF-8"
allow_url_fopen = Off
default_socket_timeout = 30
mysql.allow_persistent = Off
[...]
# This should go to the end of the file:
[apc]
apc.stat = "0"
apc.max_file_size = "1M"
apc.localcache = "1"
apc.localcache.size = "256"
apc.shm_segments = "1"
apc.ttl = "3600"
apc.user_ttl = "7200"
apc.gc_ttl = "3600"
apc.cache_by_default = "1"
apc.filters = ""
apc.write_lock = "1"
apc.num_files_hint= "512"
apc.user_entries_hint="4096"
apc.shm_size = "256M"
apc.mmap_file_mask=/tmp/apc.XXXXXX
apc.include_once_override = "0"
apc.file_update_protection="2"
apc.canonicalize = "1"
apc.report_autofilter="0"
apc.stat_ctime="0"
;This should be used when you are finished with PHP file changes.
;As you must clear the APC cache to recompile already cached files.
;If you are still developing, set this to 1.
apc.stat="0"
DAEMON_OPTS="-a :80 \
-T localhost:6082 \
-f /etc/varnish/default.vcl \
-u www-data -g www-data \
-S /etc/varnish/secret \
-p thread_pools=2 \
-p thread_pool_min=25 \
-p thread_pool_max=250 \
-p thread_pool_add_delay=2 \
-p session_linger=50 \
-p sess_workspace=262144 \
-p cli_timeout=40 \
-s malloc,768m"
user = www-data
group = www-data
listen = /var/run/php-fpm.socket
listen.owner = www-data
listen.group = www-data
listen.mode = 0666
listen.allowed_clients = 127.0.0.1
pm = dynamic
pm.max_children = 50
pm.start_servers = 15
pm.min_spare_servers = 5
pm.max_spare_servers = 25
pm.process_idle_timeout = 60s
request_terminate_timeout = 30
security.limit_extensions = .php
[...]
# This should to the end of the file:
php_flag[display_errors] = off
php_admin_value[error_reporting] = 0
php_admin_value[error_log] = /var/log/php5-fpm.log
php_admin_flag[log_errors] = on
php_admin_value[memory_limit] = 128M
server {
# Default server block blacklisting all unconfigured access
listen [::]:8080 default_server;
server_name _;
return 444;
}
server {
# Configure the domain that will run WordPress
server_name yourdomain.tld;
listen [::]:8080 deferred;
port_in_redirect off;
server_tokens off;
autoindex off;
client_max_body_size 15m;
client_body_buffer_size 128k;
# WordPress needs to be in the webroot of /var/www/ in this case
root /var/www;
index index.html index.htm index.php;
try_files $uri $uri/ /index.php?q=$uri&amp;$args;
# Define default caching of 24h
expires 86400s;
add_header Pragma public;
add_header Cache-Control "max-age=86400, public, must-revalidate,
proxy-revalidate";
# deliver a static 404
error_page 404 /404.html;
location /404.html {
internal;
}
# Deliver 404 instead of 403 "Forbidden"
error_page 403 = 404;
# Do not allow access to files giving away your WordPress version
location ~ /(\.|wp-config.php|readme.html|licence.txt) {
return 404;
}
# Add trailing slash to */wp-admin requests.
rewrite /wp-admin$ $scheme://$host$uri/ permanent;
# Don't log robots.txt requests
location = /robots.txt {
allow all;
log_not_found off;
access_log off;
}
# Rewrite for versioned CSS+JS via filemtime
location ~* ^.+\.(css|js)$ {
rewrite ^(.+)\.(\d+)\.(css|js)$ $1.$3 last;
expires 31536000s;
access_log off;
log_not_found off;
add_header Pragma public;
add_header Cache-Control "max-age=31536000, public";
}
# Aggressive caching for static files
# If you alter static files often, please use "add_header Cache-Control
"max-age=31536000, public, must-revalidate, proxy-revalidate";"
location ~* \.(asf|asx|wax|wmv|wmx|avi|bmp|class|divx|doc|docx|eot|exe|gif|gz|gzip|ico|jpg|jpeg|jpe|mdb|mid|midi|mov|qt|mp3|m4a|mp4|m4v|mpeg|mpg|mpe|mpp|odb|odc|odf|odg|odp|ods|odt|ogg|ogv|otf|pdf|png|pot|pps|ppt|pptx|ra|ram|svg|svgz|swf|tar|t?gz|tif|tiff|ttf|wav|webm|wma|woff|wri|xla|xls|xlsx|xlt|xlw|zip)$ {
expires 31536000s;
access_log off;
log_not_found off;
add_header Pragma public;
add_header Cache-Control "max-age=31536000, public";
}
# pass PHP scripts to Fastcgi listening on Unix socket
# Do not process them if inside WP uploads directory
# If using Multisite or a custom uploads directory,
# please set the */uploads/* directory in the regex below
location ~* (^(?!(?:(?!(php|inc)).)*/uploads/).*?(php)) {
try_files $uri = 404;
fastcgi_split_path_info ^(.+.php)(.*)$;
fastcgi_pass unix:/var/run/php-fpm.socket;
fastcgi_index index.php;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
include fastcgi_params;
fastcgi_intercept_errors on;
fastcgi_ignore_client_abort off;
fastcgi_connect_timeout 60;
fastcgi_send_timeout 180;
fastcgi_read_timeout 180;
fastcgi_buffer_size 128k;
fastcgi_buffers 4 256k;
fastcgi_busy_buffers_size 256k;
fastcgi_temp_file_write_size 256k;
}
# Deny access to hidden files
location ~ /\. {
deny all;
access_log off;
log_not_found off;
}
}
# Redirect all www. queries to non-www
# Change in case your site is to be available at "www.yourdomain.tld"
server {
listen [::]:8080;
server_name www.yourdomain.tld;
rewrite ^ $scheme://yourdomain.tld$request_uri? permanent;
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment