Overview
Private gem servers provide controlled distribution of Ruby gems within organizations or closed environments. Ruby's gem system supports custom gem sources through the gem
command and Bundler, allowing developers to host proprietary gems on private infrastructure while maintaining the same installation and dependency management workflows used with public gems.
The core components of private gem server infrastructure include the gem server application, gem repository storage, authentication mechanisms, and client configuration. Ruby applications interact with private gem servers through standard gem installation commands and Gemfile declarations, with authentication handled through API keys or credential files.
Popular private gem server implementations include Geminabox, Gemfury, Nexus Repository, and custom solutions built with frameworks like Sinatra or Rails. Each implementation provides HTTP endpoints that comply with Ruby's gem server API specification, enabling seamless integration with existing Ruby toolchains.
# Basic private gem server configuration in Gemfile
source 'https://rubygems.org'
source 'https://gems.company.com' do
gem 'internal_library'
gem 'proprietary_toolkit'
end
# Installing gems from private server via command line
system('gem install private_gem --source https://gems.company.com')
# Configuring multiple gem sources with authentication
Bundler.configure do |config|
config.gem_path = '/custom/gem/path'
end
source_credentials = {
'https://gems.company.com' => 'api_key_here'
}
Private gem servers maintain the standard gem specification format, including gem metadata, versioning, dependencies, and platform-specific builds. The server responds to HTTP requests for gem listings, individual gem downloads, and gem specification files, providing the same interface that clients expect from public gem repositories.
Basic Usage
Setting up a private gem server requires configuring both server-side infrastructure and client-side gem installation tools. The server must implement the gem repository API endpoints while clients need proper source configuration and authentication credentials.
Geminabox provides the simplest path to private gem hosting with minimal configuration requirements. The server runs as a Rack application and stores gems in the local filesystem:
# config.ru for Geminabox server
require 'geminabox'
Geminabox.data = '/var/geminabox'
Geminabox.build_legacy = false
Geminabox.rubygems_proxy = true
# Basic authentication
Geminabox.require_auth = true
Geminabox.basic_authentication_enabled = true
Geminabox.basic_authentication_user = 'admin'
Geminabox.basic_authentication_password = 'secure_password'
run Geminabox::Server
# Publishing gems to private server
require 'net/http'
require 'uri'
def push_gem_to_server(gem_file, server_url, api_key)
uri = URI("#{server_url}/upload")
http = Net::HTTP.new(uri.host, uri.port)
http.use_ssl = true if uri.scheme == 'https'
request = Net::HTTP::Post.new(uri)
request['Authorization'] = "Token #{api_key}"
form_data = [['file', File.open(gem_file), { filename: File.basename(gem_file) }]]
request.set_form(form_data, 'multipart/form-data')
response = http.request(request)
case response.code
when '200'
puts "Gem uploaded successfully"
when '401'
puts "Authentication failed"
when '422'
puts "Gem validation failed: #{response.body}"
else
puts "Upload failed: #{response.code} #{response.message}"
end
end
Client-side configuration involves adding the private gem server as a source in Gemfiles or gem configuration files. Bundler supports multiple gem sources with source blocks that scope specific gems to particular repositories:
# Gemfile with private gem server configuration
source 'https://rubygems.org'
source 'https://gems.internal.company.com' do
gem 'company_auth', '~> 2.1'
gem 'internal_metrics'
gem 'shared_utilities', '>= 1.0.0'
end
source 'https://team-gems.company.com' do
gem 'team_specific_tools'
end
# Public gems continue using default source
gem 'rails', '~> 7.0'
gem 'pg', '~> 1.4'
Authentication configuration varies by server implementation but commonly uses API keys stored in gem credentials files or environment variables:
# ~/.gem/credentials configuration
:private_server: "api_key_value_here"
:team_server: "different_api_key"
# Setting up credentials programmatically
require 'yaml'
credentials_path = File.expand_path('~/.gem/credentials')
credentials = File.exist?(credentials_path) ? YAML.load_file(credentials_path) : {}
credentials[:private_server] = ENV['PRIVATE_GEM_API_KEY']
credentials[:team_server] = ENV['TEAM_GEM_API_KEY']
File.write(credentials_path, credentials.to_yaml)
File.chmod(0600, credentials_path)
Bundle configuration files provide another approach for managing private gem server settings across development teams:
# .bundle/config file generation
bundle_config = {
'BUNDLE_GEMS__INTERNAL__COMPANY__COM' => ENV['INTERNAL_GEM_TOKEN'],
'BUNDLE_MIRROR__HTTPS://RUBYGEMS__ORG/' => 'https://gems.internal.company.com',
'BUNDLE_GEMS__INTERNAL__COMPANY__COM__USERNAME' => 'deploy_user',
'BUNDLE_GEMS__INTERNAL__COMPANY__COM__PASSWORD' => ENV['DEPLOY_PASSWORD']
}
bundle_config.each do |key, value|
system("bundle config #{key} #{value}")
end
Private gem servers support gem pushing through standard gem commands with proper authentication and source specification:
gem push my_gem-1.0.0.gem --host https://gems.company.com --key private_server
The gem specification process remains identical to public gems, with gemspec files containing standard metadata fields and dependency declarations that reference both public and private gems.
Production Patterns
Production private gem server deployments require careful consideration of availability, security, authentication, and integration with existing development workflows. Multiple server instances with load balancing provide redundancy while proper authentication mechanisms ensure secure access to proprietary code.
Container-based deployments offer scalability and simplified management for private gem servers. Docker configurations can include the gem server application, authentication proxy, and storage backend:
# Production Dockerfile for Geminabox server
FROM ruby:3.2-alpine
RUN apk add --no-cache build-base sqlite-dev
WORKDIR /app
# Install gems
COPY Gemfile Gemfile.lock ./
RUN bundle install --deployment --without development test
# Copy application code
COPY . .
# Create gem storage directory
RUN mkdir -p /app/data && chmod 755 /app/data
EXPOSE 9292
# Production configuration
ENV RACK_ENV=production
ENV GEMINABOX_DATA=/app/data
ENV GEMINABOX_BASE_URL=https://gems.company.com
CMD ["bundle", "exec", "rackup", "-o", "0.0.0.0", "-p", "9292"]
# Production configuration with Redis caching
require 'geminabox'
require 'redis'
# Redis connection for caching gem metadata
redis = Redis.new(url: ENV['REDIS_URL'])
Geminabox.data = ENV['GEMINABOX_DATA']
Geminabox.allow_replace = false
Geminabox.build_legacy = false
# Custom caching middleware
class GemCacheMiddleware
def initialize(app)
@app = app
@redis = Redis.new(url: ENV['REDIS_URL'])
end
def call(env)
request = Rack::Request.new(env)
if request.path_info =~ /\.gem$/
cache_key = "gem:#{File.basename(request.path_info)}"
if cached_response = @redis.get(cache_key)
return [200, {'Content-Type' => 'application/octet-stream'}, [cached_response]]
end
end
status, headers, response = @app.call(env)
if status == 200 && request.path_info =~ /\.gem$/
cache_key = "gem:#{File.basename(request.path_info)}"
response_body = response.respond_to?(:body) ? response.body : response.join
@redis.setex(cache_key, 3600, response_body)
end
[status, headers, response]
end
end
use GemCacheMiddleware
run Geminabox::Server
Authentication integration with corporate identity systems requires custom middleware that validates tokens against LDAP, OAuth, or other enterprise authentication providers:
# Enterprise authentication middleware
class EnterpriseAuthMiddleware
def initialize(app)
@app = app
@ldap_config = {
host: ENV['LDAP_HOST'],
port: ENV['LDAP_PORT'].to_i,
base: ENV['LDAP_BASE'],
encryption: :simple_tls
}
end
def call(env)
request = Rack::Request.new(env)
# Skip authentication for gem downloads (GET requests)
if request.get? && request.path_info =~ /\.(gem|gemspec)$/
return @app.call(env)
end
# Require authentication for uploads and admin operations
auth_header = request.env['HTTP_AUTHORIZATION']
unless auth_header && authenticate_user(auth_header)
return [401, {'Content-Type' => 'text/plain'}, ['Authentication required']]
end
@app.call(env)
end
private
def authenticate_user(auth_header)
type, credentials = auth_header.split(' ', 2)
return false unless type == 'Bearer'
# Validate JWT token or API key against enterprise system
validate_enterprise_token(credentials)
end
def validate_enterprise_token(token)
# Implementation depends on enterprise authentication system
# Could validate JWT tokens, API keys, or LDAP credentials
enterprise_auth_service.validate_token(token)
rescue
false
end
end
Monitoring and logging configurations track gem server performance, authentication attempts, and gem usage patterns:
# Comprehensive logging middleware
class GemServerLogger
def initialize(app)
@app = app
@logger = Logger.new('/var/log/gem_server.log')
@metrics = StatsD.new(ENV['STATSD_HOST'], ENV['STATSD_PORT'].to_i)
end
def call(env)
start_time = Time.now
request = Rack::Request.new(env)
# Log request details
@logger.info({
timestamp: start_time.iso8601,
method: request.request_method,
path: request.path_info,
user_agent: request.user_agent,
remote_ip: request.ip,
params: request.params
}.to_json)
status, headers, response = @app.call(env)
duration = Time.now - start_time
# Log response details
@logger.info({
timestamp: Time.now.iso8601,
status: status,
duration: duration,
response_size: headers['Content-Length']&.to_i || 0
}.to_json)
# Send metrics to StatsD
@metrics.timing('gem_server.request_duration', duration * 1000)
@metrics.increment("gem_server.response.#{status}")
if request.path_info =~ /\.gem$/
gem_name = File.basename(request.path_info, '.gem')
@metrics.increment('gem_server.downloads', tags: ["gem:#{gem_name}"])
end
[status, headers, response]
end
end
Deployment automation integrates private gem servers with CI/CD pipelines for automatic gem publishing and version management:
# Automated gem publishing script
class GemPublisher
def initialize(config)
@gem_server_url = config[:server_url]
@api_key = config[:api_key]
@git_repo = config[:git_repo]
end
def publish_if_version_changed
current_version = extract_version_from_gemspec
last_published_version = fetch_last_published_version
if version_changed?(current_version, last_published_version)
build_and_publish_gem(current_version)
tag_git_release(current_version)
end
end
private
def extract_version_from_gemspec
gemspec_content = File.read(Dir.glob('*.gemspec').first)
version_match = gemspec_content.match(/version\s*=\s*['"]([^'"]+)['"]/)
version_match[1] if version_match
end
def fetch_last_published_version
uri = URI("#{@gem_server_url}/api/v1/gems/#{gem_name}/versions")
http = Net::HTTP.new(uri.host, uri.port)
http.use_ssl = true
request = Net::HTTP::Get.new(uri)
request['Authorization'] = "Bearer #{@api_key}"
response = http.request(request)
if response.code == '200'
versions = JSON.parse(response.body)
versions.first['number'] if versions.any?
end
rescue
nil
end
def build_and_publish_gem(version)
system('gem build *.gemspec')
gem_file = "#{gem_name}-#{version}.gem"
push_gem_to_server(gem_file, @gem_server_url, @api_key)
File.delete(gem_file) if File.exist?(gem_file)
end
end
Error Handling & Debugging
Private gem server operations encounter various failure modes including authentication failures, network connectivity issues, gem specification problems, and server-side errors. Comprehensive error handling addresses these scenarios with appropriate fallback mechanisms and diagnostic information.
Authentication errors represent the most common category of private gem server problems. Invalid credentials, expired tokens, and misconfigured authentication settings prevent gem installation and publishing operations:
# Robust authentication error handling
class GemServerClient
class AuthenticationError < StandardError; end
class NetworkError < StandardError; end
class GemNotFoundError < StandardError; end
def initialize(server_url, credentials)
@server_url = server_url
@credentials = credentials
@retry_count = 3
@retry_delay = 2
end
def install_gem(gem_name, version = nil)
attempt = 0
begin
attempt += 1
response = fetch_gem(gem_name, version)
case response.code
when '200'
return response.body
when '401', '403'
handle_authentication_error(response)
when '404'
raise GemNotFoundError, "Gem #{gem_name} not found on server"
when '500', '502', '503'
raise NetworkError, "Server error: #{response.code}"
else
raise StandardError, "Unexpected response: #{response.code}"
end
rescue NetworkError, Timeout::Error => e
if attempt < @retry_count
sleep(@retry_delay * attempt)
retry
else
raise e
end
rescue AuthenticationError => e
# Try to refresh credentials
if attempt == 1 && refresh_credentials
retry
else
raise e
end
end
end
private
def handle_authentication_error(response)
error_details = JSON.parse(response.body) rescue {}
case error_details['error_type']
when 'expired_token'
raise AuthenticationError, "API token has expired. Please regenerate token."
when 'insufficient_permissions'
raise AuthenticationError, "Insufficient permissions to access gem repository."
when 'invalid_credentials'
raise AuthenticationError, "Invalid authentication credentials."
else
raise AuthenticationError, "Authentication failed: #{response.body}"
end
end
def refresh_credentials
# Attempt to refresh OAuth tokens or regenerate API keys
credential_refresher = CredentialRefresher.new(@credentials)
@credentials = credential_refresher.refresh
true
rescue
false
end
end
Network connectivity problems require retry logic with exponential backoff and circuit breaker patterns to handle temporary outages and slow responses:
# Network resilience with circuit breaker pattern
class GemServerCircuitBreaker
FAILURE_THRESHOLD = 5
TIMEOUT_SECONDS = 30
RECOVERY_TIME = 60
def initialize
@failure_count = 0
@last_failure_time = nil
@state = :closed # :closed, :open, :half_open
end
def call
case @state
when :closed
execute_with_failure_tracking { yield }
when :open
check_recovery_time
raise CircuitBreakerOpenError, "Circuit breaker is open"
when :half_open
execute_recovery_attempt { yield }
end
end
private
def execute_with_failure_tracking
result = yield
reset_failure_count
result
rescue => e
record_failure
raise e
end
def execute_recovery_attempt
result = yield
@state = :closed
reset_failure_count
result
rescue => e
@state = :open
@last_failure_time = Time.now
raise e
end
def record_failure
@failure_count += 1
@last_failure_time = Time.now
if @failure_count >= FAILURE_THRESHOLD
@state = :open
end
end
def check_recovery_time
if Time.now - @last_failure_time > RECOVERY_TIME
@state = :half_open
end
end
def reset_failure_count
@failure_count = 0
@last_failure_time = nil
end
end
Gem specification validation prevents corrupt or incompatible gems from entering private repositories. Server-side validation checks gem metadata, dependency specifications, and file integrity:
# Comprehensive gem validation
class GemValidator
def initialize
@required_fields = %w[name version authors summary]
@max_file_size = 50 * 1024 * 1024 # 50MB
end
def validate_gem(gem_file_path)
errors = []
# Basic file validation
errors << validate_file_size(gem_file_path)
errors << validate_file_format(gem_file_path)
# Extract and validate gemspec
begin
spec = extract_gemspec(gem_file_path)
errors << validate_gemspec_fields(spec)
errors << validate_dependencies(spec)
errors << validate_version_format(spec.version)
rescue => e
errors << "Failed to extract gemspec: #{e.message}"
end
errors.compact
end
private
def validate_file_size(gem_file_path)
size = File.size(gem_file_path)
return "Gem file too large: #{size} bytes (max: #{@max_file_size})" if size > @max_file_size
nil
end
def validate_file_format(gem_file_path)
# Check if file is a valid gzipped tar archive
begin
require 'zlib'
require 'rubygems/package'
File.open(gem_file_path, 'rb') do |file|
Gem::Package::TarReader.new(Zlib::GzipReader.new(file)) do |tar|
tar.each { |entry| break } # Just check if we can read it
end
end
nil
rescue => e
"Invalid gem file format: #{e.message}"
end
end
def validate_gemspec_fields(spec)
missing_fields = @required_fields.select { |field| spec.send(field).nil? || spec.send(field).empty? }
return "Missing required fields: #{missing_fields.join(', ')}" if missing_fields.any?
nil
end
def validate_dependencies(spec)
spec.dependencies.each do |dep|
unless valid_dependency_name?(dep.name)
return "Invalid dependency name: #{dep.name}"
end
unless valid_version_requirements?(dep.requirement)
return "Invalid version requirement for #{dep.name}: #{dep.requirement}"
end
end
nil
end
def valid_dependency_name?(name)
name =~ /\A[a-zA-Z0-9\-_.]+\z/
end
def valid_version_requirements?(requirement)
requirement.requirements.all? do |operator, version|
%w[= != > < >= <= ~>].include?(operator) && version.is_a?(Gem::Version)
end
end
end
Debugging tools help diagnose gem server configuration problems, dependency resolution conflicts, and installation failures:
# Gem server diagnostic tools
class GemServerDiagnostics
def initialize(server_url, credentials)
@server_url = server_url
@credentials = credentials
end
def run_full_diagnostic
results = {}
results[:connectivity] = test_connectivity
results[:authentication] = test_authentication
results[:gem_listing] = test_gem_listing
results[:gem_download] = test_gem_download
results[:dependency_resolution] = test_dependency_resolution
generate_diagnostic_report(results)
end
private
def test_connectivity
start_time = Time.now
begin
uri = URI(@server_url)
http = Net::HTTP.new(uri.host, uri.port)
http.use_ssl = uri.scheme == 'https'
http.open_timeout = 10
http.read_timeout = 10
response = http.get('/')
{
status: 'success',
response_time: Time.now - start_time,
server_status: response.code,
ssl_verified: http.use_ssl? ? verify_ssl_certificate(uri) : 'N/A'
}
rescue => e
{
status: 'failure',
error: e.message,
response_time: Time.now - start_time
}
end
end
def test_authentication
begin
uri = URI("#{@server_url}/api/v1/gems")
http = Net::HTTP.new(uri.host, uri.port)
http.use_ssl = uri.scheme == 'https'
request = Net::HTTP::Get.new(uri)
request['Authorization'] = "Bearer #{@credentials[:api_key]}"
response = http.request(request)
{
status: response.code == '200' ? 'success' : 'failure',
response_code: response.code,
permissions: extract_permissions_from_response(response)
}
rescue => e
{
status: 'failure',
error: e.message
}
end
end
def test_dependency_resolution
# Test complex dependency scenarios
test_cases = [
{ gem: 'rails', version: '>= 6.0' },
{ gem: 'internal_gem', version: '~> 1.0' }
]
results = test_cases.map do |test_case|
begin
resolver = Gem::DependencyResolver.new
# Simulate dependency resolution
{
gem: test_case[:gem],
version: test_case[:version],
status: 'success',
resolved_dependencies: []
}
rescue => e
{
gem: test_case[:gem],
version: test_case[:version],
status: 'failure',
error: e.message
}
end
end
results
end
def generate_diagnostic_report(results)
report = []
report << "=== Gem Server Diagnostic Report ==="
report << "Server: #{@server_url}"
report << "Timestamp: #{Time.now.iso8601}"
report << ""
results.each do |category, result|
report << "#{category.to_s.upcase}:"
report << format_result(result)
report << ""
end
report.join("\n")
end
end
Reference
Private gem server reference documentation provides quick access to configuration options, API endpoints, authentication methods, and troubleshooting information developers need for implementation and maintenance.
Server Configuration Options
Option | Type | Default | Description |
---|---|---|---|
data_directory |
String | /var/gems |
Directory for storing gem files |
bind_address |
String | 0.0.0.0 |
IP address to bind server |
port |
Integer | 9292 |
Port number for HTTP server |
enable_ssl |
Boolean | false |
Enable HTTPS connections |
ssl_cert_path |
String | nil |
Path to SSL certificate file |
ssl_key_path |
String | nil |
Path to SSL private key file |
auth_required |
Boolean | true |
Require authentication for uploads |
auth_method |
Symbol | :api_key |
Authentication method (:api_key , :basic , :oauth ) |
cache_enabled |
Boolean | true |
Enable response caching |
cache_ttl |
Integer | 3600 |
Cache time-to-live in seconds |
max_file_size |
Integer | 52428800 |
Maximum gem file size in bytes |
allow_overwrite |
Boolean | false |
Allow gem version overwriting |
Authentication Configuration
Method | Configuration | Required Fields | Security Level |
---|---|---|---|
API Key | auth_method: :api_key |
api_key |
Medium |
Basic Auth | auth_method: :basic |
username , password |
Low |
OAuth 2.0 | auth_method: :oauth |
client_id , client_secret , token_endpoint |
High |
JWT Token | auth_method: :jwt |
jwt_secret , issuer |
High |
LDAP | auth_method: :ldap |
ldap_host , ldap_base , bind_dn |
High |
HTTP API Endpoints
Endpoint | Method | Purpose | Authentication | Parameters |
---|---|---|---|---|
/api/v1/gems |
GET | List all gems | Optional | page , per_page |
/api/v1/gems/:name |
GET | Get gem details | Optional | name |
/api/v1/gems/:name/versions |
GET | List gem versions | Optional | name |
/api/v1/gems |
POST | Upload new gem | Required | file (multipart) |
/api/v1/gems/:name/versions/:version |
DELETE | Delete gem version | Required | name , version |
/gems/:name-:version.gem |
GET | Download gem file | Optional | name , version |
/quick/Marshal.4.8/:name-:version.gemspec.rz |
GET | Download gemspec | Optional | name , version |
Client Configuration
Configuration File | Location | Format | Priority |
---|---|---|---|
.bundle/config |
Project root | YAML | Highest |
~/.bundle/config |
User home | YAML | Medium |
/etc/bundler/config |
System-wide | YAML | Low |
~/.gem/credentials |
User home | YAML | Authentication |
Bundler Source Configuration
Source Type | Syntax | Use Case | Example |
---|---|---|---|
Default | source 'URL' |
Primary gem source | source 'https://gems.company.com' |
Block | source 'URL' do |
Scoped gems | source 'https://private.com' do gem 'secret' end |
Git | gem 'name', git: 'URL' |
Development gems | gem 'lib', git: 'git@github.com:company/lib.git' |
Path | gem 'name', path: 'PATH' |
Local gems | gem 'local_gem', path: '../local_gem' |
Environment Variables
Variable | Purpose | Example Value | Required |
---|---|---|---|
BUNDLE_GEMS__COMPANY__COM |
Authentication token | abc123token |
For private sources |
BUNDLE_MIRROR__HTTPS://RUBYGEMS__ORG/ |
Mirror configuration | https://gems.company.com |
Optional |
BUNDLE_PATH |
Gem installation path | /usr/local/bundle |
Optional |
BUNDLE_CACHE_ALL |
Cache all gems | true |
Optional |
GEM_HOME |
Gem installation directory | /usr/local/gems |
Optional |
GEM_PATH |
Gem search path | /usr/local/gems:/home/gems |
Optional |
Error Codes and Messages
HTTP Code | Error Type | Typical Cause | Resolution |
---|---|---|---|
401 | Unauthorized | Invalid credentials | Check API key or credentials |
403 | Forbidden | Insufficient permissions | Verify user permissions |
404 | Not Found | Gem doesn't exist | Check gem name and version |
409 | Conflict | Gem version exists | Use different version number |
413 | Payload Too Large | File size exceeded | Reduce gem size |
422 | Unprocessable Entity | Invalid gem format | Fix gemspec validation errors |
429 | Too Many Requests | Rate limit exceeded | Implement retry with backoff |
500 | Internal Server Error | Server malfunction | Check server logs |
Command Line Usage
Command | Purpose | Example | Options |
---|---|---|---|
gem install |
Install from private server | gem install mygem --source https://gems.company.com |
--source , --version |
gem push |
Upload to private server | gem push mygem-1.0.gem --host https://gems.company.com |
--host , --key |
gem sources |
Manage gem sources | gem sources --add https://gems.company.com |
--add , --remove , --list |
bundle config |
Configure Bundler | bundle config set --global gems.company.com token123 |
--global , --local |
bundle install |
Install gems | bundle install |
--deployment , --path |
Gem Specification Fields
Field | Type | Required | Description |
---|---|---|---|
name |
String | Yes | Gem name |
version |
String | Yes | Semantic version |
authors |
Array | Yes | Gem authors |
email |
Array | Yes | Author email addresses |
summary |
String | Yes | Brief description |
description |
String | No | Detailed description |
homepage |
String | No | Project homepage URL |
license |
String | No | Software license |
files |
Array | No | Included files |
dependencies |
Hash | No | Runtime dependencies |
development_dependencies |
Hash | No | Development dependencies |