Rails + Puma + Capistrano + Nginx

About a month ago I decided I wanted to get a website going for Cerulean Labs, my catch-all organization that has supported game dev, mentoring meetups, and other random group projects. It would be good to have a website that allowed users in the organization to coordinate meeting up, sharing projects, and reviewing important info like community guidelines.

I specifically chose Rails because I haven’t developed on it since Rails 3 and I miss developing in Ruby. Puma is the default app server out of the box, and Capistrano takes care of deploys. Last, Nginx is used as a proxy.


Below are the components I had to install on the host machine. The order is all out of whack — I bounced around as I figured out what still needed to be setup. If you’re looking for specific guides, check out the bottom of this post.

SSL Certificate

I used Let’s Encrypt to get my certificate. First, I setup my dependencies:

$ sudo add-apt-repository ppa:certbot/certbot
$ sudo apt-get update
$ sudo apt-get install python-certbot-nginx

Installing the cert:

$ sudo certbot --nginx -d www.ceruleanlabs.com

Testing renewal:

$ sudo certbot renew --dry-run

It’ll prompt you along the way but the questions are straight-forward.

Nginx Gotos

I used the following when I wanted to check the syntax of my config files, check to see if nginx was running, and then to restart it whenever I made a change.

$ sudo nginx -t $ sudo service nginx status $ sudo service nginx restart

Nginx Config

When you first look at /etc/nginx/sites-available/default it looks something like this:

