Here's another interesting story about how we do things at Browserling. This time the story is about how we use ploy to deploy code at Browserling and Testling.

First off, what is ploy? Ploy is a deployment system that we created at Browserling, that includes a http router, a git endpoint and a process manager all in one. You just push your code at ploy, and it deploys it and runs the necessary processes.

Ploy overview

Here's a simple overview and an example of how to get started with ploy. First, setup auth.json that ploy will use for git authentication when you git push at it:

$ cat auth.json
{
  "pkrumins" : "password",
  "substack" : "password2"
}

Then setup a git remote in your app's repository:

$ git remote add ploy http://pkrumins:password@myapp.com/_ploy/myapp.git

Ploy uses the special /_ploy/NAME.git url as a git endpoint.

Then start ploy:

$ ploy ./myapp -a auth.json --port 80

This will start ploy on port 80 and use ./myapp directory to store the repository and logs. Surely you never want to run any code as a privileged user, so see the section below on how to setup ploy on port 8080 and redirect port 80 with iptables to port 8080.

Your app should have a package.json file, such as the following:

{
  "name": "myapp",
  "private": true,
  "version": "0.0.1",
  "scripts": {
    "start": {
      "index": "node server.js",
      "stats": "node stats/server.js",
      "_auth": "node auth/server.js"
    },
    "postinstall": "./install.sh"
  }
}

The scripts.start lists processes that ploy will manage. The index process will handle requests to www.myapp.com and myapp.com, whereas stats will handle requests to the stats.myapp.com subdomain. The process starting with an underscore _auth is a non-http process that will simply be run by ploy and won't be mapped to any http requests.

Now git push your code to ploy:

$ git push ploy master

When you push code at a ploy server, your processes will be started and any previous processes running under the same branch name will be killed. Ploy does not detach your server processes. When the ploy server goes down, it takes the processes it started with it. However, when the ploy server is started back up, it will attempt to restart all of the processes it was previously running.

Before starting the processes, ploy will run npm install . that will handle the scripts.postinstall (also install and preinstall) hooks. Ploy'll set the PORT environment variable that your application should listen to. It will then route all the http requests on port 80 to your application listening on PORT.

You can also setup staging branches easily with ploy, just push to ploy like this:

$ git push ploy master:staging

