Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save johnwbaxter/52cd70741b35abedf9be to your computer and use it in GitHub Desktop.
Save johnwbaxter/52cd70741b35abedf9be to your computer and use it in GitHub Desktop.

"Medium traffic" = able to handle around 50 concurrent users on average and burst up to 100-150 without causing your server to thrash.

If you want to handle 500+ concurrent users with the same modest hardware see the Varnish section below.

VPS

  • 4096 MB memory
  • 125GB SSD
  • 4 CPUs
  • Cpanel
  • Centos 6

Apache 2.4.9

  • Mod_ruid2
  • Mod Security
  • Deflate
  • Expires
  • Fileprotect
  • Headers
  • MPM Prefork
  • Proxy
  • UniqueId

PHP 5.5.13

Since we're using mod_ruid2 we're using DSO as the PHP handler. My load testing shows it's faster and more stable than the only other sensible option on cPanel (FCGI), but YMMV. When cPanel finally adds PHP-FPM to EasyApache, use that (do NOT be tempted to go the DIY route). Tick the Silence Deprecated Patch if you are running EE < 2.8, since a few things throw deprecated notices in PHP 5.5.

  • Bcmath
  • CGI
  • Calendar
  • CurlSSL
  • Exif
  • FTP
  • GD
  • Gettext
  • Iconv
  • Imap
  • Mbregex
  • Mbstring
  • Mcrypt
  • Mysql
  • Mysql of the system
  • Opensssl
  • Pear
  • PGsql
  • Phar
  • SQLite3
  • Silence Deprecated Patch
  • Sockets
  • System Timezone
  • TTF
  • Zlib

Apache config

Min Spare Servers 3

Max Spare Servers 6

  • should be double min spare servers

Start Servers 3

  • should be same as min spare servers

Server Limit 100

  • No more than 75% of available ram. If your webpages typically uses 30Mb per process then 100x30Mb = 3000Mb of ram used. Don't rely on the Template Debugger to tell you how much a typical Apache process uses - run top in your terminal while your site is under load.

Max Request Workers 100

  • Same as server limit

Max Connections Per Child 5000

  • Should be at between 10x - 50x the max clients.
  • Higher means more chance of memory leaks from poorly coded PHP scripts (bad) but fewer new processes (good), so experiment.

Keep-Alive On
Keep-Alive Timeout 1
Max Keep-Alive Requests 50

Time Out 120

MySQL config

Edit /etc/my.cnf

