Wrangling Toy Applications with Supervisor and Nginx

I am a toymaker. Some toys are simple, others complex, and some are just plain wacky. What unites these toys are that they are:

  • Awesome
  • Stand alone applications that don’t share a common codebase
  • Written in a variety of languages

Showcasing, hosting, and monitoring applications can be difficult, especially if you want to have them all live under one domain or URL path (e.g. http://gleitzman.com/apps/appname). Whether you’re working in Python, Javascript, or Ruby you need a reverse-proxy that can receive requests and delegate to a specific application’s server. I’ve tried a number of techniques but ultimately landed with supervisor and nginx.

Supervisor

Web hosting is finicky. It can crash. Networks can fail. Things fall apart. Enter supervisor, a system for controlling UNIX processes. It in a nutshell it starts a process with a certain process id and then makes sure that process is healthy. Configuring supervisor is a snap. Let’s start with mahjong, a Node.js artificial intelligence for the popular game played in China, Japan, Korea, and New Jersey. The supervisor configuration file looks like this:

/etc/supervisor/conf.d/mahjong.conf


[program:mahjong]
environment = NODE_ENV=production
directory = /path/to/mahjong
command = node app.js
stdout_logfile = /path/to/node_supervisor.log
redirect_stderr = true ; Save stderr in the same log
autostart = true ; Start at login
autorestart = true ; Restart if necessary

Save the file and run sudo supervisorctl update followed by sudo supervisorctl restart You can check the status of the application with sudo supervisorctl status

If your application requires a little more configuration (e.g. loading a virtualenv) you can pass a shell script to supervisor. Here’s the configuration for Symphony of Satellites, a Flask application served by gunicorn and written in Python that turns satellite orbits into music. We begin with a shell script for running the application:

/path/to/spaceharp_start.sh


#!/bin/bash
VIRTUALENV_NAME="spaceharp" # Name of the application
ROOTDIR=/path/to/$VIRTUALENV_NAME # Project root dir
SOCKFILE=/path/to/$VIRTUALENV_NAME.sock  # Unix socket for communication
PORT=9001
NUM_WORKERS=5 # (2 * num_cores + 1)
WSGI_MODULE=app # WSGI module name

echo "Starting $VIRTUALENV_NAME"

# Activate the virtual environment
cd $ROOTDIR
source /path/to/$VIRTUALENV_NAME/bin/activate

# Create the run directory if it doesn't exist
RUNDIR=$(dirname $SOCKFILE)
test -d $RUNDIR || mkdir -p $RUNDIR

# Start your Gunicorn process
# With supervisor don't use --daemon
exec gunicorn ${WSGI_MODULE}:app \
  --bind=unix:$SOCKFILE
  --name $VIRTUALENV_NAME \
  --workers $NUM_WORKERS \
  --log-level=debug \

Now, we’ll have a similar supervisor configuration file for this application:

/etc/supervisor/conf.d/spaceharp.conf


[program:spaceharp]
command = /path/to/spaceharp_start.sh
stdout_logfile = /path/to/gunicorn_supervisor.log
redirect_stderr = true ; Save stderr in the same log
autostart=true
autorestart=true

Just like we did before, run sudo supervisorctl update followed by sudo supervisorctl restart

Nginx

Now that we have our applications monitored and starting automatically we can perform URL delegation through a reverse-proxy, an application that handles requests, passing them to other servers such as Node.js’ built-in server or gunicorn. The configuration file looks like this:

/etc/nginx/sites-available/default


# There are three upstream applications:
# mahjong, spaceharp, and a generic apache installation.
upstream mahjong {
  server localhost:8081;
}
upstream spaceharp {
  server unix:/path/to/spaceharp.sock fail_timeout=0;
}
upstream apache {
  server localhost:8082;
}

server {
  listen   80 default;
  server_name _;

  access_log  /var/log/nginx/localhost.access.log;
  error_log /var/log/nginx/localhost.error.log;

  location /apps/mahjong {
    # When the user accesses yoursite.com/apps/mahjong
    # forward the request to the upstream server listed above
    proxy_set_header Host $host;
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    proxy_set_header X-Real-IP $remote_addr;
    proxy_set_header X-Scheme $scheme;
    proxy_set_header X-Script-Name /apps/mahjong;
    # rewrite URLs beginning with /apps/mahjong to /
    rewrite ^/apps/mahjong/(.*) /$1 break;
    proxy_pass http://mahjong;
  }

  location /apps/spaceharp {
    # When the user accesses yoursite.com/apps/spaceharp
    # forward the request to the upstream server listed above
    proxy_pass http://spaceharp;
    proxy_set_header Host $host;
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    proxy_set_header X-Scheme $scheme;
    proxy_set_header X-Real-IP $remote_addr;
    proxy_set_header X-Script-Name /apps/spaceharp;
  }

  location / {
    # Requests to yoursite.com/* are forwarded to apache
    proxy_set_header Host $host;
    proxy_set_header    X-Real-IP       $remote_addr;
    proxy_set_header    X-Forwarded-for $remote_addr;
    proxy_pass http://apache;

  }
}

Now that you have the configuration complete you can see it in action by visiting http://gleitzman.com – The main site, served via nginx to Apache http://gleitzman.com/apps/mahjong/game – Mahjong, served via nginx to Node.js http://gleitzman.com/apps/spaceharp – Symphony of Satellites, served via nginx to gunicorn

One particular gotcha is that your application needs to be aware of the reverse proxy. If your application expects static files and AJAX requests to be served at /public or /ajax, they now needed to be served at /apps/spaceharp/public and /apps/spaceharp/ajax, respectively. Flask provides a nice little snippet for handling this use case based on the “X-Script-Name” header which was added by the nginx configuration above. I haven’t found a similar snippet for Node.js but you can use a small function that checks for req.headers['x-script-name'] and appends it to the beginning of static URLs.

You can also use nginx’s rewrite rule to convert a path like /apps/mahjong/game to /game. The syntax is: rewrite ^/apps/mahjong/(.*) /$1 break;

Update

Special thanks to Matt Spitz for pointing out that you can avoid the whole HTTP-header-as-a-prefix issue by hosting your applications on different subdomains. Instead of whatever.com/apps/myapp and whatever.com/apps/yourapp, you’d have myapp.whatever.com and yourapp.whatever.com. No rewriting of URLs, since everything’s served relatively as /.

In nginx:


server {
    listen 80;
    server_name myapp.whatever.com;
    location / {
        ...
        proxy_pass myapp_upstream;
    }
}

server {
    listen 80;
    server_name yourapp.whatever.com;
    location / {
        ...
        proxy_pass yourapp_upstream;
    }
}

Neato!