There wasn’t much information available when I was first learning how to build a production environment for Racemetric, a Haskell Snap Framework web app. I was looking for a comprehensive overview of all the operational pieces. Given the lack of such a guide, the following is my production environment overview.

Overview

At the highest level AWS EC2 is used for hosting an Ubuntu image. When the image starts, the system init process, Upstart, starts Angel. Angel then starts Racemetric whenever it’s not started. Nginx is also started when the image starts and reverse proxies all outside requests to the app server.

Upstart

Upstart is an event-based replacement for the /sbin/init daemon which handles starting of tasks and services during boot, stopping them during shutdown and supervising them while the system is running.

Upstart website

Upstart basically starts Angel as the racemetric user. It’s currently the official Ubuntu init process but that’s changing to systemd in future releases.

upstart.conf

start on startup
setuid racemetric
exec angel angel.conf

Angel

Angel is a Haskell-based process monitor. It has one task: start the web app process when it’s not started.

angel.conf

racemetric {
    # -I0 = Turn off idle GC, -A4M = 4megs of initial heap, -qg1 = Parallel GC in gen 1
    exec = "racemetric -p 8080 -e prod +RTS -I0 -A4M -qg1"
    stdout = "log/stdout.log"
    stderr = "log/stderr.log"
}

Nginx

Nginx is responsible for a few things:

  1. HTTPS TLS connection handling.
  2. Stripping the “www” from the domain.
  3. Static asset hosting at the /static/ location.
  4. Static robots.txt hosting.
  5. Reverse proxying all HTTP requests to the Snap app HTTP server.
  6. GZIP Compression

Why not just expose the Snap web server to the open Internet? Snap can handle HTTPS as well as any arbitrary filtering and header management just like Nginx. The following are my main reasons:

  1. Flexibility: Nginx is designed for config flexability. HTTPS especially has many different options like the cypher set and supported protocols.
  2. Config hotswapping: Nginx supports no-downtime config changes. The app server can be updated behind the scenes and Nginx will allow all currently open connections to complete while send all new connections to the updated app.
  3. Separation of concerns: The app server is concerned with dynamically generating content and handling sessions while Nginx is responsible for static content hosting and load balancing.
  4. Unprivileged: The app server can run with unelevated privilages on unprotected ports (like 8080).

In short, separation of concerns is huge. Let the app server handle dynamic content generation and Nginx handle everything else.

nginx.conf

gzip_vary on;
gzip_proxied any;
gzip_comp_level 6;
gzip_buffers 16 8k;
gzip_http_version 1.1;
gzip_types text/plain text/css application/json application/x-javascript text/xml application/xml application/xml+rss text/javascript;

ssl_certificate ssl-bundle.crt;
ssl_certificate_key ssl-private.key;
ssl_session_timeout 5m;
ssl_protocols TLSv1 TLSv1.1 TLSv1.2;
ssl_ciphers ALL:!aNULL:!ADH:!eNULL:!LOW:!EXP:RC4+RSA:+HIGH:+MEDIUM;
ssl_prefer_server_ciphers on;

server {
    listen 80 default_server;
    listen 443 ssl default_server;
    server_name _;
    return 444;
}

server {
    listen 443 ssl;
    server_name www.racemetric.com;
    return 301 https://racemetric.com$request_uri;
}

server {
    listen 80;
    server_name racemetric.com www.racemetric.com;
    return 301 https://racemetric.com$request_uri;
}

server {
    listen 443 ssl;
    server_name racemetric.com;
    add_header Strict-Transport-Security max-age=15768000;

    location / {
        proxy_pass http://127.0.0.1:8080www.racemetric.com;
    return 301 https://racemetric.com$request_uri;
}

server {
    listen 80;
    server_name racemetric.com www.racemetric.com;
    return 301 https://racemetric.com$request_uri;
}

server {
    listen 443 ssl;
    server_name racemetric.com;
    add_header Strict-Transport-Security max-age=15768000;

    location / {
        proxy_pass http://127.0.0.1:8080;
    }

    location /static/ {
        alias racemetric/static/;
        autoindex off;
        expires max;
        add_header Cache-Control public;
    }

    location /robots.txt {
        alias racemetric/static/internal/robots.txt;
    }
}