Some simple rules for a modern web application install

1. Do not keep configuration for different environments in code

You can read it in via an external storage or configuration file that is set by environment variables in the local setup. For example, consider setting the configuration file path in your Nginx config for a fastcgi deployment, e.g.:

fastcgi_param MY_CONFIG /path/to/config-file.json

And then read this parameter in your code and parse the attached file.

2. Do include example configurations in your source distribution

If you include real examples of configuration setups in your source code distribution it will be much easier for anyone coming after you to set up systems. Put example config files for the relevant software in a config directory with a file name or path that indicates what service it is and what type of example setup you’re providing, e.g. config/devhost.nginx.conf and config/livehost.nginx.conf

3. Do provide an SQL file for creating a minimal database setup for the system, or equivalent scripts/commands. If you use standard frameworks like Ruby on Rails, follow their conventions with regards to how a system is set up.

4. Do not allow general execution of code — limit it to exactly the locations you need to execute code for. This will limit the exploitability of your site significantly.

5. Ensure all static files are served as static files and not sent through your dynamic app service — GIFs, PNGs and similar can be served by the front web server which will lessen the load on your server

6. Employ caching — Read up on FastCGI cache if using FastCGI — or Proxy caching if you’re using a reverse proxy. Ensure that you cache requests if only for a limited amount of time. Configure the cache so that stale files are used in the case of server errors or temporary unavailability. This will lessen perceived downtime.

7. Always have a fallback. Don’t have a standard 503 error page appear if your application layer Web server is down for example — set up a static branded page that sends a simple, easy to understand message that will work well with YOUR users. This could be an auto reloading static page, where you cache it at the front end for a limited amount of time, for example. Never think this won’t happen — it will.

8. Configure database connections and similar to private aliases. For example, connect to the host ‘db’ and set up the host ‘db’ in your hosts configuration to point to the right private IP within your VPC. This allows you to manage these aliases on a host basis, which is useful for redirecting traffic during planned downtime — or to redistribute loads if not using load balancing at that layer. And if you are, the alias can point to the load balancer. Even then it’ll save you a DNS lookup, or worse, tying the configuration to a specific IP.

9. Set up worker scripts in Cron — but check for existing processes — always use a PID file

10. For any daemon processes, make sure to set up systemd scripts and appropriate log redirection and log rotation

(Work in progress)

Blanket rewrite to HTTPS (Nginx)

This is simple — but I’ve seen it done in a few different ways. This is the shortest possible recipe:

server {
	listen 80 default_server;
	server_name _;
	return 301 https://$host$request_uri;

Ensure that you only listen for 443 ssl in other config files, e.g.

server {
    root /home/stephan/src/testsite/www;
    index index.html;


    #listen 80 default_server;
    listen 443 ssl;

    ssl_certificate /etc/letsencrypt/live/testsite/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/testsite/privkey.pem;
    ssl_trusted_certificate /etc/letsencrypt/live/testsite/chain.pem;

Some docker-compose recipes

This article will go through a set of simple step by step instructions to set up Docker containers for various popular services using docker-compose.

Always remember that you can combine or split these recipes as you want so as to create your perfect mix of services; these are mostly laid out according to how I first used them or how I typically use them.


This setup assumes Docker installed on a Mac. Other operating systems will vary a bit but the differences shouldn’t be too significant.

Each service can be run using:

docker-compose up

Or, perhaps more typically, if you don’t want to bind to the terminal:

docker-compose up -d

Files provided expose services to equivalent local ports on your workstation, so before you get started on each one, make sure you don’t have same services running locally (e.g. a local Apache server would stop the Nginx setup below from working as it would already have claimed port 80 on your computer).

Also, directories mounted into Docker images in the examples below will typically use my username (replace with yours) and assumes source directories are organised under ~/src — adjust so it matches your setup. Note that ~/src/docker is used as a directory to store data on the host file system in the examples below — for example the MySQL data directory. This allows you more transparent control of files from Docker images and improved persistence, but feel free to adjust as you see fit.

PHP + MySQL + Nginx


version: '3'

        image: mariadb:latest
        container_name: mysql
        restart: always
            - "3306:3306"
            - /Users/stephan/src/docker/mysql/data:/var/lib/mysql
            - /Users/stephan/src:/home/stephan/src
            - /Users/stephan/src/docker/mysql/etc:/etc/mysql/conf.d
            MYSQL_ROOT_PASSWORD: root-password
            MYSQL_DATABASE: database-name
            MYSQL_USER: database-user
            MYSQL_PASSWORD: database-user-passsword
            context: ./
            dockerfile: Dockerfile-PHP71
        container_name: php
            - /Users/stephan/src:/home/stephan/src
            - /Users/stephan/src/docker/nginx/sites-enabled:/etc/nginx/sites-enabled
            - db
        image: nginx:stable
        container_name: web
            - "80:80"
            - "443:443"
            - /Users/stephan/src:/home/stephan/src
            - /Users/stephan/src/docker/nginx/sites-enabled:/etc/nginx/conf.d
            - /Users/stephan/src/docker/nginx/certs:/etc/nginx/certs
            - php

We need an extra file to explicitly install some PHP extensions for it to work with MySQL, store a file in the same directory called “Dockerfile-PHP71” (this is referred to in the Compose file above):

FROM php:7.1-fpm
RUN docker-php-ext-install pdo pdo_mysql
RUN docker-php-ext-install mysqli

To run, just run:

docker-compose up -d

Once running, if you need to reload Nginx configuration (which is in a directory that’s mounted from your computer’s file structure if you followed the pattern above), simply execute:

docker exec -ti web bash -c 'kill -HUP 1'

You will now be able to access the web server by pointing your web browser to your local host (alternatively via entries in the hosts file pointing to

MySQL is available on port 3306 (the standard port) on — note that you cannot connect naming localhost, as MySQL clients will then typically try to use socket connections.

If you need to connect to the MySQL host to add user privileges, just run:

docker exec -it mysql mysql -p


GRANT ALL PRIVILEGES ON *.* TO myuser@`%` IDENTIFIED BY 'mypassword';


This is a fairly simple “Kafka to go” setup for experimentation with the platform:


version: '3'

        container_name: zookeeper
        image: jplock/zookeeper
        restart: always
            - "2181:2181"
        container_name: kafka
        image: ches/kafka
        restart: always
            - "9092:9092"
            ZOOKEEPER_IP: zookeeper
            - zookeeper

Replace above with the IP of your local workstation.

After getting the images online, you can connect to the Kafka instance to create a topic:

docker exec -it kafka bash

Once in run this:

export JMX_PORT=9998 --create --topic test-topic --replication-factor 1 --partitions 1 --zookeeper zookeeper:2181

(Note that the export line is needed before you run any command line tools but will persist throughout your command line session — the CLI tools will otherwise try to bind to the same port as the main Kafka instance which will lead to a conflict; the port used is not important)

Which will create a new Kafka Topic; we can now test adding to the topic and read it back via the CLI as such: --topic test-topic --broker-list kafka:9092

Then enter three sample messages (each line is a message):
… message 1 …
… message 2 …
… message 3 …

End with Ctrl+D

This can now be replayed using a consumer: --topic test-topic --from-beginning --zookeeper zookeeper:2181

This will play back all messages from the beginning and then listen for further messages; hit Ctrl+C to abort at any time.

Cloudonomics: Virtual Server Instances

As part of any service provider’s work, particularly one focusing on cost sensitive areas (our clients are mostly operating in Africa), is ensuring we get the best value out of our service providers.

Using cloud services has been the obvious choice for us for some time, providing isolation between clients, (near) instant scalability, and the flexibility to spin up and down test instances and deployments.

As part of our recent work to find the best cloud hosting provider for a mid-size site (ca. 10 mill page views/month), the advantages of a multi cloud strategy soon became obvious.

I’m sharing some of our findings here in the hope that this might help others, as I had little luck in finding similar figures / stats myself before getting started.

The three/four providers evaluated were AWS, Rackspace, Google Cloud and Microsoft Azure (which quickly proved the wrong choice for us so is dropped from some of the below).


Note: There are a lot of different instance types on offer. The instance types selected where there isn’t a clear match may not be the one you would have chosen. Generally, we’ve tried to select the cheapest instance with at least as much CPU power and memory.

Below you’ll find numbers and graphs based on some of each of the providers’ standard instance types


Amazon Google Rackspace AWS 1y CPU MEM
t2.nano 4.60 10.98 27.01 3.65 0.5 1
t2.micro 9.49 21.95 27.01 7.30 1 1
t2.small 18.25 24.55 54.02 14.60 1 2
t2.medium 36.00 49.10 108.04 29.20 2 4
t2.large 72.72 59.48 108.04 58.40 2 8
m4.large 85.68 59.48 108.04 65.70 2 8
t2.xlarge 145.44 118.96 216.08 116.80 4 16
m4.xlarge 171.36 118.96 216.08 131.40 4 16



Amazon Google Rackspace AWS 1y CPU MEM
n1-standard-1 18.25 26.36 58.40 14.60 1 4
n1-standard-2 73.73 52.72 116.80 58.40 2 8
n1-standard-4 147.46 105.44 233.60 116.80 4 16
n1-standard-8 346.75 210.87 467.20 262.80 8 32
n1-standard-16 693.50 421.75 934.40 526.33 16 64


Amazon Google Rackspace AWS 1y RSUK £
1 CPU – 1 GB 9.49 21.95 27.01 7.30 25.18
2 CPU – 2 GB 36.50 43.91 54.02 29.20 50.35
4 CPU – 4 GB 147.46 87.81 108.04 116.80 100.70
8 CPU – 8 GB 294.92 175.63 216.08 234.33 201.41

RSUK is the pricing in GBP from Rackspace UK converted into USD at the daily rate of 14 April 2017.


For virtual servers, Google seems to offer the best value with more than 2 CPUs while Amazon offers the best value with 2 CPUs or below.

It’s slightly more complex than that, of course.

The performance of the clouds vary a bit as well, but in our experience if anything this favours Google.

Some services are not available on all clouds — for example, we use Amazon for transcoding, on the other hand we can’t use Amazon’s CloudFront due to poor coverage in Africa. Rackspace offer a decent deal on Akamai CDN services.

Generally, the best approach seems to be to pick and mix.