Hosting RubyGems for local use
Last week at work I set up my own RubyGems repository to host some local builds of upstream gems. Following the recommendation on guides.rubygems.org, I tried out Gem in a Box. It works perfectly as advertised, but the setup is largely left as an exercise for the reader (who is assumed to know their way around a web app deployment process).
I’ll run through the process I used both as a step-by-step guide to setting up a reasonably locked-down and authenticated (for upload) gem server, and as an example of how to deploy an arbitrary Ruby web app using a sane, repeatable process.
Gem in a Box consists of a Sinatra app and a RubyGems plugin, packaged together in a single gem. We could thus just gem install geminabox and go from there, but we want to do this the Right Way(tm). That means:
- Being able to manage dependencies on deployment easily: Bundler
- Using a deployment tool: Vlad
- Keeping our setup in source control: Mercurial
Substitute Capistrano, Subversion, Git, or other tools as appropriate; the above make up my preferred toolkit. We’ll also be deploying with Unicorn, and we’ll want the appropriate Vlad plugins (namely vlad-hg and vlad-unicorn) to ease our deployment too.
1. Create a repository
$ hg init boxogems
$ cd boxogems
2. Create a Gemfile
source :rubygems
gem 'geminabox'
gem 'unicorn'
group :development do
gem 'rake'
gem 'vlad'
gem 'vlad-hg'
gem 'vlad-unicorn'
end
3. Set up Vlad
# Rakefile
begin
require 'vlad'
Vlad.load(:scm => :mercurial, :type => nil,
:app => :unicorn, :config => 'vlad.conf.rb')
rescue LoadError
end
This is just your standard Vlad.load call, but note that we’re passing :type => nil. The :type option defaults to :rails — but we don’t want to load any of the Rails-specific setup tasks. This way we bypass creating unnecessary symlinks from our release code into the shared/ directory.
Also note that I’m using a non-standard filename for the Vlad config–if you want to create a config/ directory in your project and use the normal config/deploy.rb, that’s fine. Setting up Gem in a Box requires so little code that I decided to avoid creating unnecessary subdirectories.
Now we create the deployment configuration. We’ll put the whole gem repository, both the app deployment we’re creating here and the hosted gems, in a subdirectory of /srv, with the app in /srv/rubygems/app and the hosted gems in /srv/rubygems/data.
# vlad.conf.rb (or config/deploy.rb)
set :application, 'boxogems'
set :domain, 'backend.example.com'
set :deploy_to, '/srv/rubygems/app'
set :repository, 'ssh://backend.example.com//path/to/boxogems'
set :unicorn_command, "cd #{current_path} && bundle exec unicorn"
4. Create the rackup file
Now that we’ve got the deployment harness in place, we can describe our app instance:
require 'geminabox'
Geminabox.data = '/srv/rubygems/data'
map '/gems' do
run Geminabox
end
I didn’t feel the need to create a whole new virtual host when I’ve already got a perfectly good back-end web server (with an SSL certificate), so I’m using the Rack::URLMap middleware to serve the app on a URI prefix (subdirectory, path prefix, etc.) of ‘/gems’.
5. Commit and push
$ hg ci -m 'initial Gem in a Box setup'
$ hg clone . ssh://backend.example.com//path/to/boxogems
6. Deploy the app
Now you should be able to tell Vlad to deploy your app:
$ rake vlad:setup vlad:update
If all goes well, you can now SSH into your server and…
7. Configure the server
Create the Unicorn configuration file at /srv/rubygems/app/shared/config/unicorn.rb. The specifics are up to you, but here’s what mine looks like:
# shared/config/unicorn.rb
listen '127.0.0.1:4242'
pid '/srv/rubygems/app/shared/pids/unicorn.pid'
stdout_path '/srv/rubygems/app/shared/log/unicorn.stdout.log'
stderr_path '/srv/rubygems/app/shared/log/unicorn.stderr.log'
If you’re following the usual practice, you’ll probably bundle the gems in your deployed app. I don’t do it that way–I use Bundler to resolve dependencies, not to duplicate the same gems all over my server’s hard drive. So after I run rake vlad:update, I always:
server$ cd /srv/rubygems/app/current
server$ sudo bundle install --system --without development
Now you should be able to start the app using Vlad:
$ rake vlad:start_app
Check for output from Unicorn in /srv/rubygems/app/shared/log/unicorn.stderr.log. You should see a line that says something like:
I, [2012-03-22T11:32:55.918690 #10411] INFO -- : worker=0 ready
That means the app is ready to serve requests; now to make Apache proxy requests to it. This is a bit different from the usual Apache config for a Rails app, so we use a <Location> block inside the virtual host config to do the proxying:
# Proxy anything under /gems to Gem in a Box
RewriteEngine On
RewriteRule ^/gems http://127.0.0.1:4242%{REQUEST_URI} [P,QSA,L]
<Location /gems>
# Limit access to local network
Order deny,allow
Deny from all
Allow from 127.0.0.1 172.16.0.0/20
# Set up HTTP Basic authentication
# Substitute your own authentication info here
AuthName "Box o' Gems"
AuthType Basic
AuthUserFile /srv/rubygems/app/shared/config/htpasswd
# Only allow authenticated users to upload gems
<LimitExcept GET>
Require valid-user
</LimitExcept>
</Location>
Of course, you can force authentication even to download gems just by moving the Require valid-user line outside of the <LimitExcept> block (and removing the empty block). Adjust to your own needs.
At this point you’re probably thinking, “Hey, shouldn’t we be letting Apache serve the static files?” I’ll leave that as an exercise for the reader, but I’ll mention that I tried it, and then went back to letting Gem in a Box handle it directly. To make Apache do it I would’ve had to teach it about the MIME type of .gem files.
Now restart Apache and you should have a fully-functional RubyGems server!
Extras
If you want to be fancy, you can add a little magic into your Rakefile to make rake vlad:setup_app write a default Unicorn config into the right place for you. Put the default config into your source repository as unicorn.example.rb, then add this inside the begin ... rescue LoadError ... end block in your Rakefile:
namespace :vlad do
task :setup_app do
commands = ["umask #{umask}"]
commands << "mkdir -p #{shared_path}/config"
commands << "chown #{perm_owner} #{shared_path}/config" if perm_owner
commands << "chgrp #{perm_group} #{shared_path}/config" if perm_group
run commands.join(' && ')
put(unicorn_config) { File.read('unicorn.example.rb') }
run %(chmod 640 #{unicorn_config})
end
end
You can also add a chunk of code that automatically installs the gems like I showed above:
namespace :vlad do
task :update do
run %(cd #{current_path} && sudo bundle install --system --without development)
end
end
About this entry
You’re currently reading “Hosting RubyGems for local use,” an entry on Kevin's random thoughts
- Published:
- 3.29.12 / 2pm
- Category:
- tech
- Tags:
No comments
Jump to comment form | comments rss [?] | trackback uri [?]