Skip to content

Instantly share code, notes, and snippets.

@croxton
Last active September 5, 2021 23:34
Show Gist options
  • Star 20 You must be signed in to star a gist
  • Fork 4 You must be signed in to fork a gist
  • Save croxton/d2294e9413ed3c4ebde1 to your computer and use it in GitHub Desktop.
Save croxton/d2294e9413ed3c4ebde1 to your computer and use it in GitHub Desktop.
Battle-tested cPanel VPS server configuration for medium traffic ExpressionEngine websites

"Medium traffic" = able to handle around 50 concurrent users on average.

If you want to handle 100+ 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 65

  • Assuming MySQL uses between 0.5 - 1GB (as it will if you use the config below), Linux and other services are using around 0.4GB, so you have between 2.6GB-3.1GB to allocate to Apache. If your httpd processes typically use around 40MB per process (normal for EE) then being conservative: 65x40MB = 2600MB (~2.54GB)

  • 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 65

  • Same as server limit

Max Connections Per Child 3250

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

Keep-Alive Off

  • You may want to turn on Keep Alives, but if you do use low limits:

Keep-Alive Timeout 1
Max Keep-Alive Requests 50

Time Out 120

My.cnf config for MySQL 5.6

The following configuration is 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). Note that setting performance_schema = 0 is esential for production sites, as this can use significant amounts of memory.

Edit /etc/my.cnf

[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
performance_schema              = 0
 
# CACHES AND LIMITS # 
tmp-table-size                  = 64M
max-heap-table-size             = 64M
query-cache-type                = 1
query-cache-size                = 80M
query-cache-limit               = 256K
query-cache-min-res-unit        = 2048
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         = 512M

# MyISAM #
key-buffer-size                = 256M

# 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=0
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 600 seconds (go higher if you have a lot of content that doesn't change frequently).
  • 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";
}
}
@phripley
Copy link

It appears that documentation for mod_ruid2 has moved:
https://documentation.cpanel.net/display/EA/Apache+Module:+ModRuid2

@croxton
Copy link
Author

croxton commented Jan 22, 2016

Now recommending setting opcache.enable_cli=0 to avoid zend_mm_heap corrupted error

http://stackoverflow.com/a/24275630/1791461

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment