Gem in a Box – Really simple rubygem hosting
Geminabox lets you host your own gems, and push new gems to it just like with rubygems.org. The bundler dependencies API is supported out of the box. Authentication is left up to either the web server, or the Rack stack. For basic auth, try Rack::Auth.
System Requirements
- Ruby 3.0 or later (Ruby 3.2+ recommended)
- RubyGems 3.2.3 or later (latest version recommended)
Server Setup
gem install geminabox
Create a config.ru as follows:
require "rubygems"
require "geminabox"
Geminabox.data = "/var/geminabox-data" # ... or wherever
# Use Rack::Protection to prevent XSS and CSRF vulnerability if your geminabox server is open public.
# Rack::Protection requires a session middleware, choose your favorite one such as Rack::Session::Memcache.
# This example uses Rack::Session::Pool for simplicity, but please note that:
# 1) Rack::Session::Pool is not available for multiprocess servers such as unicorn
# 2) Rack::Session::Pool causes memory leak (it does not expire stored `@pool` hash)
use Rack::Session::Pool, expire_after: 1000 # sec
use Rack::Protection
run Geminabox::Server
Start your gem server with 'rackup' to run WEBrick or hook up the config.ru as you normally would (passenger, thin, unicorn, whatever floats your boat).
Using Geminabox alongside rubygems.org
Geminabox serves only the gems you push to it. To use it together with rubygems.org, declare it as a scoped source in your Gemfile so that every gem is pinned to exactly one source:
source "https://rubygems.org"
source "https://gems.example.com" do
gem "internal-widgets"
gem "internal-tools"
endThis pinning is Bundler's protection against dependency confusion: a gem name that exists on both servers can never silently resolve to the wrong one.
RubyGems proxy (removed in 4.0)
Earlier versions of Geminabox could proxy rubygems.org and serve remote and local gems from one merged namespace. The feature was deprecated in 3.1.0 and removed in 4.0:
- It relied on the RubyGems.org Dependency API
(
/api/v1/dependencies), which was sunset on 2023-05-24, so the proxy had not worked for years. - Serving remote and local gems from one namespace invites dependency confusion attacks and defeats Bundler's source pinning described above.
When upgrading, remove Geminabox.rubygems_proxy and its related settings
(rubygems_proxy_merge_strategy, allow_remote_failure, ruby_gems_url,
bundler_ruby_gems_url) from your config.ru, and point your Gemfile at
rubygems.org directly with a scoped source block as shown above. Leftover
settings won't break your server: 4.0 warns about each one and ignores it,
and the RUBYGEMS_PROXY and RUBYGEMS_PROXY_MERGE_STRATEGY environment
variables get the same startup warning. These shims go away in 5.0.
If you need a caching or mirroring proxy for rubygems.org (air-gapped networks, bandwidth, protection against upstream yanks), use gemstash, maintained by the RubyGems organization.
Compact index
Geminabox serves the compact index API
(/versions, /info/GEMNAME, /names). Bundler 1.12+ detects and uses it
automatically; no client configuration is needed. Responses support
If-None-Match and ranged requests, so bundle install only downloads
index data that changed since the last run.
The materialized index lives in data/compact_index/ and is updated
whenever gems are added or removed. Deleting that directory (or hitting
/reindex) is safe; it is rebuilt from the stored gems on the next request.
On installs upgraded from an earlier version with many stored gems, the
first request to /versions builds the index and can take a while as it
checksums every stored gem, so hitting /reindex or /versions right
after upgrading avoids surprising the first bundle install.
Running behind a reverse proxy
A stock nginx or Passenger deployment needs no special configuration for the
compact index. gzip is safe to leave on, even for text/plain: Bundler sends
its ranged requests without Accept-Encoding, and nginx never compresses 206
responses, so 304 revalidation and ranged tail appends keep working.
Two things do interfere:
- Proxy-level caching of
/versions,/info/*, or/names(nginxproxy_cache, or a CDN). The files reference each other by checksum, so a cache serving one fresh and another stale makes Bundler report checksum mismatches. Bundler already caches and revalidates client-side; leave these paths uncached. - Middleware or middleboxes that strip or rewrite headers. Removing
ETagdisables 304 revalidation; removingRepr-Digest/Digestmakes Bundler refuse to append partial responses and re-download the full file after every change.
Replacing a published version
gem inabox -o (and the allow_replace server option) overwrites a stored
gem in place: same version number, different contents. Geminabox updates the
compact index to match, including the new checksum. Bundler, however, guards
against a version's bytes changing.
Since 2.5, Bundler records each gem's checksum in the CHECKSUMS section of
Gemfile.lock. On a later resolve it compares the checksum the server now
advertises against the locked one, and if they differ for the same name and
version it aborts with Bundler::ChecksumMismatchError, treating the change as
a possible supply-chain swap. Nothing on the server can override this; the
conflict is between the client's lockfile and the replaced contents.
An overwrite is therefore transparent only to consumers who have not locked
that version yet. Anyone whose Gemfile.lock already pins it hits the error on
their next bundle install or bundle update. Their options are to remove
that gem's line from CHECKSUMS, delete the lockfile and re-resolve, or turn
off the check with bundle config set --local disable_checksum_validation true.
The clean fix is to bump the version instead of replacing it; treat -o as a
convenience for a private box whose gems nobody has locked yet.
HTTP adapter
Geminabox uses the HTTPClient gem to manage its connections to remote resources. The relationship is managed via Geminabox::HttpClientAdapter.
To configure options of HTTPClient, pass your own HTTPClient object in config.ru as:
# Geminabox.http_adapter = Geminabox::HttpClientAdapter.new # default
Geminabox.http_adapter.http_client = HTTPClient.new(ENV['http_proxy']).tap do |http_client|
http_client.transparent_gzip_decompression = true
http_client.keep_alive_timeout = 32 # sec
http_client.ssl_config.verify_mode = OpenSSL::SSL::VERIFY_NONE
http_client.send_timeout = 0
http_client.receive_timeout = 0
endIf you would like to use an alternative HTTP gem, create your own adapter and specify it in config.ru:
Geminabox.http_adapter = YourHttpAdapter.new
It is recommend (but not essential) that your adapter inherits from HttpAdapter. The adapter will need to replace HttpAdapter's methods with those specific to the alternative HTTP gem. It should also be able to handle HTTP proxy settings.
Defining your own adapter also allows you to configure Geminabox to use the local systems SSL certificates.
TemplateFaradayAdapter is provided as an example of an alternative HTTPAdapter.
Hooks
You can add a hook (anything callable) which will be called when a gem is successfully received.
Geminabox.on_gem_received = Proc.new do |gem|
puts "Gem received: #{gem.spec.name} #{gem.spec.version}"
endTypically you might use this to push a notification to your team chat. Any exceptions which occur within the hook is silently ignored, so please ensure they are handled properly if this is not desirable.
Also, please note that this hook blocks POST /upload and POST /api/v1/gems APIs processing.
Hook authors are responsible to perform any action non-blocking/async to avoid HTTP timeout.
Client Usage
Since version 0.10, Geminabox supports the standard gemcutter push API:
gem push pkg/my-awesome-gem-1.0.gem --host HOST
You can also use the gem plugin:
gem install geminabox
gem inabox pkg/my-awesome-gem-1.0.gem
And since version 1.2.0, Geminabox supports the standard gemcutter yank API:
gem yank my-awesome-gem -v 1.0 --host HOST
Configure Gem in a box (interactive prompt to specify where to upload to):
gem inabox -c
Change the host to upload to:
gem inabox -g HOST
Simples!
Command Line Help
Usage: gem inabox GEM [options]
Options:
-c, --configure Configure GemInABox
-g, --host HOST Host to upload to.
-o, --overwrite Overwrite Gem.
Common Options:
-h, --help Get help on this command
-V, --[no-]verbose Set the verbose level of output
-q, --quiet Silence commands
--config-file FILE Use this config file instead of default
--backtrace Show stack backtrace on errors
--debug Turn on Ruby debugging
Arguments:
GEM built gem to push up
Summary:
Push a gem up to your GemInABox
Description:
Push a gem up to your GemInABox
Docker
Using Gem in a Box is really simple with the Dockerfile. Move this Dockerfile into a directory that you want to use for your server.
That directory only needs to contain:
config.ru (explained above)
Gemfile
Gemfile.lock
Your Gemfile only needs:
source 'https://rubygems.org'
gem 'geminabox'
gem 'rackup'
gem 'webrick' # or other server you preferFrom there
docker build -t geminabox .
docker run -d -p 9292:9292 geminabox:latest
Your server should now be running!
Running the tests
Running rake will run the complete test suite.
The test suite uses
minitest-reporters
with the default reporter. To get more detailed test output, use rake MINITEST_REPORTER=SpecReporter. With this setting, output of the Geminabox
server that is started for integration tests is sent to stdout as well.