Skip to main content
CleanStart

Knowledge Hub

Getting Started with Ruby on CleanStart

New to containers? Start with Container Image Fundamentals to understand the basics before diving into this guide.

Building Ruby Applications with CleanStart

CleanStart provides hardened Ruby base images (cleanstart/ruby) with all the tools you need for production deployments. The images include Ruby versions 3.3, 3.2, and 3.1 with security patches. Bundler and gem package management are pre-installed. Multi-stage build support is fully configured. Rails framework support is included out of the box. FIPS 140-3 compliance is available for regulated environments. The images are optimized for web services and background workers.

The CleanStart two-factory architecture ensures that all Ruby gems are verified through the Package Factory before image assembly in the Image Vault, providing supply chain security for your Ruby applications and their dependencies.

Quick Start: A Rails Application

Step 1: Create Your Project

mkdir my-rails-appcd my-rails-app

Create Gemfile:

source 'https://rubygems.org' gem 'rails', '7.1.0'gem 'puma', '6.4.0'gem 'pg', '1.5.0'gem 'json', '2.6.3' group :development, :test do  gem 'rspec-rails', '6.1.0'  gem 'factory_bot_rails', '6.4.0'end

Create app.rb (simple Rack/Rails app):

require 'rack' class App  def call(env)    path = env['PATH_INFO']     case path    when '/'      [200, { 'Content-Type' => 'application/json' }, [        { message: 'Hello from CleanStart', version: '1.0' }.to_json      ]]    when '/health'      [200, { 'Content-Type' => 'application/json' }, [        { status: 'healthy', version: '1.0' }.to_json      ]]    else      [404, { 'Content-Type' => 'application/json' }, [        { error: 'Not Found' }.to_json      ]]    end  endend run App.new

Or create Gemfile for a full Rails app:

gem install railsrails new my-rails-appcd my-rails-app

Step 2: Create a CleanStart YAML Declaration

Create cleanstart.yaml to declare your application for the two-factory build process:

apiVersion: cleanstart/v1kind: ImageDeclarationmetadata:  name: my-rails-app  version: "1.0"spec:  runtime:    base: cleanstart/ruby:3.3    sdk: ruby:3.3  dependencies:    source: "./Gemfile"    lockfile: "./Gemfile.lock"    resolveWith: "bundler"  build:    stages:      - name: "builder"        commands:          - "bundle install --without development test"          - "bundle exec rake assets:precompile"      - name: "runtime"        artifacts:          - source: "./app"            destination: "/app"  security:    packageFactory:      verify: true      signingPolicy: "strict"    container:      user: 1000      readOnlyRootFilesystem: true

This declaration ensures all gems pass through the Package Factory, where each dependency is scanned and verified before assembly in the Image Vault.

Step 3: Create Production Dockerfile

# Build stageFROM cleanstart/ruby:3.3 AS builder WORKDIR /appCOPY Gemfile Gemfile.lock ./ RUN bundle install --without development test --jobs 4 --retry 3 # Asset precompilation for RailsCOPY . .RUN bundle exec rake assets:precompile # Production stageFROM cleanstart/ruby:3.3 WORKDIR /app # Create non-root userRUN useradd -m -u 1000 appuser # Copy gems from builderCOPY --from=builder /usr/local/bundle /usr/local/bundleCOPY --from=builder /app . # Set gem pathENV BUNDLE_PATH=/usr/local/bundleENV GEM_HOME=/usr/local/bundle RUN chown -R appuser:appuser /app USER appuserEXPOSE 3000 HEALTHCHECK --interval=30s --timeout=3s --start-period=10s --retries=3 \    CMD curl -f http://localhost:3000/health || exit 1 ENV RAILS_ENV=productionCMD ["bundle", "exec", "puma", "-b", "tcp://0.0.0.0:3000"]

Step 4: Build and Run

# Build the Docker imagedocker build -t my-rails-app:latest . # Run locallydocker run --rm -p 3000:3000 my-rails-app:latest # Test the applicationcurl http://localhost:3000/curl http://localhost:3000/health

Multi-Stage Builds with Gem Layer Optimization

Optimize Docker layer caching by separating gem installation from application code:

