How I setup nginx, with multiple ruby puma apps on ubuntu 16.04 with systemd

The thing that caused me the most friction was getting systemd to execute puma correctly. Since the current server I’m using still use rvm instead of rbenv, that was the thing causing me the most headache. Maybe it’s just that I’m used to rbenv these days. One of my learning out of this is most def that I should have started the migration of all those legacy apps to rbenv in the first place.

Setup puma in the ruby app

Add a config/puma.rb file. Read more about puma here. For my sinatra app it looks like this:

# Change to match your CPU core count
workers ENV.fetch("PUMA_WORKERS") { 1 }

# Min and Max threads per worker
threads ENV.fetch("PUMA_MIN_THREADS") { 1 }, ENV.fetch("PUMA_MAX_THREADS") { 10 }

app_dir = File.expand_path("../..", __FILE__)
shared_dir = "#{app_dir}/shared"

# Set up socket location
bind "unix://#{shared_dir}/sockets/puma.sock"

# Redirect STDOUT to log files
stdout_redirect "#{app_dir}/log/puma.stdout.log", "#{app_dir}/log/puma.stderr.log", true

# Set master PID and state locations
pidfile "#{shared_dir}/pids/puma.pid"
state_path "#{shared_dir}/pids/puma.state"
rackup "#{app_dir}/config.ru"

activate_control_app

For a sinatra app you also need to set a config:

configure do
  set :server, :puma
end

Setup nginx as proxy using unix socket

Minimal nginx conf. Nginx acts as a so called “reversed proxy” and redirects all traffics to puma through a unix socket. This is my nginx virtual host file:

upstream app {
  server unix:/var/www/my-website.com/shared/sockets/puma.sock;
}

server {
  listen 80;
  server_name my-website.com;

  root /var/www/my-website.com/public;

  location / {
    try_files $uri @puma;
  }

  location @puma {
    include proxy_params;

    proxy_pass http://app;
  }
}

Setup systemd service

Since ubuntu 16.04 uses systemd instead of upstart I’m using that. It’s very straight forward. Create the following file here: /etc/systemd/system/puma-my-website.service. The ExecStart command is the most important one. That is the one starting puma. The service also loads the environment variables from the .env file, see the EnvironmentFile command. So dotenv is not needed and all the variables will be available in the puma config file.

[Unit]
Description=Puma HTTP Server
After=network.target

[Service]
# Foreground process (do not use --daemon in ExecStart or config.rb)
Type=simple

# Preferably configure a non-privileged user
User=joel
Group=joel

# Specify the path to your puma application root
WorkingDirectory=/var/www/my-website.com

# Helpful for debugging socket activation, etc.
Environment=PUMA_DEBUG=1
EnvironmentFile=/var/www/my-website.com/.env

# The command to start Puma
ExecStart=/home/joel/.rvm/wrappers/ruby-2.4.1/bundle exec puma -C /var/www/my-website.com/config/puma.rb

Restart=always

[Install]
WantedBy=multi-user.target

When that file is added you can start it like this:

sudo systemctl start puma-my-website.service

And make it start on system boot

sudo systemctl enable puma-my-website.service

Check systemd logs

When you have a problem with systemd, check the logs with journalctl:

sudo journalctl -f -u puma-my-website.service

RVM gemset and wrappers (optional)

As mentioned earlier, you need a way that systemd can execute puma. In the above example I’ve just used the default ruby wrapper. But some people using rvm prefer to use gemsets to isolate each app bundle from each other. If you’re of those, follow this guide:

rvm gemset create my-website

Then you need to explicitly say that you want to use it. The ruby-2.4.1 part will depend on which ruby version you use. (I always tend to forget this)

rvm ruby-2.4.1@my-website

And then install the gems in it

bundle install

Now puma and the other executables should be available under this directory: /home/joel/.rvm/wrappers/ruby-2.4.1@my-website