CrackedRuby logo

CrackedRuby

Private Gem Servers

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