# Dependencies stageFROM cleanstart/ruby:3.3 AS dependencies WORKDIR /tmpCOPY Gemfile Gemfile.lock ./ RUN bundle install --without development test --jobs 4 --retry 3 --path vendor/bundle # Builder stageFROM cleanstart/ruby:3.3 AS builder WORKDIR /appCOPY --from=dependencies /tmp/vendor ./vendorCOPY . . # Precompile assets (Rails)RUN bundle exec rake assets:precompile || true # Runtime stageFROM cleanstart/ruby:3.3 WORKDIR /appRUN useradd -m -u 1000 appuser # Copy gems from dependencies stageCOPY --from=dependencies /tmp/vendor ./vendor # Copy application from builder stageCOPY --from=builder --chown=appuser:appuser /app . ENV BUNDLE_PATH=/app/vendorUSER appuser EXPOSE 3000HEALTHCHECK --interval=30s --timeout=3s CMD curl -f http://localhost:3000/health || exit 1 ENV RAILS_ENV=productionCMD ["bundle", "exec", "puma", "-b", "tcp://0.0.0.0:3000", "-w", "4"]

Bundler Integration and Dependency Resolution

CleanStart verifies all gems through the Package Factory. Configure your Gemfile for production deployments:

source 'https://rubygems.org'ruby '3.3.0' # Web servergem 'puma', '6.4.0'gem 'rails', '7.1.0' # Databasegem 'pg', '1.5.0'gem 'activerecord', '~> 7.1' # Logginggem 'json', '2.6.3' # Securitygem 'bcrypt', '3.1.20' # Cachinggem 'redis', '5.0.0' group :development do  gem 'pry-rails', '0.3.9'end group :test do  gem 'rspec-rails', '6.1.0'  gem 'factory_bot_rails', '6.4.0'  gem 'faker', '3.2.1'end

In your Dockerfile, ensure Bundler creates a lockfile and verifies gem checksums:

RUN bundle install --deployment --without development test && \    bundle audit check --update

Use Gemfile.lock to pin exact versions. Audit dependencies with:

docker run --rm my-rails-app bundle auditdocker run --rm my-rails-app bundle outdated

Puma and Unicorn Runtime Configuration

Puma Configuration (Recommended)

Create config/puma.rb:

max_threads_count = ENV.fetch("RAILS_MAX_THREADS") { 5 }min_threads_count = ENV.fetch("RAILS_MIN_THREADS") { max_threads_count }threads min_threads_count, max_threads_count port = ENV.fetch("PORT") { 3000 }bind "tcp://0.0.0.0:#{port}" workers = ENV.fetch("WEB_CONCURRENCY") { 4 }worker_timeout = ENV.fetch("WORKER_TIMEOUT") { 60 } environment ENV.fetch("RAILS_ENV") { "development" } plugin :tmp_restart log_requests truepreload_app! on_worker_boot do  ActiveRecord::Base.establish_connection if defined?(ActiveRecord)end

In your Dockerfile:

ENV RAILS_MAX_THREADS=5ENV RAILS_MIN_THREADS=2ENV WEB_CONCURRENCY=4ENV WORKER_TIMEOUT=60ENV PORT=3000 CMD ["bundle", "exec", "puma", "-c", "config/puma.rb"]

Unicorn Configuration (Legacy)

If using Unicorn, create config/unicorn.rb:

worker_processes 4timeout 60 before_fork do |server, worker|  if defined?(ActiveRecord::Base)    ActiveRecord::Base.connection.disconnect!  endend after_fork do |server, worker|  if defined?(ActiveRecord::Base)    ActiveRecord::Base.establish_connection  endend listen 3000, backlog: 128

FIPS 140-3 Compliance for Ruby OpenSSL

For regulated environments, use CleanStart's FIPS-enabled Ruby images and ensure OpenSSL compliance:

FROM cleanstart/ruby:3.3-fips WORKDIR /appCOPY Gemfile Gemfile.lock ./RUN bundle install --without development test

In your Ruby code, verify FIPS mode is enabled:

require 'openssl' puts "FIPS enabled: #{OpenSSL::FIPS.mode == 1}" # Use FIPS-approved algorithmsdigest = OpenSSL::Digest::SHA256.new # FIPS-approvedcipher = OpenSSL::Cipher.new('aes-256-gcm') # FIPS-approved # Avoid non-FIPS algorithms# md5 = OpenSSL::Digest::MD5.new # Non-FIPS

Test FIPS compliance in a container:

docker run --rm cleanstart/ruby:3.3-fips \  ruby -e "require 'openssl'; puts \"FIPS: #{OpenSSL::FIPS.mode == 1}\""

Scanning and Signing Images

After building your Ruby image, scan it for vulnerabilities and sign it for supply chain security:

