"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](https://gist.github.com/croxton/d2294e9413ed3c4ebde1#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](http://docs.cpanel.net/twiki/bin/view/EasyApache/Apache/ModRuid) 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, 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. - 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 - See: http://abdussamad.com/archives/169-Apache-optimization:-KeepAlive-On-or-Off.html - Also: http://www.reddit.com/r/webdev/comments/1bs4t0/3_small_tweaks_to_make_apache_fly/ **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 = 128M 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 = 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=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 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](http://ellislab.com/blog/entry/making-sites-fly-with-varnish) 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"; } }