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