# Build and tag your imagedocker build -t myregistry/my-rails-app:1.0 . # Scan with CleanStart scannercleanimg-scan myregistry/my-rails-app:1.0 # Audit gems with bundlerdocker run --rm myregistry/my-rails-app:1.0 bundle audit # Sign with Cosigncosign sign --key cosign.key myregistry/my-rails-app:1.0 # Push to Image Vaultdocker push myregistry/my-rails-app:1.0 # Verify signature before pullingcosign verify --key cosign.pub myregistry/my-rails-app:1.0

Deploying to Kubernetes

Create a complete Kubernetes deployment with CleanStart hardening:

apiVersion: apps/v1kind: Deploymentmetadata:  name: rails-app  namespace: defaultspec:  replicas: 3  selector:    matchLabels:      app: rails-app  template:    metadata:      labels:        app: rails-app    spec:      securityContext:        fsGroup: 65532        seccompProfile:          type: RuntimeDefault      containers:      - name: app        image: myregistry/my-rails-app:1.0        imagePullPolicy: IfNotPresent        ports:        - containerPort: 3000          name: http          protocol: TCP        env:        - name: RAILS_ENV          value: "production"        - name: PORT          value: "3000"        - name: WEB_CONCURRENCY          value: "4"        - name: RAILS_LOG_TO_STDOUT          value: "true"        - name: DATABASE_URL          valueFrom:            secretKeyRef:              name: app-secrets              key: database-url        securityContext:          allowPrivilegeEscalation: false          readOnlyRootFilesystem: true          runAsNonRoot: true          runAsUser: 1000          runAsGroup: 1000          capabilities:            drop:            - ALL        livenessProbe:          httpGet:            path: /health            port: 3000          initialDelaySeconds: 15          periodSeconds: 20          timeoutSeconds: 3          failureThreshold: 3        readinessProbe:          httpGet:            path: /health            port: 3000          initialDelaySeconds: 10          periodSeconds: 10          timeoutSeconds: 3          failureThreshold: 2        resources:          requests:            cpu: 250m            memory: 256Mi          limits:            cpu: 1000m            memory: 512Mi        volumeMounts:        - name: tmp          mountPath: /tmp        - name: rails-tmp          mountPath: /app/tmp      volumes:      - name: tmp        emptyDir: {}      - name: rails-tmp        emptyDir: {}---apiVersion: v1kind: Servicemetadata:  name: rails-appspec:  type: ClusterIP  selector:    app: rails-app  ports:  - port: 80    targetPort: 3000    protocol: TCP

Testing Your Ruby Application

Add RSpec to your project for comprehensive testing:

# Gemfilegroup :test do  gem 'rspec-rails', '6.1.0'  gem 'factory_bot_rails', '6.4.0'  gem 'rack-test', '2.1.0'end

Create spec/app_spec.rb:

require 'rack/test' describe 'My Rails App' do  include Rack::Test::Methods   let(:app) { MyApp.new }   it 'returns hello on root' do    get '/'    expect(last_response).to be_ok    expect(JSON.parse(last_response.body)['message']).to eq('Hello from CleanStart')  end   it 'returns health status' do    get '/health'    expect(last_response).to be_ok    expect(JSON.parse(last_response.body)['status']).to eq('healthy')  endend

Run tests in Docker:

FROM cleanstart/ruby:3.3 AS test WORKDIR /appCOPY Gemfile Gemfile.lock ./RUN bundle install COPY . .RUN bundle exec rspec

Image Options

CleanStart provides Ruby variants for different needs:

Image

Use Case

cleanstart/ruby:3.3

Latest version, recommended for production

cleanstart/ruby:3.2

Stable LTS, widely tested

cleanstart/ruby:3.1

Legacy support

cleanstart/ruby:3.3-fips

FIPS 140-3 compliance required

Performance Tips

You should layer gems separately from application code to benefit from Docker's layer caching. Use bundle install --jobs 4 to parallelize gem installation and speed up builds. Precompile Rails assets in the build stage rather than at runtime. Tune web concurrency based on CPU cores available to the container. Configure database connection pooling appropriately for your concurrency level.

Troubleshooting

Build Issues

Problem: Bundler fails with "certificate verification failed" during gem installation Cause: SSL certificate issues; corporate proxy intercepting traffic; gem server unreachable Fix: In Dockerfile, configure bundler to use HTTP or skip verification temporarily (not for production). Add to Dockerfile: RUN bundle config set --global ssl_verify_mode 0 (only in build stage). Better fix: configure proxy in Gemfile if needed: add to ~/.bundlerc or set environment variable BUNDLE_HTTPS_PROXY=http://proxy:8080. For Ruby 3.3+, use bundler cache: RUN --mount=type=cache,target=/usr/local/bundle/cache bundle install.