server {
    root /var/www/html;

    index index.html index.htm index.nginx-debian.html;
    server_name www.ceruleanlabs.com; # managed by Certbot

    location / {
            try_files $uri $uri/ =404;

    listen [::]:443 ssl ipv6only=on; # managed by Certbot
        listen 443 ssl; # managed by Certbot
        ssl_certificate /etc/letsencrypt/live/www.ceruleanlabs.com/fullchain.pem; # managed by Certbot
        ssl_certificate_key /etc/letsencrypt/live/www.ceruleanlabs.com/privkey.pem; # managed by Certbot
        include /etc/letsencrypt/options-ssl-nginx.conf; # managed by Certbot
        ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem; # managed by Certbot

server {
    if ($host = www.ceruleanlabs.com) {
        return 301 https://$host$request_uri;
    } # managed by Certbot

    listen 80 ;
    listen [::]:80 ;
    server_name www.ceruleanlabs.com;
    return 404; # managed by Certbot

The first server block shows a default Nginx page. The second block makes sure that your site is always served over HTTPS.

We want to first add an upstream block that points to where our Puma socket will be located. I put this at the very top of the file:

upstream puma {
    server unix:///var/www/ceruleanlabs/shared/tmp/sockets/puma.sock fail_timeout=0;

Next we want to rewrite the guts of that server block that is currently serving the default Nginx page. Mine looks something like this, given we’re serving a Rails app via Puma:

server {
    root /var/www/ceruleanlabs/current/public;
    access_log /var/www/ceruleanlabs/current/log/nginx.access.log;
    error_log /var/www/ceruleanlabs/current/log/nginx.error.log info;
    server_name www.ceruleanlabs.com;

    location ^~ /assets/ {
        gzip_static on;
        expires max;
        add_header Cache-Control public;

    try_files $uri/index.html $uri @puma;
    location @puma {
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header Host $http_host;
        proxy_redirect off;

        proxy_pass http://puma;

    listen [::]:443 ssl; # managed by Certbot
        listen 443 ssl; # managed by Certbot
        ssl_certificate /etc/letsencrypt/live/www.ceruleanlabs.com/fullchain.pem; # managed by Certbot
        ssl_certificate_key /etc/letsencrypt/live/www.ceruleanlabs.com/privkey.pem; # managed by Certbot
        include /etc/letsencrypt/options-ssl-nginx.conf; # managed by Certbot
        ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem; # managed by Certbot

    error_page 500 502 503 504 /500.html;
    client_max_body_size 10M;
    keepalive_timeout 10;

Firewall Config

This part was also straightforward, and again I followed the guide at the bottom of this post. I pretty much just ran the following commands:

$ sudo ufw allow "Nginx Full"
$ sudo ufw allow "OpenSSH"
$ sudo ufw delete allow "Nginx HTTP"

$ sudo ufw status

Ruby Setup

$ curl -fsSL https://github.com/rbenv/rbenv-installer/raw/master/bin/rbenv-installer | bash

I then followed the rbenv guide for installing it to my shell so it would be available when I’m running commands remotely via Capistrano. You can set that shell for that user with chsh -s /bin/bash — using Bash, for example, with a ~/.bash_profile that looks like this:

export PATH="$HOME/.rbenv/bin:$PATH" eval "$(rbenv init -)"

JavaScript Runtime

We need a runtime for compilation of the Javascript components. I ended up just installing Node on the host machine, but you can also add something like mini_racer to your Gemfile to add support as well.


$ cap production deploy

Additional Setup

This is all open-source, so you can of-course check out the rest of the app configuration on GitHub: https://github.com/ceruleanlabs/ceruleanlabs.com The rest of the changes I had to make mostly centered around configuring the Rails app with the correct database credentials, libraries, etc. I should’ve written my steps down better haha, but you’ll find the rest of what’s configured in the repo.

Useful Docs and Guides

Step 2: CircleCI and S3 Buckets

The second part took longer than usual. Part of this was because I was wrestling with DNS — I don’t think DNS changes ever go quickly — but the other part was because I was hit with another bout of depression. #yaydepression

For the second part I configured CircleCI to build and deploy this blog to an S3 bucket, which I reconfigured with the slidingdown.com URL by using Route 53 as my new DNS provider.


This part was pretty fun. I first setup Docker to make sure it could containerize correctly. This helped me debug a JS dependency for Middleman — something I quickly fixed with mini_racer. Next I figured out how to get it to build on CircleCI. CircleCI lets you build one project for free, which is why I’m having it run my blog, which is a private repo.

One useful piece of information was understanding how to persist data in workflows. The diagram in the article really helped and I was able to cobble the rest together using documentation.

AWS Configuration

The S3 configuration was really straightforward once I got the hang of the naming conventions and what how permission should be setup. I followed Amazon’s guide for setting up a custom domain static site And if you’re wary about copypasta’ing the policy into your bucket’s config, you can get the same results from using the AWS Policy Generator.

Getting CircleCI authenticated was also really easy — I created an IAM user specifically for CircleCI and gave it S3 permissions.

The most annoying part was figuring out I needed to switch from my own DNS provider to Route 53. I was trying to setup my own CNAME record and it didn’t take — S3 balked at my configuration. So I made the switch and everything worked magically.

Putting it all together

And now CI/CD for my blog works! The only nice-to-have I really want to have is SSL support. It’s not necessary because I host a static site, but it’s where the web is heading and Firefox gets angry. There might be an option using CloudFront that I can explore.

Two Related Emotions

I had a breakdown yesterday — it was the last hours of a long day, that long day being the last day of a long month. So as I descended into a ball of snot and tears, two primary emotions came to the forefront.

I am dumb

I can’t measure up to the company I keep. The gap between them and me is too great — there’s no way I can catch up. They’re all doing amazing things and I struggle with the most basic. That includes this mental illness. Why can’t I be smart enough to beat this? Why is it so irrational, but I still can’t outthink it?

I am a burden

I should remove myself from the company I keep. I’m only holding them back — they give me their precious time out of charity or pity, and I don’t know which is worse. Imagine the mental gymnastics they must do to call me their friend! I’ll be doing the world a service if I just go away.

Neither of those are true

I talked to my mom for an hour and a half last night. She really likes to talk — a trait I didn’t always appreciate growing up, but now it is exactly what I need for times like this. We talked about growing up as an immigrant, how my sister and I are similar and different, about what Mom went through when Dad passed away, cartoons. My mom likes her goofy metaphors, something I now understand I got from her — she likened my current situation to being flattened by an anvil. And even though I’m flattened, I’m just popping my feet back out and waddling around. My goal is to become 3D again.