Making multitenant LAMP less stressful.
Switch branches/tags
Nothing to show
Clone or download
Fetching latest commit…
Cannot retrieve the latest commit at this time.
Permalink
Failed to load latest commit information.
org
other
.gitignore
Makefile
README

README

			       _________

				NOPANEL
			       _________


Table of Contents
_________________

1 NoPanel: multitenant hosting without the bloat
.. 1.1 Sites
..... 1.1.1 Creating Sites
..... 1.1.2 Deleting Sites
..... 1.1.3 Backing Up Sites
..... 1.1.4 Backup It All Up
..... 1.1.5 Listing Sites
..... 1.1.6 Managing Certificates
.. 1.2 Installation


1 NoPanel: multitenant hosting without the bloat
================================================

  Sometimes a shared webserver makes sense, but you still want to save
  your sanity. NoPanel is a solution to this need. It provides a minimal
  framework for organizing many Apache docroots on the same server,
  separating user accounts from one another, and performing other basic
  tasks like backups and site setup.

  More important is what NoPanel *doesn't* do:

  - It does not provide a web user interface for administration.
  - It does not require a database for administration.
  - It eschews magic files and other ersatz-databases wherever
    applicable.
  - It doesn't use a 4,000 line Perl script when 8 lines of bash will
    do.

  I use this simple setup on my own servers, and thought it would be
  nice to share it with the community.


1.1 Sites
~~~~~~~~~

  In NoPanel, a "site" is a single domain, a single Apache config file,
  and a single docroot. There is no yaml, json, or other configuration
  file, so in order to keep everything straight, we need to rely on some
  conventions.


