How to start rails or puma (installed with rvm) with systemd

Just a few notes I took about how to start rails server or puma (installed with rvm) with systemd.

I started reading this question on StackOverflow, but it has no answers. Then I found out this gist containing an interesting example: it shows how to create a wrapper with rvm, and then how to use this wrapper to start the server.

I read more in systemd.service documentation and the rails command line documentation and wrote this (gist):

# based on https://gist.github.com/twtw/5494223
# create systemd service file for rails/puma startup
# 0. [if required: rvm use ruby@default]
# 1. rvm wrapper default systemd rails
# 2. put this file in /etc/systemd/system/rails-puma.service
# 3. systemctl enable rails-puma
# 4. systemctl start rails-puma
[Unit]
Description=Rails-Puma Webserver
 
[Service]
Type=simple
User=app
WorkingDirectory=/home/app/your-app
ExecStart=/home/app/.rvm/bin/systemd_rails server -e production
TimeoutSec=15
Restart=always
 
[Install]
WantedBy=multi-user.target

I didn’t get to test it yet, if it works for you let me know :)

Advertisements

DataMapper out of range value for column ‘id’

If you are reading this, you are (probably) using DataMapper and are having problems like this:

Exception `DataObjects::DataError' at dm-do-adapter/adapter.rb:279
Out of range value for column 'id' at row 1 (code: 1264, sql state: 22003, 
query: INSERT INTO `workspaces` (`id`, `name`) VALUES (2348944937325, 'Stardata S.r.l.')
[...]

The problem is that the default field created by DataMapper for an Integer property is a 32bit INT (at least on MySQL 5.5). If you want to store bigger integers you should use the :min and :max parameters, for example:

class Workspace
  include DataMapper::Resource

  property :id, Integer, :key => true, :min => 0, :max => 281474976710656
  property :name, String

end

Then DataMapper will use a BIGINT for the field.

Running Sinatra (and other Rack apps) on Nginx + Unicorn

Like it often happens with Open Source projects, documentation for Unicorn is quite a mess. I was searching for a tutorial on how to deploy a simple ‘Hello, world!’ Sinatra app on Nginx + Unicorn, but all tutorials I found were somewhat lacking. With some help from vjt I managed to get it up and running, so here’s the breakdown. Most config files are adapted from original project examples.

As you can imagine, the first step is to install all the packages. I’m using an Ubuntu 10.04 virtual machine, so:

user[~]% sudo aptitude install ruby-full rubygems nginx
[...]
user[~]% sudo gem install unicorn sinatra
[...]

Then, make sure Ruby gems are in PATH:

user[~]% echo 'PATH=$PATH:/var/lib/gems/1.8/bin' >> /etc/environment
user[~]% source /etc/environment
user[~]% echo $PATH
/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/games:/var/lib/gems/1.8/bin
user[~]% unicorn --version
unicorn v4.1.1

Let’s create a basic Sinatra app:

user[~]% mkdir basic; cd basic/
user[~/basic/]% cat > app.rb <<EOF
require 'rubygems'
require 'sinatra'

get '/' do
  "hello world! it's #{Time.now} here!"
end

EOF

Let’s verify the app is working with WEBrick (Sinatra development webserver):

user[~/basic/]% ruby app.rb
[2012-01-01 21:03:58] INFO  WEBrick 1.3.1
[2012-01-01 21:03:58] INFO  ruby 1.8.7 (2010-01-10) [x86_64-linux]
== Sinatra/1.3.1 has taken the stage on 4567 for development with backup from WEBrick
[2012-01-01 21:03:58] INFO  WEBrick::HTTPServer#start: pid=1474 port=4567

Try connecting to the host on port 4567:

user[~]% curl http://localhost:4567
hello world! it's Sun Jan 01 21:08:14 +0100 2012 here!

Everything is working fine. Stop WEBrick (Control + C) and let’s create unicorn config files:

user[~/basic/]% cat > config.ru <<EOF
require 'sinatra'

set :env,  :production
disable :run

require './app.rb'

run Sinatra::Application

EOF
user[~/basic/]%
user[~/basic/]% cat > unicorn.conf <<EOF
worker_processes 8
working_directory "/home/USERNAME/basic"
listen 'unix:/tmp/basic.sock', :backlog => 512
timeout 120
pid "/var/run/unicorn/basic_unicorn.pid"

preload_app true
if GC.respond_to?(:copy_on_write_friendly=)
  GC.copy_on_write_friendly = true
end

before_fork do |server, worker|
  old_pid = "#{server.config[:pid]}.oldbin"
  if File.exists?(old_pid) && server.pid != old_pid
    begin
      Process.kill("QUIT", File.read(old_pid).to_i)
    rescue Errno::ENOENT, Errno::ESRCH
      # someone else did our job for us
    end
  end
end

EOF

Make sure /var/run/unicorn is writable by the user that will run unicorn. Try starting the daemon:

user[~]% cd basic
user[~/basic/]% unicorn -c unicorn.conf
I, [2012-01-10T14:13:40.886917 #6288]  INFO -- : unlinking existing socket=/tmp/basic.sock
I, [2012-01-10T14:13:40.887076 #6288]  INFO -- : listening on addr=/tmp/basic.sock fd=3
I, [2012-01-10T14:13:40.887287 #6288]  INFO -- : Refreshing Gem list
I, [2012-01-10T14:13:40.951007 #6291]  INFO -- : worker=2 spawned pid=6291
I, [2012-01-10T14:13:40.951180 #6291]  INFO -- : worker=2 ready
I, [2012-01-10T14:13:40.951426 #6289]  INFO -- : worker=0 spawned pid=6289
I, [2012-01-10T14:13:40.951563 #6289]  INFO -- : worker=0 ready
I, [2012-01-10T14:13:40.951960 #6288]  INFO -- : master process ready
[...]

You can check if the (UNIX) socket is up:

user[~]% sudo netstat -nalx
Active UNIX domain sockets (servers and established)
Proto RefCnt Flags       Type       State         I-Node   Path
[...]
unix  2      [ ACC ]     STREAM     LISTENING     17669    /tmp/basic.sock
[...]

Now let’s change the nginx configuration. I modified the example from Unicorn website:

user[~]% sudo cat > /etc/nginx/nginx.conf <<EOF
worker_processes 1;

user USERNAME GROUP;

pid /var/run/nginx.pid;
error_log /var/log/nginx/error.log;

events {
  worker_connections 1024;
  accept_mutex off;
  use epoll;
}

http {
  include mime.types;
  default_type application/octet-stream;
  access_log /tmp/nginx.access.log combined;
  sendfile on;

  tcp_nopush on;
  tcp_nodelay off;

  gzip on;
  gzip_http_version 1.0;
  gzip_proxied any;
  gzip_min_length 500;
  gzip_disable "MSIE [1-6]\.";
  gzip_types text/plain text/html text/xml text/css
             text/comma-separated-values
             text/javascript application/x-javascript
             application/atom+xml;

  upstream app_server {
    server unix:/tmp/basic.sock fail_timeout=0;
  }

  server {
    listen 80 default deferred; # for Linux

    client_max_body_size 4G;
    server_name _;

    keepalive_timeout 5;

    # path for static files
    root /home/USERNAME/basic/public;

    # Prefer to serve static files directly from nginx to avoid unnecessary
    # data copies from the application server.
    try_files $uri/index.html $uri.html $uri @app;

    location @app {
      proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
      proxy_set_header Host $http_host;
      proxy_redirect off;
      proxy_pass http://app_server;
    }
  }
}

EOF

Restart nginx:

user[~]% sudo service nginx restart
Restarting nginx: [warn]: duplicate MIME type "text/html" in /etc/nginx/nginx.conf:68
the configuration file /etc/nginx/nginx.conf syntax is ok
configuration file /etc/nginx/nginx.conf test is successful
[warn]: duplicate MIME type "text/html" in /etc/nginx/nginx.conf:68
nginx.

Check that nginx is running:

user[~]% sudo netstat -nalt
Active Internet connections (servers and established)
Proto Recv-Q Send-Q Local Address           Foreign Address         State      
tcp        0      0 0.0.0.0:80              0.0.0.0:*               LISTEN     
[...]

Try loading the website:

user[~]% curl localhost
hello world! it's Tue Jan 10 14:21:04 +0100 2012 here!

Here you go! You should work some magic (or just download one of many startup scripts) for running Unicorn at boot time, then sit down and relax :)

Edit: fixed paths and a call to sudo.

Running an external process from Ruby

Running an external process from a Ruby script is quite easy: use backticks (output = `command`) or the %x method (output = %x[command]).

There are more options like system and exec and even more advanced ones like popen, popen3 and popen4 that allow you to manage process stdin/stdout/stderr.

A good resource with more examples is Nate Murray’s 6 Ways to Run Shell Commands in Ruby.

Creare un repo git condiviso

Sulla mia box attuale ho due dischi in raid 1 su cui si trova la /home. Per sviluppare ho una vm QEMU/KVM con sopra Ubuntu 10.04 Server, il cui disco però non risiede sul suddetto raid.

Dato che voglio avere una certa ridondanza per i dati, volevo fare in modo di avere un repository git centralizzato sulla macchina host (nella home, quindi sul raid) da cui clonare nella vm (ed eventualmente sul portatile) per sviluppare e poi pushare di nuovo sul repo sul raid.

Inizialmente ho semplicemente creato un repository sulla macchina host, l’ho clonato sulla vm, ma al momento di effettuare il push usciva questo simpatico errore:

vm ➤ git push
Counting objects: 36, done.
Delta compression using up to 2 threads.
Compressing objects: 100% (23/23), done.
Writing objects: 100% (25/25), 84.45 KiB, done.
Total 25 (delta 6), reused 0 (delta 0)
remote: error: refusing to update checked out branch: refs/heads/master
remote: error: By default, updating the current branch in a non-bare repository
remote: error: is denied, because it will make the index and work tree inconsistent
remote: error: with what you pushed, and will require 'git reset --hard' to match
remote: error: the work tree to HEAD.
remote: error: 
remote: error: You can set 'receive.denyCurrentBranch' configuration variable to
remote: error: 'ignore' or 'warn' in the remote repository to allow pushing into
remote: error: its current branch; however, this is not recommended unless you
remote: error: arranged to update its work tree to match what you pushed in some
remote: error: other way.
remote: error: 
remote: error: To squelch this message and still keep the default behaviour, set
remote: error: 'receive.denyCurrentBranch' configuration variable to 'refuse'.
To host:c0ding/ruby/project
! [remote rejected] master -> master (branch is currently checked out)
error: failed to push some refs to 'host:c0ding/ruby/project'

Il problema era che essendo loggato anche sulla macchina host ed avendo un repository “aperto”, git si rifiutava di farmi mischiare le due cose. Inizialmente, per risolvere, mi sono limitato a creare un nuovo branch sulla macchina host:

host$ git checkout -b temp-repo

La soluzione più corretta, però, l’ho trovata su stack overflow (e sul sito linkato, pro git): era necessario creare un “bare repository”, ovvero un repository che contiene solo i dati GIT e nessun altro file.

Quindi ho fatto:

host$ rm -rf ~/c0ding/ruby/project
vm ➤ git clone --bare project project.git  # creazione bare repo
vm ➤ scp -r project.git host:c0ding/ruby/  # copia del repo sull'host
vm ➤ rm -rf project project.git            # rimozione vecchi repo sulla vm
vm ➤ git clone host:c0ding/ruby/project.git # clone nuovo repo sulla vm
vm ➤ cd project
vm ➤ git log

commit xxx
Author: Gilberto "Velenux" Ficara
Date:   Mon Nov 1 15:40:20 2010 +0100

Added Cucumber, JQuery support gems. Created DB.

commit xxx
Author: Gilberto "Velenux" Ficara
Date:   Mon Nov 1 14:42:03 2010 +0100

Initial commit

vm ➤