Run Ruby on Rails on Kubernetes

Khimananda Oli 9 min read Programming and Languages
Run Ruby on Rails on Kubernetes

By Khimananda Oli | Last reviewed: August 2026

Migrating a monolithic framework to container orchestration introduces specific challenges around process management, asset compilation, and stateful background jobs. To successfully run Ruby on Rails on Kubernetes, you must treat the application not as a single server but as a collection of distinct workloads: web servers, background workers, and cron-like schedulers. This guide provides the exact configuration patterns I use in production to ensure stability, security, and audit readiness for Rails applications on modern clusters.

Ingress / LBPuma Web PodsReadiness ProbeSidekiq WorkersQueue ProcessingRedis CachePostgreSQL
High-level architecture to run Ruby on Rails on Kubernetes with separated web and worker tiers

How do you optimize a Dockerfile to run Ruby on Rails on Kubernetes efficiently?

The foundation of any stable Kubernetes deployment is a lean, reproducible container image. When you run Ruby on Rails on Kubernetes, image size directly impacts scaling speed and node costs. A 1.5GB image takes significantly longer to pull during a scale-out event than a 350MB one. Multi-stage builds are mandatory for production Rails containers in 2026.

Multi-stage build pattern

Separate your build dependencies (Node.js, Yarn, build-essential) from your runtime environment. The final stage should contain only the compiled assets, bundled gems, and the Ruby runtime. This reduces the attack surface—a critical requirement for SOC 2 compliance—and minimizes cold-start latency.

# syntax=docker/dockerfile:1
FROM ruby:3.3-slim AS builder

