Rails DevOps Patterns
Analyze and recommend best practices for Rails deployment, containerization, CI/CD, monitoring, security, and production optimization.
Follow standard Docker, CI/CD, and Puma conventions. Focus on the opinionated Rails-specific patterns below.
See patterns.md for full configuration examples.
Quick Reference
| Area | Key Files | Tool/Service |
|------|-----------|-------------|
| Docker | Dockerfile, docker-compose.yml | Docker, BuildKit |
| CI/CD | .github/workflows/ci.yml | GitHub Actions |
| Server | config/puma.rb | Puma |
| Security | config/initializers/rack_attack.rb | Rack::Attack |
| Monitoring | config/environments/production.rb | Lograge, structured JSON |
| Deploy | bin/deploy, config/deploy/ | Kamal, Capistrano |
Core Principles
- Immutable deploys: Build once, deploy the same artifact everywhere
- Non-root containers: Always run as a non-root USER in production
- Health checks at /up: Load balancers and orchestrators need them
- Structured JSON logging: Machine-parseable logs to STDOUT
- Defense in depth: SSL + rate limiting + CSP headers + statement timeouts
Production Dockerfile (Alpine, Non-Root)
The key non-obvious elements: SECRET_KEY_BASE=precompile_placeholder for asset compilation, non-root user, and HEALTHCHECK directive.
# syntax=docker/dockerfile:1
FROM ruby:3.2-alpine AS base
RUN apk add --no-cache postgresql-dev tzdata gcompat
WORKDIR /app
ENV RAILS_ENV=production \
BUNDLE_DEPLOYMENT=true \
BUNDLE_WITHOUT="development:test"
FROM base AS build
RUN apk add --no-cache build-base git
COPY Gemfile Gemfile.lock ./
RUN bundle install --jobs 4 --retry 3
# Only install Node deps if package.json exists (skip for importmap apps)
COPY package.json* yarn.lock* ./
RUN if [ -f package.json ]; then apk add --no-cache nodejs yarn && yarn install --production --frozen-lockfile; fi
COPY . .
RUN SECRET_KEY_BASE=precompile_placeholder bundle exec rails assets:precompile
FROM base AS runtime
COPY --from=build /usr/local/bundle /usr/local/bundle
COPY --from=build /app /app
RUN addgroup -S rails && adduser -S rails -G rails && \
chown -R rails:rails /app
USER rails
EXPOSE 3000
HEALTHCHECK --interval=30s --timeout=3s CMD wget -qO- http://localhost:3000/up || exit 1
CMD ["bundle", "exec", "puma", "-C", "config/puma.rb"]
Health Check Endpoint
# config/routes.rb — minimal for load balancers
get "/up", to: proc { [200, {}, ["OK"]] }
# Detailed check (database, redis, migrations) — see patterns.md
get "/health", to: "health#show"
Structured JSON Logging
# config/environments/production.rb
config.logger = ActiveSupport::TaggedLogging.new(
Logger.new(STDOUT).tap do |logger|
logger.formatter = proc do |severity, time, _progname, msg|
{
severity: severity,
time: time.iso8601(3),
msg: msg,
host: Socket.gethostname,
pid: Process.pid,
tid: Thread.current.object_id
}.to_json + "\n"
end
end
)
config.log_tags = [:request_id]
config.log_level = ENV.fetch("LOG_LEVEL", "info").to_sym
SSL with Health Check Exclusion
# config/environments/production.rb
config.force_ssl = true
config.ssl_options = {
hsts: { subdomains: true, preload: true, expires: 1.year },
redirect: { exclude: ->(request) { request.path.start_with?("/health", "/up") } }
}
Production Environment Variables
RAILS_ENV=production
RAILS_LOG_TO_STDOUT=true
RAILS_SERVE_STATIC_FILES=true
SECRET_KEY_BASE=<generated>
DATABASE_URL=postgres://user:pass@host:5432/dbname
REDIS_URL=redis://host:6379/0
RAILS_MAX_THREADS=5
WEB_CONCURRENCY=2
Database Production Config
Key non-default settings: statement_timeout and lock_timeout prevent runaway queries.
production:
adapter: postgresql
url: <%= ENV["DATABASE_URL"] %>
pool: <%= ENV.fetch("RAILS_MAX_THREADS", 5) %>
variables:
statement_timeout: "30s"
lock_timeout: "10s"
prepared_statements: true
Zero-Downtime Deployment
- Use
pumactl phased-restartfor rolling restarts (workers restart one at a time) - Run
db:migratebefore restarting app servers - Ensure migrations are backward-compatible (add column, then backfill, then add constraint)
- Health check endpoint lets load balancer drain connections
Anti-Patterns
| Bad | Good | Why |
|-----|------|-----|
| Secrets in ENV files committed to git | rails credentials:edit | Security |
| latest Docker tag in production | Pinned image versions | Reproducibility |
| No health check endpoint | GET /up returning 200 | Load balancer needs it |
| Running as root in container | adduser + USER rails | Security |
| Logging to files in containers | Log to STDOUT as JSON | Container best practice |
| No statement_timeout | statement_timeout: '30s' | Prevent runaway queries |
Output Format
When reporting on DevOps configuration, use:
## DevOps Analysis: [area]
**Current State:**
- summary of what exists
**Issues:**
- [severity] description
**Recommendations:**
1. actionable recommendation with file path