Problem: "Could not find gem 'xyz' in any of the gem sources" during bundle install Cause: Gem source not configured; typo in Gemfile; gem removed from rubygems.org Fix: Verify gem name in Gemfile matches exactly (case-sensitive). Check if gem exists: gem search xyz. Add explicit gem source if using private registry: add source 'https://private-gem-server.com' before gem declaration. Ensure Gemfile.lock is current: run bundle lock --update locally and commit.

Problem: Docker layer caching fails; every source code change rebuilds all gems Cause: Gemfile copied with application code; Docker detects changes and invalidates cache Fix: Restructure Dockerfile: copy only Gemfile and Gemfile.lock first, install gems, then copy application code. This preserves gem layer cache when source changes: COPY Gemfile Gemfile.lock . && RUN bundle install && COPY . ..

Problem: Build takes 20+ minutes due to slow gem compilation Cause: Native extensions compiling from source; C dependencies missing; slow network to gem server Fix: Use pre-compiled gems where available (most modern gems have pre-built binaries). Parallel installation: bundle install --jobs 4. For gems with native extensions (e.g., pg, mysql2), ensure build tools available: RUN apt-get install -y build-essential ruby-dev. Cache gem cache between builds: use BuildKit cache mount. Or pre-download gems in separate Docker layer.

Problem: Gemfile.lock conflict; "different gems specified in Gemfile" or lock file corruption Cause: Manual edits to Gemfile.lock; bundle version mismatch; concurrent bundle lock operations Fix: Regenerate Gemfile.lock locally: rm Gemfile.lock && bundle install && bundle lock. Never manually edit lock file. Commit lock file to version control. Use same bundler version in Docker as development: pin in Dockerfile: RUN gem install bundler:2.5.0 if needed.

Runtime Issues

Problem: Rails app crashes with "Couldn't find gem 'xyz'" at runtime Cause: Gem not in Gemfile; transitive dependency missing; bundle cache not mounted Fix: Verify all gems copied from build stage: check final image contains all gems. Run bundle list in container to audit installed gems. Ensure Gemfile.lock unchanged between build and commit. For multi-stage, copy entire vendor/bundle or gem path correctly: COPY --from=builder /usr/local/bundle /usr/local/bundle.

Problem: Puma workers crash with "Worker timeout" or "Process stuck" Cause: Workers set too high; long-running requests; database connection pool exhausted Fix: Tune worker count: typically (2 * CPU_cores) + 1. In container, check actual CPUs: docker run --cpus=1 my-app cat /proc/cpuinfo | grep processor | wc -l. Increase request timeout: set ENV WORKER_TIMEOUT=120. For database connections, ensure connection pool size matches worker count: pool: 4 in config/database.yml if using 4 workers.

Problem: Port binding fails with "Address already in use" Cause: Puma binding to wrong interface; previous container still holding port; port conflict Fix: Ensure Puma binds to all interfaces: bind 'tcp://0.0.0.0:3000' in puma.rb or --bind tcp://0.0.0.0:3000 on CLI. Check for orphaned containers: docker ps -a | grep rails-app. Clean up: docker container prune. Verify port exposure in Dockerfile: EXPOSE 3000.

Problem: Rails log levels not working; debug logs not appearing Cause: Rails.logger not configured for stdout; log level ENV variable ignored Fix: Ensure RAILS_LOG_TO_STDOUT=true in environment. Configure logging in config/environments/production.rb: config.logger = ActiveSupport::Logger.new(STDOUT). For custom log level, set RAILS_LOG_LEVEL=debug (default is info). Verify with: docker run -e RAILS_LOG_LEVEL=debug my-app rails console.

Problem: HEALTHCHECK fails; endpoints return 500 or timeout Cause: Database not ready; Rails not fully initialized; ActiveRecord connection issue Fix: Implement proper health checks that verify database connectivity: add health controller that checks ActiveRecord::Base.connection.active?. Increase initialDelaySeconds in Kubernetes or start-period in Docker to allow Rails to boot. For cold starts, use startup probe in Kubernetes. Verify endpoint locally: curl -v http://localhost:3000/health.

Problem: Memory usage grows unbounded; "Out of Memory" after hours Cause: Memory leak in gems or application code; object cache not limited; connection pool holding connections Fix: Monitor memory: docker stats. For Rails, check for memory leaks with derailed_benchmarks gem. Limit gem cache size: wrap caching in background jobs with memory limits. Ensure connection pooling not creating unlimited connections. Use ruby-prof or memory_profiler to identify leaks locally.

