Tiago Scolari

bits for fun

Build your own private gem server

2013-01-08

With Bundler/Gemfile it’s easy to link gems directly to it’s git repository. But some times it’s not ideal. Revisions are not versions, and comparing them is tedious.

Luckly It’s easy to setup a private gem server!

There are services, e.g. GemFury that for a little cash gives you this kind of service.

But there are open source tools out there that you can combine to make the same service, for free, and fully customized.

1 The Gem Server

There is a awesome project called Geminabox that works some how like rubygems. Rubygems demands that every user needs an account for pushing gems, with privileges for it’s own gems. With gembox there is no user account, it takes into account that you trust the people you share the server. Supposing you trust your mates, this is a good option for your company gem server.

Geminabox is a sinatra app. The server configuration is pretty straight forward. Just create a config.ru to hook a rack server, unicorn for example.

gem install gembox
# Gemfile
source 'https://rubygems.org'

gem 'sinatra'
gem 'geminabox'
gem 'unicorn'
# Config.ru
require "bundler/setup"
require "rubygems"
require "geminabox"

Geminabox.data = "/var/gems" # …or wherever
run Geminabox
# unicorn.rb
worker_processes 2
preload_app true

shared_path = "/home/gem/deploy/shared"
current_path= "/home/gem/deploy/current"

# Restart any workers that haven't responded in 30 seconds
timeout 30

# Listen on a Unix data socket
listen "#{shared_path}/sockets/unicorn.sock", :backlog => 2048
user    'gem', 'gem'

stderr_path "#{current_path}/log/unicorn_site.stderr.log"
stdout_path "#{current_path}/log/unicorn_site.stdout.log"

pid     File.join(shared_path, "pids/unicorn.pid")

before_fork do |server, worker|
  old_pid = File.join(shared_path, 'pids/unicorn.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
    end
  end
end

This is a basic setup. This is meant to run on the private network card, and exposed using nginx, with some basic authentication.

2 The HTTP Server & Privacy

Geminabox will be running behind nginx. On nginx it’s possible to add an hash in the url (like gemfury does), or use simple HTTP AUTH, or both.

It’s also possible to setup SSL for it, tip: StartSSL, they offer free SSL certificates.

Basic nginx configuration:

# nginx.conf

server {
  listen  80;
  #listen  443 ssl;
  server_name gems.myself.com;
  root /home/gem/deploy/current/public;

  error_page  404          /404.html;
  error_page  500          /500.html;

  #ssl on;
  #ssl_certificate     /home/gem/.server_cert/server.crt;
  #ssl_certificate_key /home/gem/.server_cert/server.key;
  #ssl_session_timeout  5m;
  #ssl_protocols  SSLv2 SSLv3 TLSv1;
  #ssl_ciphers  HIGH:!aNULL:!MD5;
  #ssl_prefer_server_ciphers   on;

  proxy_buffering on;
  proxy_buffers 8 64k;
  proxy_buffer_size 172k;
  proxy_set_header  X-Real-IP  $remote_addr;
  proxy_set_header  X-Forwarded-For $proxy_add_x_forwarded_for;
  proxy_ignore_headers Cache-Control;
  proxy_set_header Host $http_host;
  proxy_redirect off;

  access_log /home/gem/deploy/shared/log/access_log;

  client_max_body_size 10000k;

  location /random_and_secret_string {
    auth_basic "Restricted";
    auth_basic_user_file  /home/gem/deploy/current/config/http/authentication.htpasswd;
    rewrite ^/random_and_secret_string/(.*)   /$1 break;
    proxy_pass http://unicorn_cluster;
  }

  location / {
    return 404;
  }
}

upstream unicorn_cluster {
  server unix://home/gem/deploy/shared/sockets/unicorn.sock;
}

This is really standard configuration, you are probably familiar with. Replace random_and_secret_string with your own secret hash, nginx shall proxy everything that is behind that hash to the geminabox server. I’ve also added HTTP Auth to the directory, it will require the user/password to access the gems.

3 Using the Gem Server

With the server up.

3.1 Pushing Gems

For pushing gems, it also requires the geminabox gem in the developer machine. It adds the new option inabox to the gem command. It will ask about the server url the first time you run this command. Supposing that the server is running at gems.myself.com, without SSL, and with http auth (user1:password2), and our secret hash is 1234567890:

gem inabox pkg/my_private_gem-0.0.1.gem
Enter the root url for your personal geminabox instance. (E.g. http://gems/)
Host: http://user1:password2@gems.myself.com/1234567890/

This will push the gems. There’s also a webinterface for administration (same url as above).

3.2 Requesting gems from Gemfile

Adding the new gem server to a Gemfile is easy:

# Gemfile

source 'http://user1:password2@gems.myself.com/1234567890/'
source 'https://rubygems.org'

...
gem 'my_private_gem', '>= 0.0.1'

And it’s done. I’ve been using this approach for almost one year, and it works like perfectly. You can find the source for these files in this Git Repository.