# Rails

> Running Rails here — the database, the secret key, assets in the build, db:prepare at deploy, uploads and SQLite kept.

Rails runs on the [Ruby](stack-ruby.md) image, served by Puma, with no nginx in
front: the platform's proxy terminates TLS and passes requests to it. What
Rails needs from the platform — the database, a secret key base, the
hostnames — is in the environment under the names Rails already reads, so a new
application runs with nothing added to it.

## The manifest

```yaml
version: 1
type: rails

runtime:
  ruby: '4.0'

services:
  - postgres: '18'

build:
  steps:
    - name: Gems
      run: bundle install
    - name: Assets
      run: SECRET_KEY_BASE_DUMMY=1 bin/rails assets:precompile
  cache:
    - vendor/bundle

deploy:
  steps:
    - bin/rails db:prepare
  on_failure: rollback

workers:
  - name: jobs
    command: bin/jobs

health:
  path: /up
```

`type: rails` is a Ruby project; `ruby` says the same. Puma serves the
application from the release with no `start` written, reading
`config/puma.rb`, and `/up` is the health check Rails 7.1 and later answer
once the application has booted. On PostgreSQL, Rails 8's `database.yml` needs
one change before Solid Queue's `bin/jobs` runs: see
[Solid Cache, Queue and Cable](#solid-cache-queue-and-cable).

**Assets are compiled in the build**, which can write, and served by Rails from
the release — fingerprinted, so the CDN keeps them. A build is given no
environment, and `SECRET_KEY_BASE_DUMMY` lets Rails 7.1 and later start without
the key.

**JavaScript bundled with jsbundling-rails or cssbundling-rails** is built
with Node, which the Ruby image does not have. Name Node.js beside your services, and
build in its image first, before Rails compiles what it wrote:

```yaml
services:
  - postgres: '18'
  - node: '26'

build:
  steps:
    - name: Gems
      run: bundle install
    - name: JavaScript and CSS
      image: node
      run: npm ci && npm run build --if-present && npm run build:css --if-present
    - name: Assets
      run: SKIP_JS_BUILD=1 SKIP_CSS_BUILD=1 SECRET_KEY_BASE_DUMMY=1 bin/rails assets:precompile
  cache:
    - vendor/bundle
```

Import maps and Propshaft, Rails's own defaults, and tailwindcss-rails need
neither. See [Node.js as a build tool](stack-node.md).

## The database

**PostgreSQL** is what the platform sets `DATABASE_URL` for, and Rails reads
that over `database.yml`'s production entry: nothing in the file has to change.
**MariaDB** works the same way with the `mysql2` gem, on Rails 7.2 and later,
which read the URL's `mysql://` as mysql2; with Trilogy, add
`ActiveRecord.protocol_adapters.mysql = "trilogy"` to `config/application.rb`.
The first deploy takes the database the file names, or keeps the environment's
where `services` names none, and after it the database is fixed.

**SQLite**, Rails 8's default, runs on an environment of one machine. Keep its
files under `storage/`, which is linked out of every release to a directory
that outlives them all — a new application made without Kamal leaves the paths
for you to fill in:

```yaml
production:
  primary:
    <<: *default
    database: storage/production.sqlite3
```

Where several machines run the application, `storage/` is shared over the
network, and SQLite's files must not be: run PostgreSQL or MariaDB there. Run
one of them for anything you could not lose, too — a database service is backed
up as a database, and SQLite's files are copied as files.

### Solid Cache, Queue and Cable

Rails 8 keeps each in a database of its own: `cache`, `queue` and `cable` in
`database.yml`, files beside the primary's with SQLite. On PostgreSQL the
environment's user may create databases and the backup takes every database on
the server, so give each a name beside your own, from the parts the platform
sets:

```yaml
production:
  primary: &primary_production
    <<: *default
    host: <%= ENV["DB_HOST"] %>
    port: <%= ENV["DB_PORT"] %>
    username: <%= ENV["DB_USER"] %>
    password: <%= ENV["DB_PASSWORD"] %>
    database: <%= ENV["DB_NAME"] %>
  cache:
    <<: *primary_production
    database: <%= ENV["DB_NAME"] %>_cache
    migrations_paths: db/cache_migrate
  queue:
    <<: *primary_production
    database: <%= ENV["DB_NAME"] %>_queue
    migrations_paths: db/queue_migrate
  cable:
    <<: *primary_production
    database: <%= ENV["DB_NAME"] %>_cable
    migrations_paths: db/cable_migrate
```

`db:prepare` creates them and loads their schemas on the first deploy.

#### On MariaDB, in the one database

On MariaDB the environment's user has one database and may not create another,
so the three live in it, as each gem's README describes for the application's
own database:

1. `config/database.yml`: production is the primary alone, and the platform's
   `DATABASE_URL` says where it is:

   ```yaml
   production:
     primary:
       <<: *default
   ```

2. `config/cache.yml`: take `database: cache` out of `production`.
3. `config/cable.yml`: take `connects_to:`, with its `database:` and
   `writing: cable`, out of `production`.
4. `config/environments/production.rb`: take out
   `config.solid_queue.connects_to = { database: { writing: :queue } }`.
5. Their tables, by a migration of your own: `bin/rails generate migration
   CreateSolidTables`, then copy into its `change` what is inside the
   `define … do … end` of `db/cache_schema.rb`, `db/queue_schema.rb` and
   `db/cable_schema.rb` — the `create_table` and `add_foreign_key` lines — and
   delete the three files.

`db:prepare` migrates them into the one database on the first deploy, and the
cache, `bin/jobs` and Action Cable all use it from then on. Or move the cache
and Action Cable to the stack's Valkey, and keep only Solid Queue in the
database.

## Migrations

`bin/rails db:prepare` runs as a deploy step, once the release is live and
before the health check asks. On an empty database it loads the schema and the
seeds; on one with tables it migrates — one command for both, and it runs on
the first release too. `on_failure: rollback` puts the previous release back if
a migration fails.

## The secret key base

`SECRET_KEY_BASE` is set for every Ruby environment, derived from the
environment's entropy and never changed, and Rails reads it over the one in
`config/credentials.yml.enc`. A new application needs nothing more; set
`RAILS_MASTER_KEY` as a secret variable only when your credentials hold
something else the application reads.

Moving a site whose sessions and signed links you want to keep? Set its key as
a secret variable named `SECRET_KEY_BASE`, which replaces the platform's.

## Hosts and HTTPS

The proxy terminates TLS and sets `X-Forwarded-Proto`, so `config.assume_ssl`
and `config.force_ssl`, on in a new application's `production.rb`, work as they
are. To hold Rails to the environment's own hostnames:

```ruby
config.hosts = ENV.fetch("VALLIC_HOSTNAMES", "").split(",")
```

The health check is sent with the primary hostname, so it passes.

## Uploads

Active Storage's `local` service keeps files under `storage/`, linked to a
directory that outlives every release and is in every backup, and Rails serves
them through its own controllers. For a site whose traffic is its uploads,
attach a bucket to the environment and use Active Storage's S3 service, with
the `aws-sdk-s3` gem, from the `S3_FILES_…` variables — see
[Storage](storage.md):

```yaml
# config/storage.yml
bucket:
  service: S3
  endpoint: <%= ENV["S3_FILES_ENDPOINT"] %>
  region: <%= ENV["S3_FILES_REGION"] %>
  bucket: <%= ENV["S3_FILES_BUCKET"] %>
  access_key_id: <%= ENV["S3_FILES_ACCESS_KEY"] %>
  secret_access_key: <%= ENV["S3_FILES_SECRET_KEY"] %>
  force_path_style: <%= ENV["S3_FILES_PATH_STYLE"] == "1" %>
```

`log/` and `tmp/` are linked out of the release the same way, so the file
cache store — the cache where `config.cache_store` names none — and anything a
gem writes there keep working in a release that is read-only. What is in
`tmp/` is left out of backups, as Rails means it to be temporary; to leave out
more, such as exports under `storage/`, see
[Files left out](backup-storage.md#files-left-out).

## Workers, jobs and cron

Solid Queue, Rails 8's default, runs as a worker — `bin/jobs`, in the
application's own image and release, supervised, with no port — and its
recurring tasks in `config/recurring.yml` run inside it. On one machine it can
run inside Puma instead: set `SOLID_QUEUE_IN_PUMA` to `1` as a variable and
declare no worker.

Sidekiq is a worker too, `bundle exec sidekiq`, on the stack's Valkey. Give it a
database of its own, apart from any cache: the platform clears the cache after
a restore, and clearing a Redis cache empties its whole database, queue and
all.

```ruby
# config/initializers/sidekiq.rb
redis = { url: "redis://#{ENV.fetch("REDIS_HOST", "localhost")}:6379/1" }
Sidekiq.configure_server { |config| config.redis = redis }
Sidekiq.configure_client { |config| config.redis = redis }
```

Rails has no scheduler of its own, and nothing is scheduled by default. Declare
what you need:

```yaml
cron:
  - name: unattached-uploads
    schedule: '0 4 * * *'
    command: "bin/rails runner 'ActiveStorage::Blob.unattached.where(created_at: ..2.days.ago).find_each(&:purge)'"
```

After a restore, the platform runs `Rails.cache.clear`.

## The CDN

Rails names its session cookie after the application — `_shop_session` for one
called Shop — so the platform cannot know it: name it on the CDN tab, and pages
are kept from the CDN for a visitor carrying it. Rails marks its pages
`private` unless an action says otherwise, with `expires_in` or
`http_cache_forever`, and serves the fingerprinted assets to be cached for a
year.
