August 28, 2014

Deploying a Clojure web app to a VPS

So you wrote an app. Great! Next step is to put it somewhere where people can use it. This tutorial will walk you through the process of deploying your app.

Our goal is to take our project, which runs locally on our dev machine on port 8080, and turn it into a big-boy app which:

  • Runs on port 80 (i.e. the default http port)
  • Starts automatically (via upstart)
  • Runs proxied behind Nginx, since nginx can serve up static assets more effectively

We’ll host our app on DigitalOcean, which seems to be the best deal going in terms of running simple VMs.

If you don’t have a test app to follow along with, you can see my tutorial to make one in 15 minutes

Getting Started

Python (Flask, Django) and Ruby (Sinatra, Rails) generally suggest that you use a different application server for production use than for development (i.e. Puma instead of WEBrick).

With clojure applications, it is considered both common and reasonable to just serve the whole thing with ring-jetty or perhaps httpkit. There are many reasons for this, most of which I haven’t studied, but I’ll just wave my hands here and say it’s because Clojure, being a JVM affair, does concurrent connections with threads instead of processes. I don’t think I’m too far off there.

It is, however, important that your app be served with the appropriate ring middleware. Ring includes some middleware that’s helpful for development, like wrap-reload and wrap-stacktrace, that you don’t want running in production. My preferred way is to create two handlers, and then switch between them with an environment variable present only on the dev machine:


(defn prod-handler []
    (-> app
        ; ... ring middleware))

(defn dev-handler []
    (-> (prod-handler)
        wrap-reload
        wrap-stacktrace))

(defn handler []
  (let [env (get (System/getenv) "MYPROJECT_ENVIRONMENT" "production")]
    (case env
      "development" (dev-handler)
      (prod-handler))))

Create a DigitalOcean server

How to set up a new server is a bit out of scope for this tutorial, but you should get an Ubuntu instance of your choice ready to go before continuing. For DigitalOcean, try following this getting started article to set up your droplet, followed by this one to get your ubuntu server started.

Get your app onto the server

There are main ways to deploy a java app, but the simplest is probably to pack up the whole thing as a jar, upload it to the server, and run java -jar <xxx>, so that’s the route we’ll take.

For your uberjar to work, you’ll need to make sure your project.clj includes a :main setting to tell it which class to run:

:main myproject.core

Now, run lein uberjar. It should tell you what jar file was created. Next, upload that jar to your server:

$ scp username@<domain_or_ip>:~ <jar_file>

Then, ssh into your server and run the jar using java -jar <the_jar>. You should now be able to open a browser and navigate to http://<domain_or_ip>:8080, and see your app. Great!

Troubleshooting: If you can’t see your application, check that your ufw firewall is allowing traffic to port 8080. You’ll want to disable this later, but open up that port for testing.

Configure upstart

Using upstart will automatically manage the lifecycle of your app, starting it up when the server starts and so forth. Since we’re just running a jar, configuration is very easy. Put this in a file called /etc/init/myproject.conf:

description "Run my project's jar"

start on runlevel startup
stop on runlevel shutdown

respawn

exec java -jar /path/to/my_project.jar

You might also want to set some memory arguments to keep things in check on the smaller instances. I use -Xmx400m -Xms200m on the 512mb vps.

Kill your running jar, and run this on the server:

$ start my_project

Now, navigate again to http://<domain_or_ip>:8080. You should see your app, again.

Configure Nginx

If nginx isn’t installed on your server, run the following:

$ sudo apt-get install nginx

Then, create a file called /etc/nginx/sites-available/myproject with these contents:

server{
  listen 0.0.0.0:80;
  server_name mydomain.com www.mydomain.com;

  access_log /var/log/myproject_access.log;
  error_log /var/log/myproject_error.log;

  location / {
    proxy_pass http://localhost:8080/;
    proxy_set_header Host $http_host;
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    proxy_set_header X-Forwarded-Proto $scheme
    proxy_redirect  off;
  }
}

Then, create a symlink of that config file to the /etc/nginx/sites-enabled directory. By keeping config files in sites-available and linking them in sites-enabled, you can easily enable and disable your nginx sites.

ln -s /etc/nginx/sites-available/myproject /etc/nginx/sites-enabled/myproject

After you restart nginx (service nginx restart), this config will accept requests on port 80 and serve your app from localhost. Once you’ve confirmed that works, you should lock down port 8080 from the outside:

$ sudo ufw deny 8080

Voila! Your app is serving requests. But, it would be better if nginx were serving all our assets too. So, put those on the server somewhere and point nginx at them by adding this snippet to your server configuration.

  location /static/ {
    alias /opt/myproject/static/;
  }

Now, nginx will serve all requests to http://<my_domain_or_ip>/static with files from /opt/myproject/static, bypassing your app.

And that’s it! If you have any questions or you ran into trouble, leave a comment below. Otherwise, enjoy!

Some suggestions

Some improvements to this article were suggested by weavejester on reddit, all of which are on the money to the point of being embarrassing:

I’ve done this a few times, so I have a few suggestions. In your Upstart script, rather than:

start on runlevel [2345]
stop on runlevel [!2345]

You could write:

start on startup
stop on shutdown

It’s also good practice to run the app as an unprivileged user, so:

setuid deploy

And it’s not a bad idea to start the app inside its own folder:

chdir /deploy/appname

I’d also suggest setting the port via the environment variable PORT, rather than hard-coding the port in the jar. Then you can specify the port in your upstart script:

env PORT=8080

Moving onto the nginx configuration, you need to explicitly set the Host header, or else it’ll default to 127.0.0.1:

proxy_set_header  Host  $http_host;

If you’re supporting HTTPS, you also want to setup X-Forwarded-For, as otherwise you lose protocol information:

proxy_set_header  X-Forwarded-For  $proxy_add_x_forwarded_for;

Finally, it’s useful to set:

proxy_redirect  off;

This ensures nginx doesn’t try and rewrite your Location header.

Finally, moving onto the firewall, it’s better to have a deny-all policy, and allow specific ports, than the other way round. Therefore:

ufw allow ssh
ufw allow http
ufw allow https
ufw enable