This will make the staging branch available at staging.myapp.com (given that you've an A DNS record for staging.myapp.com that points to the same server that myapp.com does). The stats process will be available at staging.stats.myapp.com.

Once you're done with your staging branch, you can simply move staging to production (master branch) by running ploy mv staging master. That will re-route the connections and you'll instantly be running your staging in production.

Ploy provides many commands for managing processes, logs, and branches. You can list the processes by running ploy ls from your repository. You can restart them by running ploy restart <process name>. You can view the logs through ploy log, and manage branches branches through ploy mv, ploy rm commands, and ploy redeploy commands.

Ploy makes it really easy to deploy your apps to production and staging! Read more about it on ploy's project page!

Ploy at Browserling and Testling

Now let's turn to how we use ploy in production to deploy Browserling and Testling. We've a more complex ploy setup, where we use ssl certificates for running https, and a request router for more complicated routing.

Here's how we run ploy for Testling:

ploy ./testling \
  -a auth.json \
  --port 8000 \
  --sslPort 8443 \
  --ca=./certs/bundle.crt \
  --cert=./certs/ci_testling_com.crt \
  --key=./certs/server.key \
  -f testling-router.js
  • The -a argument specifies the authentication info for the git endpoint.
  • The --port argument specifies the http port that ploy will listen on.
  • The --sslPort argument specifies the https port that ploy will listen on.
  • The --ca, --cert, and --key argument setup the ssl cert.
  • The -f argument specifies the http router that ploy will use to route the connections.

We run ploy as non-root and it listens on port 8000 for http connections, and on port 8443 for https connections. We've the following iptables rules setup on the testling server that redirect ports 80 to 8000, and 443 to 8443:

iptables -A PREROUTING -t nat -p tcp --dport 80 -j REDIRECT --to-port 8000
iptables -t nat -I OUTPUT -p tcp -d 127.0.0.1 --dport 80 -j REDIRECT --to-ports 8000
iptables -t nat -I OUTPUT -p tcp -d testling.com --dport 80 -j REDIRECT --to-ports 8000

iptables -A PREROUTING -t nat -p tcp --dport 443 -j REDIRECT --to-port 8443
iptables -t nat -I OUTPUT -p tcp -d 127.0.0.1 --dport 443 -j REDIRECT --to-ports 8443
iptables -t nat -I OUTPUT -p tcp -d testling.com --dport 443 -j REDIRECT --to-ports 8443

Auth file auth.json contains the user access info, in our case it's just me and substack:

{ "pkrumins": "pass1", "substack": "pass2" }

The router file is used in more complicated http routing cases. If we didn't have the testling-router.js router, then all the requests to ports 80 and 443 would simply be routed to app's PORT (environment variable that was set by ploy before it started the processes), but in our case we redirect all unencrypted http connections to https:

module.exports = function (req, res, bounce) {
    if (!req.connection.encrypted) {
        if (req.headers.host) {
            var hostname = req.headers.host.split(':')[0];
            if (hostname == 'ci.testling.com') {
                for (var i = 0; i < this.bouncers.length; i++) {
                    if (!this.bouncers[i].key) continue;

                    res.statusCode = 302;
                    res.setHeader('location', 'https://ci.testling.com' + req.url);
                    return res.end();
                }
                res.statusCode = 404;
                return res.end('no https endpoint configured\n');
            }
        }
    }
    bounce()
};

What this router does is it checks if a connection is encrypted, and if it's not, it simply sets the Location: https://ci.testling.com/URL header and the browser redirects the visitors to https.

You can do all kinds of crazy things with a router, for example, route based on the source/destination IP, or route based on http headers, route based on date/time, etc.

Here's how the root directory package.json looks for Testling:

{
  "name": "testling-ci",
  "private": true,
  "version": "0.0.1",
  "scripts": {
    "start": {
      "ci": "node ci/server.js",
      "git": "node git/server.js"
    },
    "postinstall": "./install.sh"
  }
}

And here's how the postinstall script looks like:

#!/bin/bash
#

(cd ci; npm install .)
(cd git; npm install .)

What this script does is it simply goes into the ci/ and git/ directories and npm installs the modules. After it's done, ploy starts the scripts.start.ci and scripts.start.git processes that map to http://ci.testling.com and http://git.testling.com subdomains.

Browserling uses pretty much the same arguments to ploy but has a much more complicated router that uses seaport:

var seaport = require('seaport');
var config = require('figc')(__dirname + '/config.json');
var pick = require('deck').pick;

var ports = seaport.connect(config.seaport);
ports.on('connect', function () { console.log('browserling-router.js: connected to seaport') });
ports.on('disconnect', function () { console.log('browserling-router.js: disconnected from seaport') });

module.exports = function (req, res, bounce) {
    if (req.headers.host && req.headers.host.split(':')[0] == 'status.browserling.com') {
        var ps = ports.query('web.status')
        if (ps.length) {
            return bounce(pick(ps), {
                headers: { 'x-forwarded-for': req.connection.remoteAddress }
            });
        }
        res.statusCode = 404;
        return res.end('no matching services are running for status.browserling.com');
    }

    if (req.headers.host && req.headers.host.split(':')[0] == 'browserling.com') {
        console.log(req.method + ' request from ' + req.connection.remoteAddress + ' to ' + req.url);
        if (req.connection.encrypted || req.url == '/account/verify.json') {
            var ps = ports.query('web.browserling');
            if (ps.length) {
                return bounce(pick(ps), {
                    headers: { 'x-forwarded-for': req.connection.remoteAddress }
                });
            }
            res.statusCode = 404;
            return res.end('no matching services are running for browserling.com\n');
        }
        else {
            res.statusCode = 302;
            res.setHeader('location', 'https://browserling.com' + req.url);
            return res.end();
        }
    }
    else if (req.headers.host && req.headers.host.split(':')[0] == 'www.browserling.com') {
        res.statusCode = 302;
        res.setHeader('location', 'https://browserling.com' + req.url);
        return res.end();
    }

    bounce()
};

At Browserling all our services are registered with seaport, so the routing no longer happens to the port in the PORT environment variable. Instead the the application's port is requested through seaport's ports.query. I'll write more about how we use seaport at Brwoserling some time later.

Ploy has been really wonderful to work with. If you haven't used it and you're looking for a deployment system for node (and not just node!), definitely try it out!

Until next time!