Ruby on Rails
Despliega Rails y otras apps Ruby en Skiffly con Railpack — Bundler, la versión de Ruby, precompilación de assets, `SECRET_KEY_BASE` y `RAILS_MASTER_KEY`, Puma en `PORT`, Postgres y Redis, `db:prepare`, Sidekiq y Active Storage.
Qué detecta Railpack#
| Revisa | Efecto |
|---|---|
Gemfile | el servicio es una app de Ruby; bundle install con la versión de Bundler de Gemfile.lock |
RAILPACK_RUBY_VERSION, .ruby-version, la línea ruby del Gemfile, .tool-versions/mise.toml | la versión de Ruby (por defecto 3.4) |
config/application.rb | Rails: paquetes apt para las gemas de base de datos (libpq-dev para pg, cliente MySQL para mysql2), bundle exec rake assets:precompile cuando hay Sprockets o Propshaft, precompilación de Bootsnap; se omite en apps API-only |
package.json o la gema execjs | se instala Node y npm run build se ejecuta antes de los assets (jsbundling, cssbundling, esbuild, Tailwind) |
config/environment.rb, luego config.ru, luego un Rakefile | el comando de inicio cuando no es Rails |
El runtime de Railpack define MALLOC_ARENA_MAX=2 y carga jemalloc, lo que mantiene estable la memoria de Puma. RAILS_ENV no se define por ti: agrega RAILS_ENV=production (y RACK_ENV=production) como variables del servicio antes del primer despliegue para que bundle install omita los grupos development/test y los assets se compilen para producción.
Assets y secretos en tiempo de build#
assets:precompile se ejecuta durante el build, donde todas las variables del servicio están disponibles. Rails necesita credenciales para arrancar incluso en ese paso:
RAILS_MASTER_KEY: el contenido deconfig/master.key(oconfig/credentials/production.key), oSECRET_KEY_BASE: si no usas credenciales cifradas.SECRET_KEY_BASE_DUMMY=1permite que el paso de precompilación arranque sin una clave real en Rails 7.1+, pero el runtime sigue necesitando una de las dos.
skiffly variables set --skip-deploys RAILS_ENV=production RAILS_MASTER_KEY=$(cat config/master.key)Puerto y Puma#
El config/puma.rb de un Rails reciente ya hace port ENV.fetch("PORT", 3000) y se enlaza a 0.0.0.0. Skiffly define PORT con el puerto del servicio; deja el puerto del servicio sin definir (entonces PORT=8080) o configúralo en 3000, ambas opciones funcionan. RAILS_SERVE_STATIC_FILES=1 (o config.public_file_server.enabled = true) es obligatorio, no hay nginx delante del contenedor; agrega RAILS_LOG_TO_STDOUT=1 para que skiffly logs muestre el log de solicitudes.
# config/environments/production.rb
config.hosts << ENV["SKIFFLY_PUBLIC_DOMAIN"] if ENV["SKIFFLY_PUBLIC_DOMAIN"]
config.hosts << "app.example.com"
config.force_ssl = true # X-Forwarded-Proto lo define el edgeconfig.hosts debe incluir el dominio o Rails responde Blocked hosts. La sonda del health check llega con la dirección privada como Host, así que excluye la ruta de la sonda: config.host_authorization = { exclude: ->(req) { req.path == "/up" } }.
Comando de inicio y migraciones#
Configura el comando de inicio en Configuración → Despliegue → Comando de inicio (startCommand):
bundle exec rails db:prepare && bundle exec puma -C config/puma.rbdb:prepare crea la base de datos en la primera ejecución y migra en las siguientes; no hay fase de release, así que esta línea hace lo que release: rails db:migrate hacía en Heroku. El arranque tiene cinco minutos antes de que el health check se rinda. Para migraciones de datos largas, ejecútalas tú mismo: skiffly ssh -- bundle exec rails db:migrate (o rails console, rails runner).
Postgres y Redis#
skiffly deploy --template postgres
skiffly deploy --template redis
skiffly variables set 'DATABASE_URL=${{Postgres.DATABASE_URL}}' 'REDIS_URL=${{Redis.REDIS_URL}}'config/database.yml con url: <%= ENV["DATABASE_URL"] %> para producción (Rails lo combina automáticamente cuando solo está definida DATABASE_URL). Action Cable, Sidekiq, Solid Queue/Cache/Cable (con Postgres) y Rails.cache leen REDIS_URL/DATABASE_URL como de costumbre. La base de datos solo es alcanzable en la red privada; skiffly connect postgres abre psql desde tu máquina.
Sidekiq, Solid Queue y cron#
- Sidekiq / Solid Queue / GoodJob: un segundo servicio del mismo repositorio, comando de inicio
bundle exec sidekiq -C config/sidekiq.yml(obin/jobs), sin dominio, las mismas variables mediante referencias (RAILS_MASTER_KEY=${{web.RAILS_MASTER_KEY}}). - Tareas programadas: un servicio con programación cron y
bundle exec rails cleanup:runcomo comando de inicio, o sidekiq-cron dentro del worker.
Active Storage y subidas#
El almacenamiento en disco es efímero. Monta un volumen (skiffly volume add --mount-path /app/storage, una sola réplica) o usa un servicio compatible con S3 (gema aws-sdk-s3 con la plantilla minio o un bucket externo) con config.active_storage.service = :s3.
Health check#
Rails 7.1+ incluye GET /up. Configura la ruta del health check en /up y exclúyela de la autorización de hosts (arriba).
Problemas comunes#
| Síntoma | Solución |
|---|---|
ArgumentError: Missing secret_key_base for 'production' | define SECRET_KEY_BASE o RAILS_MASTER_KEY |
ActiveSupport::MessageEncryptor::InvalidMessage | RAILS_MASTER_KEY incorrecta para el archivo de credenciales del repositorio |
Blocked hosts: web-x1y2.skiffly.cloud | agrega el dominio a config.hosts |
| Assets con 404, página sin estilos | RAILS_SERVE_STATIC_FILES=1, y revisa el paso de precompilación en skiffly logs --build |
PG::ConnectionBad: could not connect | falta la referencia DATABASE_URL o el servicio de Postgres todavía está arrancando |
Could not find gem 'pg' durante el build | Gemfile.lock desactualizado, o la gema está en un grupo excluido por RAILS_ENV |
Puma registra Listening on http://127.0.0.1:3000 | bind "tcp://0.0.0.0:#{ENV.fetch('PORT', 3000)}" en puma.rb |
Bundler::GemNotFound para una gema de plataforma | bundle lock --add-platform x86_64-linux y haz commit del lockfile |
Ejemplo#
import { defineSkiffly, generate, github, postgres, project, redis, service } from "@skiffly/config";
export default defineSkiffly(() => {
const db = postgres("db");
const cache = redis("cache");
const env = { RAILS_ENV: "production", RAILS_LOG_TO_STDOUT: "1", RAILS_SERVE_STATIC_FILES: "1", DATABASE_URL: db.env.DATABASE_URL, REDIS_URL: cache.env.REDIS_URL, SECRET_KEY_BASE: generate("hex64") };
const web = service("web", { source: github("acme/app"), start: "bundle exec rails db:prepare && bundle exec puma -C config/puma.rb", healthcheck: "/up", port: 3000, env, domain: true });
const jobs = service("jobs", { source: github("acme/app"), start: "sh -c 'ruby -run -e httpd /tmp -p $PORT -b 0.0.0.0 & exec bundle exec sidekiq'", env: { ...env, SECRET_KEY_BASE: web.env.SECRET_KEY_BASE } });
return project("app", { resources: [db, cache, web, jobs] });
});Landing: /deploy/rails