Pular para o conteúdo

Buscar na documentação

Buscar na documentação do Skiffly

Ruby on Rails

Deploy Rails and other Ruby apps on Skiffly with Railpack — Bundler, the Ruby version, asset precompilation, `SECRET_KEY_BASE` and `RAILS_MASTER_KEY`, Puma on `PORT`, Postgres and Redis, `db:prepare`, Sidekiq and Active Storage.

What Railpack detects#

Looks atEffect
Gemfilethe service is a Ruby app; bundle install with the Bundler version from Gemfile.lock
RAILPACK_RUBY_VERSION, .ruby-version, the ruby line in the Gemfile, .tool-versions/mise.tomlthe Ruby version (default 3.4)
config/application.rbRails: apt packages for the database gems (libpq-dev for pg, MySQL client for mysql2), bundle exec rake assets:precompile when Sprockets or Propshaft is present, Bootsnap precompile; skipped for API-only apps
package.json or the execjs gemNode is installed and npm run build executes before assets (jsbundling, cssbundling, esbuild, Tailwind)
config/environment.rb, then config.ru, then a Rakefilethe start command when it is not Rails

Railpack's runtime sets MALLOC_ARENA_MAX=2 and loads jemalloc, which keeps Puma's memory flat. RAILS_ENV is not set for you: add RAILS_ENV=production (and RACK_ENV=production) as service variables before the first deploy so bundle install skips development/test groups and assets compile for production.

Assets and secrets at build time#

assets:precompile runs during the build, where every service variable is available. Rails needs credentials to boot even for that step:

  • RAILS_MASTER_KEY — the contents of config/master.key (or config/credentials/production.key), or
  • SECRET_KEY_BASE — if you do not use encrypted credentials. SECRET_KEY_BASE_DUMMY=1 lets the precompile step boot without a real key on Rails 7.1+, but the runtime still needs one of the two.
skiffly variables set --skip-deploys RAILS_ENV=production RAILS_MASTER_KEY=$(cat config/master.key)

Port and Puma#

config/puma.rb from a recent Rails already does port ENV.fetch("PORT", 3000) and binds to 0.0.0.0. Skiffly sets PORT to the service port; either leave the service port unset (then PORT=8080) or set it to 3000, both work. RAILS_SERVE_STATIC_FILES=1 (or config.public_file_server.enabled = true) is required, there is no nginx in front of the container; add RAILS_LOG_TO_STDOUT=1 so skiffly logs shows the request log.

# 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 is set by the edge

config.hosts must include the domain or Rails answers Blocked hosts. The health-check probe comes with the private address as Host, so exclude the probe path: config.host_authorization = { exclude: ->(req) { req.path == "/up" } }.

Start command and migrations#

Set the start command in Settings → Deploy → Start command (startCommand):

bundle exec rails db:prepare && bundle exec puma -C config/puma.rb

db:prepare creates the database on the first run and migrates afterwards; there is no release phase, so this line does what release: rails db:migrate did on Heroku. Startup has five minutes before the health check gives up. For long data migrations run them yourself: skiffly ssh -- bundle exec rails db:migrate (or rails console, rails runner).

Postgres and 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 with url: <%= ENV["DATABASE_URL"] %> for production (Rails merges it automatically when only DATABASE_URL is set). Action Cable, Sidekiq, Solid Queue/Cache/Cable (with Postgres) and Rails.cache read REDIS_URL/DATABASE_URL as usual. The database is only reachable on the private network; skiffly connect postgres opens psql from your machine.

Sidekiq, Solid Queue and cron#

  • Sidekiq / Solid Queue / GoodJob: a second service from the same repository, start command bundle exec sidekiq -C config/sidekiq.yml (or bin/jobs), no domain, the same variables through references (RAILS_MASTER_KEY=${{web.RAILS_MASTER_KEY}}).
  • Scheduled tasks: a service with a cron schedule and bundle exec rails cleanup:run as the start command, or sidekiq-cron inside the worker.

Active Storage and uploads#

Disk storage is ephemeral. Either mount a volume (skiffly volume add --mount-path /app/storage, one replica) or use an S3-compatible service (aws-sdk-s3 gem with the minio template or an external bucket) with config.active_storage.service = :s3.

Health check#

Rails 7.1+ ships GET /up. Set the healthcheck path to /up and exclude it from host authorization (above).

Common problems#

SymptomFix
ArgumentError: Missing secret_key_base for 'production'set SECRET_KEY_BASE or RAILS_MASTER_KEY
ActiveSupport::MessageEncryptor::InvalidMessagewrong RAILS_MASTER_KEY for the credentials file in the repository
Blocked hosts: web-x1y2.skiffly.cloudadd the domain to config.hosts
Assets 404, page unstyledRAILS_SERVE_STATIC_FILES=1, and check the precompile step in skiffly logs --build
PG::ConnectionBad: could not connectDATABASE_URL reference missing or the Postgres service is still starting
Could not find gem 'pg' during the buildGemfile.lock out of date, or the gem is in a group excluded by RAILS_ENV
Puma logs Listening on http://127.0.0.1:3000bind "tcp://0.0.0.0:#{ENV.fetch('PORT', 3000)}" in puma.rb
Bundler::GemNotFound for a platform gembundle lock --add-platform x86_64-linux and commit the lockfile

Example#

.skiffly/skiffly.ts
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