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 healthcheck 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