1.1.1 Creating Sites
--------------------

  Each site in NoPanel requires a "name", which will be used as the name
  of the Linux system user (and group) that this site uses. Since you
  might be typing this quite often, take a second and think about what
  username you would like to use. While NoPanel technically doesn't
  care, it will be easier for you, a human, if you come up with some
  ration scheme.

  Personally, I use the convention <client descriptor>_<site
  descriptor>. For example, if I were setting up two sites for Frank's
  Pet Shop and they had two domains, one for their public-facing site,
  and the other a private wiki for employees, I might call the two sites
  `franks_main' and `franks_wiki'.

  In any case, the `create_site.sh' script will need to know the name
  you choose, and the domain of the site:

  ,----
  | domain=$1
  | user=$2
  | 
  | if [[ "$domain" == "" ]]; then
  |     echo "No domain provided." 1>&2
  |     exit 1
  | fi
  | 
  | if [[ "$user" == "" ]]; then
  |     echo "No user provided." 1>&2
  |     exit 1
  | fi
  `----

  The docroot of a site lives in in `/var/www/$domain', and the Apache
  configuration lives in =/etc/-apache2/sites-available/$domain.conf.

  ,----
  | docroot="/var/www/$domain"
  | conf="/etc/apache2/sites-available/$domain.conf"
  | echo -e "I'll try to create a site with domain '$domain' and matching user '$user'.\n"
  `----


  We create the user without a homedir, /with/ a user group of the same
  name, and with no login shell (or more accurately, the login shell set
  to /bin/nologin. This is under the assumption that only you, root,
  will be the only one messing with these files at all. Remember,
  there's no control panel here. In 2018, if a user knows enough to be
  logging into a shell, he can get his own VPS for $12 a year and
  doesn't want to be on your shared server.

  ,----
  | echo -n "Creating new user and user-group..."
  | useradd -M -U -s /bin/nologin "$user"
  | echo "Done!"
  `----

  The Apache config file is created using a template which looks like
  this:

  ,----
  | <VirtualHost *:8080>
  | 	ServerName {domain}
  | 	ServerAlias www.{domain}
  | 
  | 	DocumentRoot /var/www/{domain}
  | 
  | 	LogLevel info
  | 
  | 	ErrorLog /var/log/apache2/{domain}-error.log
  | 	CustomLog /var/log/apache2/{domain}-access.log combined
  | 
  | 	AssignUserId {user} {user}
  | </VirtualHost>
  `----

  Rather than use a complex templating engine, we rely on a few `sed'
  expressions to replace "single-hug" placeholders with their actual
  values.

  ,----
  | conf_file=/etc/apache2/sites-available/$domain.conf
  | echo -n "Creating new apache config '$conf_file'..."
  | {
  |     sed -e "s/{domain}/$domain/g" -e "s/{user}/$user/g" <<CONFTEMPLATE
  | 
  | <VirtualHost *:8080>
  | 	ServerName {domain}
  | 	ServerAlias www.{domain}
  | 
  | 	DocumentRoot /var/www/{domain}
  | 
  | 	LogLevel info
  | 
  | 	ErrorLog /var/log/apache2/{domain}-error.log
  | 	CustomLog /var/log/apache2/{domain}-access.log combined
  | 
  | 	AssignUserId {user} {user}
  | </VirtualHost>
  | 
  | CONFTEMPLATE
  | }>$conf_file
  `----

  Notice that the VirtualHost listens on port 8080, since in my usual
  configuration, Apache is actually sitting behind some kind of
  front-end proxy or load balancer. This isn't always on the same
  machine, but I've standardized on having Apache listen on 8080 even
  when the load-balancer is on a different machine. As with other
  decisions in NoPanel, this one was made to reduce the amount of
  site-specific configuration that is necessary for the types of sites I
  typically deal with.

  As previously mentioned, the DocumentRoot is in `/var/www/$domain'.

  `LogLevel info', as is configured here, is one level above "debug" and
  logs quite a bit. I find this helpful when troubleshooting. With large
  disks, low-traffic sites, and proper log rotation, the volume is
  rarely a problem. To make it easy to find logs relating to a
  particular site, each domain has its own logs.

  Now we're ready to create the docroot itself...

  ,----
  | echo -n "Creating new document in '$docroot'..."
  | mkdir -p "$docroot"
  | echo "Done!"
  `----

  ...and to populate it with a test page:

  ,----
  | echo -n "Creating test page at '$docroot/index.php'..."
  | echo "<?php phpinfo(); ?>" > $docroot/index.php
  | chown -R $user. $docroot
  | echo "Done!"
  `----

  The last thing we need to do is "enable" the site with `a2ensite'
  (which itself just symlinks the config file into
  `/etc/apache/sites-enabled/') and reload the Apache config.

  ,----
  | a2ensite $domain
  | 
  | echo -n "Reloading Apache..."
  | service apache2 reload
  | echo "Done!"
  | 
  | echo "Test site should be available at 'http://$domain' (barring DNS snafus)."
  `----

  Like it says on the time, you should then be able to check out the
  `phpinfo()' page at the proper domain, provided DNS is set up, or you
  have the appropriate entries in your local hosts file.

  Taken together, we have a straightforward Bash function of about
  two-dozen non-blank lines:

  ,----
  | function create_site {
  | domain=$1
  | user=$2
  | 
  | if [[ "$domain" == "" ]]; then
  |     echo "No domain provided." 1>&2
  |     exit 1
  | fi
  | 
  | if [[ "$user" == "" ]]; then
  |     echo "No user provided." 1>&2
  |     exit 1
  | fi
  | 
  | docroot="/var/www/$domain"
  | conf="/etc/apache2/sites-available/$domain.conf"
  | echo -e "I'll try to create a site with domain '$domain' and matching user '$user'.\n"
  | 
  | echo -n "Creating new user and user-group..."
  | useradd -M -U -s /bin/nologin "$user"
  | echo "Done!"
  | 
  | echo -n "Creating new document in '$docroot'..."
  | mkdir -p "$docroot"
  | echo "Done!"
  | 
  | conf_file=/etc/apache2/sites-available/$domain.conf
  | echo -n "Creating new apache config '$conf_file'..."
  | {
  |     sed -e "s/{domain}/$domain/g" -e "s/{user}/$user/g" <<CONFTEMPLATE
  | 
  | <VirtualHost *:8080>
  | 	ServerName {domain}
  | 	ServerAlias www.{domain}
  | 
  | 	DocumentRoot /var/www/{domain}
  | 
  | 	LogLevel info
  | 
  | 	ErrorLog /var/log/apache2/{domain}-error.log
  | 	CustomLog /var/log/apache2/{domain}-access.log combined
  | 
  | 	AssignUserId {user} {user}
  | </VirtualHost>
  | 
  | CONFTEMPLATE
  | }>$conf_file
  | 
  | echo -n "Creating test page at '$docroot/index.php'..."
  | echo "<?php phpinfo(); ?>" > $docroot/index.php
  | chown -R $user. $docroot
  | echo "Done!"
  | 
  | a2ensite $domain
  | 
  | echo -n "Reloading Apache..."
  | service apache2 reload
  | echo "Done!"
  | 
  | echo "Test site should be available at 'http://$domain' (barring DNS snafus)."
  | }
  `----


1.1.2 Deleting Sites
--------------------

  To delete a site from NoPanel, we need to be absolutely sure that we
  need to know the domain of the site in question, and we need to be
  darn sure that we aren't getting a blank domain and deleting all of
  `/var/www':

  ,----
  | domain="$1"
  | docroot="$(cd "/var/www/$domain"; pwd)"
  | 
  | if [[ "$domain" == "" ]]; then
  |     echo "No domain provided." 1>&2
  |     exit 1
  | fi
  | 
  | if [[ ! -e "$docroot" ]]; then
  |     echo "'$docroot' does not exist." 1>&2
  |     exit 1
  | fi
  | 
  | if [[ ! -d "$docroot" ]]; then
  |     echo "'$docroot' is not a directory." 1>&2
  |     exit 1
  | fi
  | 
  | if [[ "$docroot" == "/var/www" ]]; then
  |     echo "Not deleting '$docroot'. Please double-check your input." 1>&2
  |     exit 1
  | fi
  `----

  Once we're sure of that, we can determine the Linux user for the site
  in question, and make sure it's a sane one:

  ,----
  | user="$(stat -c "%U" "$docroot")"
  | 
  | if [[ "$user" == "" ]]; then
  |     echo "No user found for site '$domain'." 1>&2
  |     exit 1
  | fi
  | 
  | if [[ "$user" == "root" ]]; then
  |     echo "Not deleting user '$user'." 1>&2
  |     exit 1
  | fi
  `----

  Next we need to find the config file for the site:

  ,----
  | conf_file="/etc/apache2/sites-available/$domain.conf"
  | 
  | if [[ ! -f "$conf_file" ]]; then
  |     echo "Conf file '$conf_file' does not exist." 1>&2
  |     exit 1
  | fi
  `----

  Finally, we can start doing some damage. First, we disable the site's
  config:

  ,----
  | a2dissite "$domain"
  `----

  Then, we delete the config file itself:
  ,----
  | rm "$conf_file"
  `----

  Remove the document root:
  ,----
  | rm -rfv "$docroot"
  `----

  Finally, we delete the user:
  ,----
  | userdel "$user"
  `----

  ...and reload the Apache config:

  ,----
  | service apache2 reload
  `----

  And that's it! That site won't bother us any more.

  ,----
  | function delete_site {
  | domain="$1"
  | docroot="$(cd "/var/www/$domain"; pwd)"
  | 
  | if [[ "$domain" == "" ]]; then
  |     echo "No domain provided." 1>&2
  |     exit 1
  | fi
  | 
  | if [[ ! -e "$docroot" ]]; then
  |     echo "'$docroot' does not exist." 1>&2
  |     exit 1
  | fi
  | 
  | if [[ ! -d "$docroot" ]]; then
  |     echo "'$docroot' is not a directory." 1>&2
  |     exit 1
  | fi
  | 
  | if [[ "$docroot" == "/var/www" ]]; then
  |     echo "Not deleting '$docroot'. Please double-check your input." 1>&2
  |     exit 1
  | fi
  | 
  | user="$(stat -c "%U" "$docroot")"
  | 
  | if [[ "$user" == "" ]]; then
  |     echo "No user found for site '$domain'." 1>&2
  |     exit 1
  | fi
  | 
  | if [[ "$user" == "root" ]]; then
  |     echo "Not deleting user '$user'." 1>&2
  |     exit 1
  | fi
  | 
  | conf_file="/etc/apache2/sites-available/$domain.conf"
  | 
  | if [[ ! -f "$conf_file" ]]; then
  |     echo "Conf file '$conf_file' does not exist." 1>&2
  |     exit 1
  | fi
  | 
  | a2dissite "$domain"
  | rm "$conf_file"
  | rm -rfv "$docroot"
  | userdel "$user"
  | }
  `----


1.1.3 Backing Up Sites
----------------------

  Backing up a site in NoPanel results in a tidy single-file tarball
  that contains the site's files, config, username, and (/Real Soon
  Now/) dumps of its databases.

  As always, we'll first sanity-check our input:

  ,----
  | domain="$1"
  | docroot="$(cd "/var/www/$domain"; pwd)"
  | user="$(stat -c "%U" "$docroot")"
  | 
  | if [[ "$domain" = "" ]]; then
  |     echo "No domain provided." 1>&2
  |     exit 1
  | fi
  | 
  | if [[ "$user" == "" ]]; then
  |     echo "No user found for site '$domain'." 1>&2
  |     exit 1
  | fi
  | 
  | if [[ ! -e "$docroot" ]]; then
  |     echo "'$docroot' does not exist." 1>&2
  |     exit 1
  | fi
  | 
  | if [[ ! -d "$docroot" ]]; then
  |     echo "'$docroot' is not a directory." 1>&2
  |     exit 1
  | fi
  | 
  | if [[ "$docroot" == "/var/www" ]]; then
  |     echo "Not backing up all of '$docroot'. Please double-check your input." 1>&2
  |     exit 1
  | fi
  `----

  Next, we'll need a temporary directory to work in:

  ,----
  | tmpdir="$(mktemp -d)"
  `----

  Now we can move the files over:
  ,----
  | mkdir -p $tmpdir
  | rsync -aP /var/www/$domain/ $tmpdir/docroot
  | chown -R $user. $tmpdir/docroot
  `----

  ...save the username:

  ,----
  | echo "$user" > $tmpdir/USER
  `----

  ...and tar the whole thing up:

  ,----
  | olddir="$(pwd)"
  | cd $tmpdir
  | tar -czvf /root/backups/$domain-$(date +%F-%s).tar.gz .
  `----

  Finally, we want to clean up our temporary files:

  ,----
  | rm -rf $tmpdir
  | cd "$olddir"
  `----

  The finished function:

  ,----
  | function backup_site {
  | 
  | domain="$1"
  | docroot="$(cd "/var/www/$domain"; pwd)"
  | user="$(stat -c "%U" "$docroot")"
  | 
  | if [[ "$domain" = "" ]]; then
  |     echo "No domain provided." 1>&2
  |     exit 1
  | fi
  | 
  | if [[ "$user" == "" ]]; then
  |     echo "No user found for site '$domain'." 1>&2
  |     exit 1
  | fi
  | 
  | if [[ ! -e "$docroot" ]]; then
  |     echo "'$docroot' does not exist." 1>&2
  |     exit 1
  | fi
  | 
  | if [[ ! -d "$docroot" ]]; then
  |     echo "'$docroot' is not a directory." 1>&2
  |     exit 1
  | fi
  | 
  | if [[ "$docroot" == "/var/www" ]]; then
  |     echo "Not backing up all of '$docroot'. Please double-check your input." 1>&2
  |     exit 1
  | fi
  | 
  | 
  | tmpdir="$(mktemp -d)"
  | 
  | mkdir -p $tmpdir
  | rsync -aP /var/www/$domain/ $tmpdir/docroot
  | chown -R $user. $tmpdir/docroot
  | 
  | olddir="$(pwd)"
  | cd $tmpdir
  | tar -czvf /root/backups/$domain-$(date +%F-%s).tar.gz .
  | 
  | rm -rf $tmpdir
  | cd "$olddir"
  | }
  `----


1.1.4 Backup It All Up
----------------------

  With `backup_site' and `list_sites' in our quiver, backing up all
  sites becomes not much more than a oneliner:

  ,----
  | function backup_all {
  |     list_sites | awk '{print $1}' | while read domain;
  |     do
  | 	backup_site "$domain";
  |     done
  | }
  `----


1.1.5 Listing Sites
-------------------

  So far, we can create, delete, and backup NoPanel sites. What if we
  just want to list them? Due to the "convention over configuration"
  approach NoPanel takes, this is actually just a one liner, but we'll
  pack the functionality into the final script anyway.

  ,----
  | function list_sites {
  | find  /var/www/ -mindepth 1 -maxdepth 1 -type d -printf '%f\t%u\n' | grep -v ^\\. | grep -v ^html | grep -v ^htpasswd
  | }
  `----

  This simply finds all directories immediately beneath `/var/www' and
  filters out the default `html' directory that Ubuntu provides, as well
  as the `htpasswd' dir, where we keep Apache credential files (more on
  this later).


1.1.6 Managing Certificates
---------------------------

  The [Let's Encrypt Project] is a wonderful thing. They run a
  Certificate Authority and provide totally-automated provisioning of
  free SSL certificates. While they don't (yet) off all the goodies of a
  "real" CA like extended validation, and the supplied certs are only
  good for 90 days, it's still a really good option for most sites.

  NoPanel uses an HAProxy layer in front of all HTTP/HTTPS connections,
  which is the perfect place to terminate TLS connections. If you
  haven't already, use the `nopanel.sh install' command to install and
  configure HAProxy for your NoPanel environment.


  [Let's Encrypt Project] https://letsencrypt.org/


* 1.1.6.1 Installing or Renewing Certificates

  As mentioned, Let's Encrypt certificates have short validity periods,
  so unless you want to be manually validating and re-installing every 3
  months, it's best to put some automation in place.

  Our HAProxy config is our first ally in renewing our cert.

  In particular, we want to look at the `https-in' frontend. This will
  be doing some of the work for us.

  ,----
  | frontend https-in
  | 	bind *:443 ssl crt /etc/haproxy/certs/
  | 	http-request set-header X-Forwarded-Proto https
  | 
  | 	#mark ACME (Let's Encrypt) challenge requests
  | 	acl acme path_beg /.well-known/acme-challenge/
  | 
  | 	# domains in api.lst should be proxied to the API server
  | 	acl api_host hdr(Host) -f /etc/haproxy/api.lst
  | 	use_backend api if api_host !acme #unless they're ACME challenges
  | 
  | 	default_backend apache
  `----

  ,----
  | backend apache
  | 	server hweb 127.0.0.1:8080
  | 
  | backend api
  | 	server api api:80
  `----

  The `https-in' backend listens on port 443 on all interfaces, and
  terminates connections based on the certs it finds in
  `/etc/haproxy/certs'. For good measure, we also set the
  `X-Forwarded-Proto' header so that downstream applications know that
  we're terminating SSL ahead of them. We'll talk about the rest later,
  but the `acme' ACL will make sure that, not matter where any other
  traffic for this domain ultimately goes (to another box, to some
  non-Apache daemon locally, to the moon, whatever), that requests
  beginning with `/.well-known/acme-challenge' will be sent to Apache.

  This is important because Let's Encrypt validates your ownership of a
  domain, and therefore allows you to get a cert for it, by making your
  prove that you can post arbitrary files somewhere below that
  path. With that in place, we can build our certificate renewal
  function.

  As usual, we start off with some configuration and input checking:

  ,----
  | expiration_cutoff=14 #minimum number of days before expiration to renew the cert
  | domain="$1"
  | webroot="/var/www/$domain"
  | haproxy_cert_dir="/etc/haproxy/certs"
  | letsencrypt_basedir=/etc/letsencrypt
  | certfile="$letsencrypt_basedir/live/$domain/cert.pem"
  | email="inquiries@exactatechnologies.com"
  | 
  | if [[ ! -d "$webroot" ]]; then
  |     echo "There is no directory '$webroot'." 1>&2
  |     exit 1
  | fi
  `----

  In order to be nice to the Let's Encrypt server infrastructure, we
  don't actually want to call out to try to validate unless it's
  necessary. To do so, we'll calculaute how many days are left until
  expiration of our cert. If it's under `$expiration_cutoff', we'll just
  skip it for now. I set this to 2 weeks to give me plenty of time to
  manuall renew a cert if something goes wrong here. To do the actual
  calculation, we need to know what time it is now, in seconds since the
  Unix epoch. We also need to know when the cert expires, which we
  ascertain by using the `openssl' binary to dump the cert, then grep
  out the "Not After" header. Finally, we subtract the two and convert
  the result to days. For floating point calculations like this in shell
  scripts, I like to call out to `bc', the *nix arbitrary precision
  calculator. If there is no cert yet, assume it expires today to force
  a "renewal".

  ,----
  | if [[ -e "$certfile" ]]
  | then
  |     timestamp_now=$(date -d "now" +%s)
  |     expiration_timestamp=$(date -d "$(openssl x509 -in $certfile -text -noout|grep "Not After"| cut -c 25-)" +%s)
  |     days_until_expiration=$(echo \( $expiration_timestamp - $timestamp_now \) / 86400 | bc)
  | else
  |     days_until_expiration=0
  | fi
  `----

  The actual ACME validation is handled by the `letsencrypt' command. We
  use the `-w' switch to force it into "webroot" mode, using the Apache
  docroot to prove that we are able to post the validation key to
  `/.well-known/acme-challenge/'. If we succeed, we still need to stitch
  together the `fullchain.pem' and `privkey.pem' files that the
  `letsencrypt' tool gives us into one file, since that's the format
  HAProxy expects.

  Note that first we use `dig' to try and resolve the domain. If it
  doesn't even resolve, there's no point in trying to validate it.

  ,----
  | if [[ "$days_until_expiration" -lt "$expiration_cutoff" ]]
  | then
  |     for d in "$domain" "www.$domain"
  |     do
  | 	if [[ "$(dig +short $d)" != ""]]
  | 	then #the domain actually resolves, so let's go ahead
  | 	    if  letsencrypt certonly \
  | 			       --webroot \
  | 			       --keep-until-expiring \
  | 			       --email "$email" \
  | 			       --agree-tos \
  | 			       -w $webroot \
  | 			       -d $d
  | 	       then
  | 		   cat /etc/letsencrypt/live/$d/fullchain.pem \
  | 		       /etc/letsencrypt/live/$d/privkey.pem > $haproxy_cert_dir/$d.pem
  | 
  | 		   service haproxy reload
  | 	       fi
  | 	else # The domain doesn't even resolve. Either the
  | 	     # 'www.' subdomain for this site isn't used, or this is a
  | 	     # deprecated site. TODO: log/notify on this.
  | 	    echo ""
  | 	fi
  |     done
  | fi
  `----

  ,----
  |   function renew_cert {
  | <<renew_cert_input_check>>      
  | 
  | <<renew_cert_calculate>>
  | 
  | <<renew_cert_do_challenge>>
  |   }
  `----


1.2 Installation
~~~~~~~~~~~~~~~~

  So far, we've talked only about things that can be done once your
  server is already set up and running NoPanel. What, you might ask, do
  I do if I'm starting from a bare-nothing Ubuntu install? Fear
  not. NoPanel is intended to be simple to install. In fact, it's a
  single shellscript, `nopanel.sh', which you will want to copy
  somewhere in your path and `chmod +x'.

  ,----
  | ~# cp nopanel.sh /usr/bin
  `----

  Next, avail yourself of the `nopanel.sh install' command. It's quite a
  simple little function. First, we install all the necessary system
  packages:

  ,----
  | apt-get install -y haproxy apache2 bc letsencrypt php-imagick php-pear php-pecl-http php7.0 php7.0-bcmath php7.0-bz2 php7.0-cgi php7.0-cli php7.0-common php7.0-curl php7.0-dba php7.0-dev php7.0-enchant php7.0-fpm php7.0-gd php7.0-gmp php7.0-imap php7.0-interbase php7.0-intl php7.0-json php7.0-ldap php7.0-mbstring php7.0-mysql php7.0-sqlite3 php7.0-sybase php7.0-tidy php7.0-xml php7.0-xmlrpc php7.0-xsl php7.0-zip
  `----

  Next, we install the HAproxy configuration:

  ,----
  | {
  |     cat <<HAPROXYCONF
  |     <<haproxy_config>>
  | HAPROXYCONF
  | }>/etc/haproxy/haproxy.cfg.new
  `----

  Finally, start Apache and HAproxy. Actually, we stop them first, in
  case one or the other is already running.

  ,----
  | service apache2 stop
  | service apache2 start
  | 
  | service haproxy reload
  `----

  This leaves us with a few lines to get our system up and running:

  ,----
  | function install {
  | apt-get install -y haproxy apache2 bc letsencrypt php-imagick php-pear php-pecl-http php7.0 php7.0-bcmath php7.0-bz2 php7.0-cgi php7.0-cli php7.0-common php7.0-curl php7.0-dba php7.0-dev php7.0-enchant php7.0-fpm php7.0-gd php7.0-gmp php7.0-imap php7.0-interbase php7.0-intl php7.0-json php7.0-ldap php7.0-mbstring php7.0-mysql php7.0-sqlite3 php7.0-sybase php7.0-tidy php7.0-xml php7.0-xmlrpc php7.0-xsl php7.0-zip
  | 
  | {
  |     cat <<HAPROXYCONF
  |     global
  | 	    log /dev/log    local0
  | 	    log /dev/log    local1 notice
  | 	    chroot /var/lib/haproxy
  | 	    stats socket /run/haproxy/admin.sock mode 660 level admin
  | 	    stats timeout 30s
  | 	    user haproxy
  | 	    group haproxy
  | 	    daemon
  | 
  | 	    # Default SSL material locations
  | 	    ca-base /etc/ssl/certs
  | 	    crt-base /etc/ssl/private
  | 
  | 	    # Default ciphers to use on SSL-enabled listening sockets.
  | 	    # For more information, see ciphers(1SSL). This list is from:
  | 	    #  https://hynek.me/articles/hardening-your-web-servers-ssl-ciphers/
  | 	    ssl-default-bind-ciphers ECDH+AESGCM:DH+AESGCM:ECDH+AES256:DH+AES256:ECDH+AES128:DH+AES:ECDH+3DES:DH+3DES:RSA+AESGCM:RSA+AES:RSA+3DES:!aNULL:!MD5:!DSS
  | 	    ssl-default-bind-options no-sslv3
  | 
  |     defaults
  | 	    log     global
  | 	    mode    http
  | 	    option  httplog
  | 	    option  dontlognull
  | 	    timeout connect 5000
  | 	    timeout client  50000
  | 	    timeout server  50000
  | 	    errorfile 400 /etc/haproxy/errors/400.http
  | 	    errorfile 403 /etc/haproxy/errors/403.http
  | 	    errorfile 408 /etc/haproxy/errors/408.http
  | 	    errorfile 500 /etc/haproxy/errors/500.http
  | 	    errorfile 502 /etc/haproxy/errors/502.http
  | 	    errorfile 503 /etc/haproxy/errors/503.http
  | 	    errorfile 504 /etc/haproxy/errors/504.http
  | 
  |     frontend http-in
  | 	    bind *:80
  | 
  | 	    #domains in no_ssl.lst shouldn't be redirected to HTTPS
  | 	    acl no_ssl_host hdr(Host) -f /etc/haproxy/no_ssl.lst
  | 	    redirect scheme https code 301 if !{ ssl_fc } !no_ssl_host
  | 
  | 	    #mark ACME (Let's Encrypt) challenge requests
  | 	    acl acme path_beg /.well-known/acme-challenge/
  | 
  | 	    # domains in api.lst should be proxied to the API server
  | 	    acl api_host hdr(Host) -f /etc/haproxy/api.lst
  | 	    use_backend api if api_host !acme #unless they're ACME challenges
  | 
  | 	    default_backend apache
  | 
  |     frontend https-in
  | 	    bind *:443 ssl crt /etc/haproxy/certs/
  | 	    http-request set-header X-Forwarded-Proto https
  | 
  | 	    #mark ACME (Let's Encrypt) challenge requests
  | 	    acl acme path_beg /.well-known/acme-challenge/
  | 
  | 	    # domains in api.lst should be proxied to the API server
  | 	    acl api_host hdr(Host) -f /etc/haproxy/api.lst
  | 	    use_backend api if api_host !acme #unless they're ACME challenges
  | 
  | 	    default_backend apache
  | 
  |     backend apache
  | 	    server hweb 127.0.0.1:8080
  | 
  |     backend api
  | 	    server api api:80
  | HAPROXYCONF
  | }>/etc/haproxy/haproxy.cfg.new
  | 
  | service apache2 stop
  | service apache2 start
  | 
  | service haproxy reload
  | }
  `----