Wednesday, September 17, 2014

Setting up "Heroku like" git push deploys on a VPS is so easy

I was reading about Docker closing a $40M series C round this morning. While containerization is extremely useful at large scale, I think that the vast majority of individual developers and small teams write many web applications that don't need to scale beyond a beefed up VPS or single physical server.

For a good developer experience it is difficult to beat a slightly expensive but convenient PaaS like Heroku. However, if you have many small web app projects and experiments then hosting on a PaaS and paying $30-$50/month per application can add up, year after year. If you need failover and scalability, then paying for a PaaS or implementing a more failsafe system on AWS makes sense. For experimental projects that don't need close to 100% uptime, I set up a .git/hooks/post-commit git hook like this:

./rsync.sh
ssh [email protected] 'bash -s' < run.sh
I have my DNS setup for myappname.com (this is not a real domain, I am using it as an example) and all other domains for my example/experimental web apps point to the IP address of my large VPS. My rsync.sh files look like this:
rsync -e "ssh" -avz --delete --delete-excluded  \
   --exclude-from=/Users/mark/Code/mywebapps/myappname.com/rsync_exclude \
   /Users/mark/BITBUCKET/myappname.com  [email protected].com:/home/mark/
In my rsync_exclude file I specify to not copy my .git folder to the server:
.git
The run.sh file that gets remotely executed on my server looks like this:
#! /bin/bash

ps aux | grep -e 'myappname.com' | grep -v grep | awk '{print $2}' | xargs -i kill {}
(cd myappname.com; lein deps; nohup lein trampoline run prod > out.log&)
This is the pattern I use for running Clojure web apps. Running Ruby/Sinatra, Haskell, and Java apps is similar.

Since I tend to run many small experiments on a single large VPS, I use entries like the following in my /etc/rc.local file to restart all applications if I reboot the VPS:

(cd /home/mark/myappname.com ; su mark -c 'nohup lein trampoline run prod > out.log&') &

I use an account on the server that does not have root or sudo privileges so my web apps use non-privileged ports and I use nginx as a proxy. In my nginx.conf file, I have entries like the following to map non-privileged to virtual domain names:

 server {
    listen       80;
    server_name myappname.com www.myappname.com;
    location / {
      proxy_pass http://localhost:7070;
      proxy_redirect off;
      proxy_set_header Host $host;
      proxy_set_header X-Real-IP $remote_addr;
      proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    }
    error_page 500 502 503 504  /error.html;
    location = /error.html {
             root  /etc/nginx;
    }
 }
In this example, the myappname.com application is running on the non-privileged port 7070 and this app would be accessed as http://myappname.com or http://www.myappname.com. On my laptop, just doing a git push has the new version of my app running on my server in a few seconds.

1 comment:

Mark Watson, author and consultant said...

Addition 2016/04/13: Heroku now has a $7/month hobbyist plan for running always on web apps. I tent to use their service for all of my small scale web apps now.