
Table of Contents
By Khimananda Oli | Last reviewed: August 2026
Achieving zero-downtime deployment for Ruby applications requires coordinating application server restarts, web server buffering, and database schema changes so that no in-flight request is dropped during a release. Many teams still experience brief 502 errors or stuck requests because they treat code deployment and database migration as separate, uncoordinated events. This guide covers the exact configuration patterns for Puma, Nginx, and ActiveRecord that eliminate these gaps in production.
How do you configure Puma for zero-downtime deployment for Ruby?
Puma’s phased restart is the cornerstone of zero-downtime deployment for Ruby. Unlike a full restart that kills all workers immediately, a phased restart spawns new workers with the updated code before terminating old ones. This overlap period is what prevents dropped requests. You must configure both the worker count and the preload behavior correctly for this to work reliably.
Essential Puma configuration
Your config/puma.rb must explicitly enable phased restarts and set appropriate worker counts. A common mistake is setting workers equal to CPU cores without accounting for the temporary 2x memory usage during the transition phase.
# 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
# Critical for zero-downtime: enables phased restart via SIGUSR1
workers ENV.fetch("WEB_CONCURRENCY", 4)
preload_app!
# Allow new workers to boot before old ones are killed
worker_timeout 60
worker_boot_timeout 120
# Bind to unix socket for Nginx upstream
bind "unix:///var/run/myapp/puma.sock"
on_worker_boot do
# Re-establish DB connection in each new worker
ActiveRecord::Base.establish_connection if defined?(ActiveRecord)
end The preload_app! directive is non-negotiable for phased restarts. Without it, Puma cannot fork new workers from an updated master process. The worker_boot_timeout should be set higher than your application’s actual boot time; if your app takes 45 seconds to load, set this to at least 90 seconds to prevent premature worker termination during slow deploys.
Triggering the phased restart
In your deployment script (Capistrano, Kamal, or custom), send SIGUSR1 to the Puma master process after code is deployed but before cleanup:
# In your deploy hook or systemd ExecReload
kill -SIGUSR1 $(cat /var/run/myapp/puma.pid)
# Or with systemd
systemctl reload myapp-puma.service Monitor pumactl stats or your observability stack to confirm new workers reach ready state before old workers begin shutting down. If you use the four golden signals of monitoring, track saturation specifically during the restart window to catch memory pressure early.
How does Nginx prevent 502 errors during Ruby deployments?
Even with perfect Puma configuration, Nginx can cause 502 Bad Gateway errors if it doesn’t handle the socket transition gracefully. During a phased restart, there is a brief moment when the old socket may be unlinked and the new one isn’t fully accepting connections. Nginx must be configured to retry failed upstream connections transparently.
Nginx upstream configuration
upstream puma_backend {
server unix:/var/run/myapp/puma.sock fail_timeout=0;
# Keepalive reduces TCP overhead and helps during transitions
keepalive 32;
}
server {
listen 80;
server_name example.com;
location / {
proxy_pass http://puma_backend;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
# Critical: retry on next upstream if current fails
proxy_next_upstream error timeout http_502 http_503;
proxy_next_upstream_tries 2;
proxy_next_upstream_timeout 10s;
# Buffer responses to free workers faster
proxy_buffering on;
proxy_buffer_size 8k;
proxy_buffers 16 8k;
}
} The fail_timeout=0 directive tells Nginx never to mark the upstream as permanently failed during transient restart blips. Combined with proxy_next_upstream, this ensures that if a request hits the socket during the exact millisecond it’s being replaced, Nginx immediately retries rather than returning an error to the client. For teams managing multiple services, understanding Nginx vs Apache performance characteristics helps justify why Nginx is preferred for this pattern.
Socket permissions and SELinux/AppArmor
A frequent production issue is the new Puma master creating the socket with different ownership than the old one. Ensure your systemd unit specifies UMask=0002 and that the socket directory is owned by the same user running Puma. On Ubuntu systems with AppArmor, verify the profile allows socket creation in /var/run/myapp/; otherwise, the new worker boots but cannot bind, causing silent failures.
How do you run safe database migrations during Ruby deployments?
Database migrations are where most zero-downtime deployment for Ruby strategies fail. If you deploy code that references a column before the migration runs, or remove a column that old workers still need, you get application errors regardless of how smooth your Puma restart is. The solution is strictly backward-compatible migrations executed in a specific order.
The expand-contract pattern
Every destructive schema change must be split across at least two deployments:
- Expand: Add new columns or tables without removing anything. Deploy code that writes to both old and new structures.
- Migrate data: Backfill existing records into the new structure. This can happen asynchronously via background jobs.
- Contract: Once all code reads from the new structure and data is backfilled, remove the old column in a subsequent deploy.
# Migration 1 (Deploy A): Expand
class AddEmailLowerToUsers < ActiveRecord::Migration[7.2]
def change
add_column :users, :email_lower, :string
add_index :users, :email_lower
end
end
# Application code in Deploy A: Write to both
before_save :sync_email_lower
def sync_email_lower
self.email_lower = email&.downcase
end
# Migration 2 (Deploy B, after backfill): Contract
class RemoveLegacyEmailIndex < ActiveRecord::Migration[7.2]
def change
remove_index :users, :email # Only after confirming zero reads
end
end Never run rails db:migrate as part of the same atomic deploy step as the code release. Run migrations in a pre-deploy hook, verify success, then proceed with the Puma phased restart. For PostgreSQL-specific safety patterns, see PostgreSQL administration essentials which covers lock timeouts and concurrent index creation.
Handling long-running migrations
If a migration takes longer than your deploy timeout, it will block the entire release pipeline. Use CONCURRENTLY for indexes, batch large UPDATE statements, and always set lock_timeout to prevent indefinite blocking:
# Safe concurrent index creation
CREATE INDEX CONCURRENTLY idx_users_email_lower ON users (email_lower);
# Batched updates to avoid long locks
UPDATE users SET email_lower = LOWER(email)
WHERE id IN (SELECT id FROM users WHERE email_lower IS NULL LIMIT 1000); What are the trade-offs between deployment strategies for Ruby?
Phased restarts aren’t the only path to zero-downtime deployment for Ruby. Understanding alternatives helps you choose the right approach for your team’s operational maturity and infrastructure constraints.
| Strategy | Downtime Risk | Memory Overhead | Complexity | Best For |
|---|---|---|---|---|
| Puma Phased Restart | Very Low | 2x during transition | Medium | Most Rails apps on VPS/bare metal |
| Blue-Green Deploy | None | 2x permanent | High | Critical services with budget for duplicate infra |
| Kubernetes Rolling Update | Low | Configurable surge | High | Containerized apps on EKS/GKE/AKS |
| Systemd Socket Activation | None | 1x + buffer | Medium | Single-server apps needing instant failover |
For teams already on Kubernetes, blue-green and canary deploys on Kubernetes provide stronger guarantees than phased restarts alone, at the cost of platform complexity. For single-server setups common among Nepal-based startups, systemd socket activation offers zero-downtime without the memory penalty of keeping two full application sets in RAM.
Implementing reliable zero-downtime deployment for Ruby in production
Reliable zero-downtime deployment for Ruby depends on treating your deployment pipeline as a first-class system component, not an afterthought. Start with Puma phased restarts and Nginx retry configuration as your baseline. Adopt the expand-contract migration pattern before attempting any destructive schema changes. Monitor memory saturation during the transition window, and only graduate to blue-green or Kubernetes rolling updates when your traffic volume or compliance requirements demand it. If your team needs help designing a deployment strategy that matches your infrastructure and audit requirements, reach out to discuss your specific setup.