Optimised for running EE websites with a mix of MySIAM and InnoDB tables. Uncomment sql-mode if you want to run MySQL in strict mode (some add-ons won't like it).

[mysqld]

# SAFETY #
local-infile                    = 0
max-allowed-packet              = 16M
max-connect-errors              = 1000000
skip-name-resolve 
# sql-mode                        = STRICT_TRANS_TABLES,ERROR_FOR_DIVISION_BY_ZERO,NO_AUTO_CREATE_USER,NO_AUTO_VALUE_ON_ZERO,NO_ENGINE_SUBSTITUTION,NO_ZERO_DATE,NO_ZERO_IN_DATE,ONLY_FULL_GROUP_BY
sysdate-is-now                  = 1
innodb                          = FORCE
innodb-strict-mode              = 1
myisam-recover                  = FORCE,BACKUP
 
# CACHES AND LIMITS # 
tmp-table-size                  = 48M
max-heap-table-size             = 48M
query-cache-type                = 1
query-cache-size                = 64M
query-cache-limit               = 2M
max-connections                 = 500
thread-cache-size               = 50
open-files-limit                = 65535
table-definition-cache          = 4096
table-open-cache                = 4096
 
# INNODB # 
innodb-flush-method             = O_DIRECT
innodb-log-files-in-group       = 2
innodb-log-file-size            = 128M
innodb_additional_mem_pool_size = 20M
innodb-flush-log-at-trx-commit  = 2
innodb-file-per-table           = 1
innodb-buffer-pool-size         = 2G

# MyISAM #
key-buffer-size                = 64M

# LOGGING #
log-error                      = /var/lib/mysql/mysql-error.log
log-queries-not-using-indexes  = 1
slow-query-log                 = 1
long_query_time                = 0.1
slow-query-log-file            = /var/lib/mysql/mysql-slow.log

This is a useful wizard if you want to make sure the values are right for your particular server: https://tools.percona.com/wizard

Zend opcache config

PHP 5.5 has opcode caching built in, but it is disabled by default. Edit /usr/local/lib/php.ini and add this to the bottom:

zend_extension=opcache.so
[opcache]
opcache.enable=1
opcache.enable_cli=1
opcache.memory_consumption=128
opcache.interned_strings_buffer=8
opcache.max_accelerated_files=4000
opcache.max_wasted_percentage=5
opcache.use_cwd=1
opcache.validate_timestamps=1
opcache.revalidate_freq=1
opcache.fast_shutdown=1

While editing php.in, make sure to to set the memory limit to no more than 4x the size of a typical Apache process. E.g.

memory_limit = 128M 	

Varnish

The easiest way to get this installed is to buy a licence for the excellent Unixy Varnish plugin for cPanel: http://www.unixy.net/varnish/

Once you have downloaded the zip from Unixy, upload it to /usr/src. Then SSH into your server and follow the installation instructions provided by Unixy.

When you're done login to WHM and find the new Varnish control panel.

On the advanced configuration page:

  • Set Cache time to Live to 60 seconds (go higher if you dare or have heavy traffic).
  • Set Memory Cache to 250M (can go up to 2G, depends on how big site is and how much is cached).
  • Add these URLs using the 'URL opt-out' field: admin\.php and ACT=. (Add any other URLs you want to exclude from caching, e.g. 'system' if you still have that in your webroot).

Now you need to customize /etc/varnish/default.vcl. The code below is adapted from Unixy's config and this article by Kevin Cupp. It enables the Mustash Varnish plugin or Kevin's Purge extension to purge the Varnish cache when cache-breaking rules are triggered. With Mustash, Varnish cache-breaking is granular: when Stash full page caches are cleared in the CP or cache-breaking is triggered by rules, the corresponding Varnish cache object will be cleared.

Refer to the article if you want to make further changes to the vcl config such as enabling Edge Side Includes.

Note: If you have set a custom cookie_prefix in your site config, change exp_sessionid to your_prefix_sessionid.

Restart Varnish (from WHM control panel) after editing default.vcl.

###################################################
# Copyright (c) UNIXY  -  http://www.unixy.net    #
# The leading truly fully managed server provider #
###################################################

include "/etc/varnish/cpanel.backend.vcl";

include "/etc/varnish/backends.vcl";

# Set passthru ACL logic
include "/etc/varnish/aggregates/passthru_acl.vcl";

# mod_security rules
include "/etc/varnish/security.vcl";

include "/etc/varnish/iratelimit.vcl";

sub vcl_recv {

# Use the default backend for all other requests
set req.backend = default;

# Setup the different backends logic
include "/etc/varnish/acllogic.vcl";

# Allow a grace period for offering "stale" data in case backend lags
set req.grace = 5m;

remove req.http.X-Forwarded-For;
set req.http.X-Forwarded-For = client.ip;

# cPanel URLs
include "/etc/varnish/cpanel.url.vcl";

# Properly handle different encoding types
if (req.http.Accept-Encoding) {
	if (req.url ~ "\.(jpg|jpeg|png|gif|gz|tgz|bz2|tbz|mp3|ogg|swf|ico)$") {
		# No point in compressing these
		remove req.http.Accept-Encoding;
	} elsif (req.http.Accept-Encoding ~ "gzip") {
		set req.http.Accept-Encoding = "gzip";
	} elsif (req.http.Accept-Encoding ~ "deflate") {
		set req.http.Accept-Encoding = "deflate";
	} else {
		# unkown algorithm
		remove req.http.Accept-Encoding;
	}
}

include "/etc/varnish/ratelimit.vcl";

# Set up disabled
include "/etc/varnish/disabled.vcl";

# Exclude upgrade, install, server-status, etc
include "/etc/varnish/known.exclude.vcl";

# Set up exceptions
include "/etc/varnish/url.exclude.vcl";

# Set up exceptions
include "/etc/varnish/debugurl.exclude.vcl";

# Set up exceptions
include "/etc/varnish/vhost.exclude.vcl";

# Set up user defined vhost exceptions
include "/etc/varnish/aggregates/disable_domains.vcl";

# Set up IP-based banning
include "/etc/varnish/aggregates/passthru_ip.vcl";

# Set up vhost+url exceptions
include "/etc/varnish/vhosturl.exclude.vcl";

# Set up cPanel reseller exceptions
include "/etc/varnish/reseller.exclude.vcl";

# Restart rule for bfile recv
include "/etc/varnish/bigfile.recv.vcl";

# Purge
if (req.request == "PURGE") {
	if (!client.ip ~ acl127_0_0_1) {error 405 "Not permitted";}
    return (lookup);
}

# Clear the cache for an entire domain
if (req.request == "EE_PURGE") {
	ban("req.url ~ ^/.*$ && req.http.host == "+req.http.host);
	error 200 "Purged";
}

# Clear any cached object that matches the exact req.url
if (req.request == "EE_PURGE_URL") {
    ban("req.url == "+req.url+" && req.http.host == "+req.http.host);
    error 200 "Purged";
}

## Default request checks
if (req.request != "GET" &&
req.request != "HEAD" &&
req.request != "PUT" &&
req.request != "POST" &&
req.request != "TRACE" &&
req.request != "OPTIONS" &&
req.request != "DELETE") {
	return (pipe);
}

# Don't cache dynamic content
if (req.request != "GET" && req.request != "HEAD") {
	return (pass);
}

# Don't cache content for logged in users
if (req.http.Cookie ~ "exp_sessionid") {
	return (pass);
}

## Modified from default to allow caching if cookies are set, but not http auth
if (req.http.Authorization) {
	return (pass);
}

include "/etc/varnish/versioning.static.vcl";

# Remove cookies
unset req.http.Cookie;

include "/etc/varnish/slashdot.recv.vcl";
include "/etc/varnish/aggregates/wp_in.vcl";
include "/etc/varnish/aggregates/gun_in.vcl";

return (lookup);
}

sub vcl_fetch {

set beresp.ttl = 40s;
set beresp.http.Server = " - Web acceleration by http://www.unixy.net/varnish ";

# Turn off Varnish gzip processing
include "/etc/varnish/gzip.off.vcl";

# Grace to allow varnish to serve content if backend is lagged
set beresp.grace = 5m;

# Restart rule bfile for fetch
include "/etc/varnish/bigfile.fetch.vcl";

# These status codes should always pass through and never cache.
if (beresp.status == 503 || beresp.status == 500) {
	set beresp.http.X-Cacheable = "NO: beresp.status";
	set beresp.http.X-Cacheable-status = beresp.status;
	return (hit_for_pass);
}

if (beresp.status == 404) {
	set beresp.http.magicmarker = "1";
	set beresp.http.X-Cacheable = "YES";
	set beresp.ttl = 20s;
	return (deliver);
}

/* Remove Expires from backend, it's not long enough */    
unset beresp.http.expires;

if (req.url ~ "\.(js|css|jpg|jpeg|png|gif|gz|tgz|bz2|tbz|mp3|ogg|swf|pdf|ico)$" && ! (req.url ~ "\.(php)") ) {
	unset beresp.http.set-cookie;
	include "/etc/varnish/static.ttl.vcl";
	include "/etc/varnish/aggregates/static_ttl.vcl";
}
include "/etc/varnish/slashdot.fetch.vcl"; 
include "/etc/varnish/aggregates/wp_out.vcl";
else {
	include "/etc/varnish/dynamic.ttl.vcl";
	include "/etc/varnish/aggregates/dynamic_ttl.vcl";
}

/* marker for vcl_deliver to reset Age: */
set beresp.http.magicmarker = "1";

# All tests passed, therefore item is cacheable
set beresp.http.X-Cacheable = "YES";

return (deliver);
}

sub vcl_deliver {

  # From http://varnish-cache.org/wiki/VCLExampleLongerCaching
  if (resp.http.magicmarker) {
     /* Remove the magic marker */
     unset resp.http.magicmarker;

     /* By definition we have a fresh object */
     set resp.http.age = "0";
   }

   #add cache hit data
   if (obj.hits > 0) {
     #if hit add hit count
     set resp.http.X-Cache = "HIT";
     set resp.http.X-Cache-Hits = obj.hits;
   }
else {
     set resp.http.X-Cache = "MISS";
   }
   
   # cache object url
   set resp.http.X-Url = req.url;

}

sub vcl_error {

if (obj.status == 503 && req.restarts < 5) {
set obj.http.X-Restarts = req.restarts;
return (restart);
}

}

# Added to let users force refresh
sub vcl_hit {

if (req.request == "PURGE") {
	purge;
	error 200 "Purged";
}

if (obj.ttl < 1s) {
	return (pass);
}

if (req.http.Cache-Control ~ "no-cache") {
# Ignore requests via proxy caches,  IE users and badly behaved crawlers
# like msnbot that send no-cache with every request.
if (! (req.http.Via || req.http.User-Agent ~ "bot|MSIE|HostTracker")) {
	set obj.ttl = 0s;
	return (restart);
} 
}

return (deliver);

}

sub vcl_hash {

	hash_data(req.http.cookie);
}

sub vcl_miss {

if (req.request == "PURGE") {
	purge;
	error 200 "Purged";
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment