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 at | Effect |
|---|---|
Gemfile | the 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.toml | the Ruby version (default 3.4) |
config/application.rb | Rails: 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 gem | Node is installed and npm run build executes before assets (jsbundling, cssbundling, esbuild, Tailwind) |
config/environment.rb, then config.ru, then a Rakefile | the 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 ofconfig/master.key(orconfig/credentials/production.key), orSECRET_KEY_BASE— if you do not use encrypted credentials.SECRET_KEY_BASE_DUMMY=1lets 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 edgeconfig.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.rbdb: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(orbin/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:runas 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 health check path to /up and exclude it from host authorization (above).
Common problems#
| Symptom | Fix |
|---|---|
ArgumentError: Missing secret_key_base for 'production' | set SECRET_KEY_BASE or RAILS_MASTER_KEY |
ActiveSupport::MessageEncryptor::InvalidMessage | wrong RAILS_MASTER_KEY for the credentials file in the repository |
Blocked hosts: web-x1y2.skiffly.cloud | add the domain to config.hosts |
| Assets 404, page unstyled | RAILS_SERVE_STATIC_FILES=1, and check the precompile step in skiffly logs --build |
PG::ConnectionBad: could not connect | DATABASE_URL reference missing or the Postgres service is still starting |
Could not find gem 'pg' during the build | Gemfile.lock out of date, or the gem is in a group excluded by RAILS_ENV |
Puma logs Listening on http://127.0.0.1:3000 | bind "tcp://0.0.0.0:#{ENV.fetch('PORT', 3000)}" in puma.rb |
Bundler::GemNotFound for a platform gem | bundle lock --add-platform x86_64-linux and commit the lockfile |
Example#
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