RUN apt-get update && apt-get install -y --no-install-recommends \
    build-essential libpq-dev nodejs npm git \
    && rm -rf /var/lib/apt/lists/*

WORKDIR /app
COPY Gemfile Gemfile.lock ./
RUN bundle config set --local deployment true \
    && bundle config set --local without 'development test' \
    && bundle install --jobs 4

COPY package.json yarn.lock ./
RUN npm install -g yarn && yarn install --frozen-lockfile

COPY . .
RUN SECRET_KEY_BASE_DUMMY=1 bundle exec rails assets:precompile

FROM ruby:3.3-slim AS runtime
RUN apt-get update && apt-get install -y --no-install-recommends \
    libpq5 curl tini \
    && rm -rf /var/lib/apt/lists/*

WORKDIR /app
COPY --from=builder /app /app
COPY --from=builder /usr/local/bundle /usr/local/bundle

ENV RAILS_ENV=production \
    RAILS_LOG_TO_STDOUT=true \
    RAILS_SERVE_STATIC_FILES=true

EXPOSE 3000
ENTRYPOINT ["/app/bin/docker-entrypoint"]
CMD ["bundle", "exec", "puma", "-C", "config/puma.rb"]

This pattern keeps the final image under 400MB while ensuring all assets are precompiled. Note the use of SECRET_KEY_BASE_DUMMY during asset compilation; Rails 7+ requires this environment variable even when compiling static assets that don't touch the database.

How do you configure Puma and health checks when you run Ruby on Rails on Kubernetes?

Puma is the standard application server for Rails, but its default configuration will cause failures in Kubernetes. The orchestrator needs to know exactly when a pod can accept traffic. Without proper probes, you will see intermittent 502 errors during deployments and scaling events. Refer to my guide on Kubernetes resource limits and requests for complementary tuning advice.

Puma thread and worker tuning

Kubernetes pods have hard memory boundaries. Unlike a traditional VM where unused RAM is just wasted, in K8s it triggers OOMKilled restarts. Configure Puma to respect container limits:

  • Workers: Set to 2–4 for most API/web workloads. Each worker forks a new process with shared memory via copy-on-write.
  • Threads: Match your database connection pool size. If pool: 5 in database.yml, set max threads to 5.
  • Preload: Enable preload_app! to maximize memory savings through COW, but ensure you reconnect DB and Redis in an on_worker_boot block.

Liveness vs. Readiness probes

A common mistake is using the same endpoint for both probes. Your readiness probe should verify that Rails can serve requests (DB connected, cache reachable), while liveness should only check if the process is alive. If your readiness probe fails, K8s stops sending traffic. If liveness fails, K8s kills the pod.

# 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

workers ENV.fetch("WEB_CONCURRENCY", 2)
preload_app!

port ENV.fetch("PORT", 3000)
environment ENV.fetch("RAILS_ENV", "production")

on_worker_boot do
  ActiveRecord::Base.establish_connection if defined?(ActiveRecord)
end

# Dedicated health endpoint
plugin :tmp_restart

In your Kubernetes manifest, configure the probes to hit /up (Rails 7.1+ default) or a custom controller that checks downstream dependencies for readiness, but only checks process existence for liveness.

KubeletPuma MasterIngress Controller1. GET /up (readiness)2. 200 OK3. Traffic routedFailure Scenario: Readiness TimeoutGET /up (timeout 3s)No response / 5xxPod remains in NotReady stateZero user traffic sent to pod
Readiness probe lifecycle preventing traffic to unready Rails pods

How do you manage Sidekiq workers and secrets when you run Ruby on Rails on Kubernetes?

Background processing is where most Rails-on-Kubernetes deployments fail. Sidekiq must be deployed as a separate Deployment, never as a sidecar or mixed into the web pod. This allows independent scaling: your web tier might need 10 replicas during peak hours while Sidekiq needs only 3, or vice versa during batch processing windows.

Sidekiq Deployment essentials

Sidekiq pods do not need HTTP probes. Instead, use a process-level check or a dedicated gem like sidekiq-status exposed via a minimal Rack endpoint if you want K8s-aware health checks. More importantly, configure graceful shutdown periods. Sidekiq needs time to finish in-flight jobs before the pod terminates.

spec:
  terminationGracePeriodSeconds: 60
  containers:
  - name: sidekiq
    image: registry.example.com/rails-app:v2.4.0
    command: ["bundle", "exec", "sidekiq", "-C", "config/sidekiq.yml"]
    env:
    - name: SIDEKIQ_CONCURRENCY
      value: "10"
    resources:
      requests:
        memory: "512Mi"
        cpu: "250m"
      limits:
        memory: "1Gi"
        cpu: "1000m"

Secrets management for compliance

When handling sensitive data in regulated environments, never store credentials in ConfigMaps or base64-encoded Secrets without encryption at rest. For teams needing SOC 2 or ISO 27001 compliance, integrate with external secret stores. I detail secure patterns in Kubernetes secrets management done right. Options include:

  • External Secrets Operator: Syncs from AWS Secrets Manager, HashiCorp Vault, or Azure Key Vault into native K8s Secrets.
  • Sealed Secrets: Encrypts secrets so they can be safely stored in GitOps repositories.
  • Vault Agent Injector: Injects secrets as files at runtime without creating K8s Secret objects.

For Rails specifically, avoid putting master.key in the container image. Mount it as a secret volume or inject RAILS_MASTER_KEY as an environment variable from your external store. This ensures credential rotation doesn't require a redeployment.

What deployment strategy prevents downtime when you run Ruby on Rails on Kubernetes?

Rails applications are stateless at the web layer, making them ideal for rolling updates. However, database migrations complicate zero-downtime deployments. You cannot run migrations inside the main application container's entrypoint; doing so causes race conditions where multiple pods attempt schema changes simultaneously.

The migration Job pattern

Run migrations as a Kubernetes Job that executes before the new Deployment rolls out. This is non-negotiable for production safety. Use Helm hooks (pre-upgrade) or Argo CD sync waves to enforce ordering. Learn more about progressive delivery in blue-green and canary deploys on Kubernetes.

StrategyDowntime RiskComplexityBest For
Rolling Update + Pre-migrate JobLowMediumMost Rails apps, standard releases
Blue/GreenNoneHighCritical financial/healthcare systems
Canary (Argo Rollouts)MinimalHighHigh-traffic apps needing validation
In-place Migration (Entrypoint)HighLowDevelopment/staging only

Backward-compatible migrations

Even with pre-migration Jobs, your new code must work with the old schema during the transition window. Follow the expand-contract pattern:

  1. Expand: Add new columns/tables without removing old ones. Deploy.
  2. Migrate: Backfill data, update code to read/write new columns. Deploy.
  3. Contract: Remove deprecated columns after confirming no code references them. Deploy.

This discipline prevents the "new code expects column X but old pods haven't restarted yet" errors that plague naive Rails Kubernetes deployments.

Rolling Updatev1 Podv2 Podv1 Podv2 PodGradual replacement, mixed versionsRequires backward-compatible schemaBlue/GreenBlue (v1) ActiveGreen (v2) StandbySwitchInstant cutover, full isolationDouble resource cost during deployDecision Matrix for Rails TeamsSmall team / frequent deploys → Rolling Update + Expand/Contract MigrationsRegulated / zero-tolerance → Blue/Green with automated smoke testsHigh traffic / risky changes → Canary with metric-based promotion
Deployment strategy comparison for production Rails workloads on Kubernetes

Operational Checklist for Running Rails on Kubernetes

Successfully operating Rails in this environment requires ongoing discipline beyond initial setup. Ensure these items are addressed before considering your deployment production-ready:

  • Log to STDOUT: Never write logs to files inside containers. Use RAILS_LOG_TO_STDOUT=true and ship to a centralized stack.
  • Static file serving: Enable RAILS_SERVE_STATIC_FILES or offload to CDN/Nginx sidecar. Puma should not serve assets in high-traffic scenarios.
  • Connection pooling: Use PgBouncer or similar if running >20 pods. Direct Postgres connections from every pod will exhaust max_connections.
  • Graceful shutdown: Set terminationGracePeriodSeconds ≥ Puma's shutdown_timeout + buffer. Handle SIGTERM properly.
  • Resource boundaries: Always set both requests and limits. Memory limits prevent noisy neighbors; CPU limits prevent throttling surprises.
  • Monitoring: Instrument with OpenTelemetry or Prometheus client gems. Track Puma queue depth, Sidekiq latency, and DB connection wait times.

Next Steps for Your Rails Kubernetes Journey

When you run Ruby on Rails on Kubernetes with these patterns, you gain horizontal scalability, resilient deployments, and infrastructure that passes compliance audits without last-minute scrambles. Start with the multi-stage Dockerfile and separate Sidekiq Deployment, then progressively add external secrets management and advanced deployment strategies as your team matures. If your team needs hands-on guidance implementing these patterns or preparing your Rails infrastructure for SOC 2 certification, reach out to discuss your specific architecture.

Frequently Asked Questions

Use a multi-stage Dockerfile with Ruby 3.3 or later. Compile assets in the build stage, copy only production gems and precompiled assets to the runtime image, and run Puma or Falcon as PID 1 to ensure proper signal handling within pods.

Store credentials in Kubernetes Secrets encrypted at rest using tools like Sealed Secrets or External Secrets Operator. Mount them as environment variables or files via volume mounts, never commit .env files to git, and rotate keys regularly using automated operators.

Yes, but configure HPA based on custom metrics like Puma request queue depth rather than just CPU. Rails is memory-heavy, so set appropriate resource requests and use KEDA for event-driven scaling tied to Sidekiq job queues or HTTP latency thresholds.

Run migrations as a separate Kubernetes Job or init container before the main deployment rolls out. This prevents race conditions where new code runs against an old schema. Ensure the migration job uses the same image tag as the application pods being deployed.

NGINX Ingress Controller or Traefik are standard choices in 2026. Configure WebSocket support if using ActionCable, enable proxy buffering adjustments for large uploads, and set appropriate timeout annotations to match your Puma worker timeout settings to prevent premature disconnects.

Set accurate memory requests based on profiling, not guesses. Use jemalloc instead of glibc malloc, tune Puma worker counts relative to pod memory limits, and enable YJIT in Ruby 3.3+ to reduce object allocation overhead and improve throughput per gigabyte of RAM allocated.

Yes, deploy Sidekiq as a separate Deployment sharing the same container image. Scale workers independently from web pods based on Redis queue length using KEDA. This isolation prevents background jobs from starving web request latency during traffic spikes or heavy batch processing windows.

Use rolling updates with maxSurge and maxUnavailable configured correctly. Implement readiness probes checking a lightweight health endpoint that verifies database connectivity. Ensure Puma handles SIGTERM gracefully by finishing in-flight requests before shutting down workers to avoid dropped connections during pod termination phases.

Prefer S3-compatible object storage over PersistentVolumeClaims for Active Storage uploads. If local disk is required, use ReadWriteMany volumes like NFS or CephFS, but expect performance penalties. Object storage decouples state from pods, simplifying scaling and backup strategies significantly compared to block storage attachments.

Check kubectl logs with previous flag to see crash output. Inspect events with kubectl describe pod for OOMKilled or liveness probe failures. Exec into running pods to verify environment variables and file permissions. Use ephemeral debug containers for distroless images lacking shell access for troubleshooting.

Usually not for single apps under moderate load due to control plane overhead. Consider managed platforms like Render or Fly.io first. Kubernetes makes sense when you have multiple services, need fine-grained autoscaling, or require specific compliance controls that justify the operational complexity and baseline infrastructure costs.

Output structured JSON logs to stdout/stderr only. Let the cluster log collector like Fluent Bit or Vector handle shipping to your backend. Avoid writing to files inside containers. Include request IDs and correlation metadata in every log line to enable distributed tracing across microservices and sidecars.

Run containers as non-root users, drop all Linux capabilities except those strictly needed, and enforce network policies restricting pod-to-pod traffic. Scan images with Trivy before deployment. Enable Pod Security Standards at namespace level to prevent privileged escalation and enforce read-only root filesystems where possible.

Terminate TLS at the ingress controller level using cert-manager for automatic Let's Encrypt certificates. Configure Rails to trust X-Forwarded-Proto headers by setting config.force_ssl and configuring trusted proxies correctly. Never terminate SSL inside the Rails application container itself as it wastes CPU cycles better spent serving requests.

Underestimating memory requirements leading to OOM kills, misconfiguring asset pipeline serving without CDN, forgetting to externalize sessions and cache to Redis, and neglecting graceful shutdown handlers. Also avoid storing uploaded files locally and ensure timezone configuration matches between containers and database to prevent subtle scheduling bugs.