Problem: FIPS mode enabled but OpenSSL raises errors; "cipher not available" Cause: Application uses non-FIPS ciphers or algorithms; gem doesn't support FIPS Fix: Audit gems with OpenSSL: bundle list | grep openssl. Review application crypto usage: replace MD5 with SHA256, DES with AES. Disable FIPS if not required: set OpenSSL::FIPS.mode = false in initializer (not recommended for compliance). Test in FIPS container first: docker run -it cleanstart/ruby:3.3-fips ruby -e 'require "openssl"; ...'.

Performance Issues

Problem: Application startup takes 30+ seconds; slow boot time in containers Cause: Large gem dependencies loaded on startup; Rails autoloading slow; gems doing too much in initializers Fix: Profile startup: use ruby-prof --printer graph_html -- bin/rails console < /dev/null to see slow requires. Defer non-essential initialization: move into lazy-loaded concerns. For rails c or rake tasks, skip unnecessary gems: add to Gemfile gem 'foo', require: false and require only when needed. Use bootsnap gem to cache require paths: add to Gemfile and preload in config/boot.rb.

Problem: Asset precompilation takes 10+ minutes in Docker build Cause: Large asset pipeline; Sprockets not parallelized; missing Node.js for JavaScript compilation Fix: Ensure Node.js available in build stage for JavaScript: RUN apt-get install -y nodejs npm. Precompile in separate Docker layer, before copying source: separate assets into own layer for caching. Use RAILS_ENV=production RAILS_GROUPS=assets bundle exec rake assets:precompile. For production, consider asset CDN and skip precompilation: set config.assets.compile = false.

Problem: Database migration timeout during deployment; "migration stuck" Cause: Long-running migration; database lock; migration not completing within timeout Fix: Run migrations separately from application startup: use init container in Kubernetes to run bundle exec rake db:migrate before app container starts. Set migration timeout explicitly: ActiveRecord::Migrator.migrations_paths; ActiveRecord::Migration.execution_mode = :all; ActiveRecord::Schema.migration_mode_to_switch_back = nil. For slow migrations, profile: bundle exec rake db:migrate:verbose.

CleanStart Production Hardening

Read-Only Root Filesystem

Enforce immutable infrastructure with read-only root and controlled writable paths:

apiVersion: v1kind: Podmetadata:  name: rails-appspec:  containers:  - name: app    image: cleanstart/ruby:3.3    securityContext:      readOnlyRootFilesystem: true    volumeMounts:    - name: tmp      mountPath: /tmp    - name: rails-tmp      mountPath: /app/tmp    - name: rails-logs      mountPath: /app/log  volumes:  - name: tmp    emptyDir: {}  - name: rails-tmp    emptyDir: {}  - name: rails-logs    emptyDir: {}

Shell-Less ENTRYPOINT

Remove shell from the container for reduced attack surface. Update your Dockerfile:

FROM cleanstart/ruby:3.3 WORKDIR /appCOPY . .RUN bundle install --without development test # Declarative Image Builder: Use cleanimg-init as PID 1ENTRYPOINT ["/cleanimg-init", "--"]CMD ["bundle", "exec", "puma", "-b", "tcp://0.0.0.0:3000"]

cleanimg-customize: Custom Ruby Configuration

Inject production settings and security hardening:

FROM cleanstart/ruby:3.3 WORKDIR /appCOPY Gemfile Gemfile.lock ./RUN bundle install --without development test COPY . . # Configure runtime hardeningENV RAILS_ENV=productionENV RAILS_LOG_TO_STDOUT=trueENV WEB_CONCURRENCY=4ENV RAILS_MAX_THREADS=5 ENTRYPOINT ["/cleanimg-init", "--"]CMD ["bundle", "exec", "puma", "-b", "tcp://0.0.0.0:3000"]

Security Context

Complete Kubernetes securityContext for hardened Ruby containers:

apiVersion: apps/v1kind: Deploymentmetadata:  name: rails-appspec:  template:    spec:      securityContext:        fsGroup: 65532        seccompProfile:          type: RuntimeDefault      containers:      - name: app        image: cleanstart/ruby:3.3        securityContext:          allowPrivilegeEscalation: false          readOnlyRootFilesystem: true          runAsNonRoot: true          runAsUser: 65532          runAsGroup: 65532          capabilities:            drop:            - ALL        resources:          requests:            cpu: 250m            memory: 256Mi          limits:            cpu: 1000m            memory: 512Mi

Next Steps

Explore Advanced Ruby Deployment. Learn Kubernetes Deployment for Ruby. Review CleanStart Image Registry.