November 7, 2024·6 min read·engineeringrubyrailsdeploymentdatabases

Rails 8 makes database-backed services part of deployment

Kamal 2 and the Solid adapters provide a smaller starting deployment. Their implementation still separates job execution, database coordination and HTTP traffic handling.

A small web application can accumulate several independently operated services before it has a substantial workload. Jobs need execution, live updates need coordination between processes, and caching needs storage. Rails 8 supplies database-backed defaults for these facilities and connects them to an application deployment path using Kamal 2.

The November 7 release announcement presents this as a way to deploy without requiring a platform-as-a-service provider. The concrete changes are more useful than the slogan. New applications receive deployment configuration and adapters that can use SQLite, while larger installations can choose PostgreSQL or MySQL for the database-backed job system.

I question choosing Ruby automatically for new systems, but its readable application model remains attractive. Reducing the operational prerequisites of that model is a coherent direction. The useful distinction is between removing a separate service dependency and removing the underlying work, which still needs somewhere to execute and store state.

A queue becomes database state plus execution processes

Solid Queue supplies an Active Job backend with database storage. The version 1.0 documentation separates workers, dispatchers and a scheduler. Workers claim ready executions and perform them. Dispatchers move scheduled work into the ready table when it becomes due, while the scheduler creates jobs for recurring tasks.

That distinction explains what database-backed means. A database stores and coordinates jobs, but it does not execute Ruby application methods itself. Processes still need to poll for work and run the handlers. The supervisor manages those processes and their lifecycle.

The system can run alongside Puma using its plugin or through bin/jobs. The release announcement identifies the plugin as the single-server default and describes dedicated dispatchers as another option. These are different arrangements for executing the same application work, allowing the first deployment to be small without fixing every future worker to the web process's placement.

The documentation recommends a separate queue database, while also supporting a single-database setup. For SQLite, the example uses separate files for primary application data and queue data. Using the same database engine therefore does not necessarily mean placing every operational table in the application's primary database.

Row locking coordinates competing workers

For higher-throughput installations, Solid Queue uses FOR UPDATE SKIP LOCKED when the database supports it. The documentation identifies PostgreSQL 9.5 and newer and MySQL 8 and newer. This allows workers polling for work to avoid waiting behind rows another worker has already locked.

That is a concrete database coordination mechanism, with conditions on the selected backend. SQLite remains supported for smaller applications, but the README says its writes are sequential and the skip-locked setting has no effect there. The common adapter interface does not make every backend's concurrency behavior identical.

Queue selection and job priority are separate controls too. A worker configured with an ordered queue list exhausts available work in an earlier queue before taking work from the next. Within a queue, smaller numeric priority values mean higher priority. A higher numeric importance assigned within a later queue does not override that queue ordering.

The documented worker configuration also separates thread count from process count. Threads bound the worker's pool of concurrent job handlers, while multiple processes provide another execution arrangement. Those settings remain meaningful even when all of the job records reside in one database.

This matters when deciding what deployment simplicity buys. The framework supplies a working starting configuration, while the queue still has scheduling semantics. An operator can separate workers or dispatchers as requirements grow without assuming that a database automatically infers the application's desired ordering.

Failure handling remains an application contract

Solid Queue relies on Active Job for retry and discard behavior. The versioned documentation says it does not provide an automatic retry policy of its own. Failed executions remain recorded until retried or discarded, with the surrounding application choosing how exceptions should lead to another attempt.

That is an important qualification to the release's broad list of job-system capabilities. A database-backed queue can retain failure information without deciding which business operations are safe to repeat. The application's Active Job policy still expresses that decision.

The documentation also describes process heartbeats and supervisor handling of expired processes. These mechanisms account for workers that stop without completing normal cleanup. They are operational responsibilities of the queue implementation even when no separate Redis service exists.

Concurrency limits introduce another explicit policy. A key derived from job arguments groups work that must not overlap beyond the configured limit. The documentation explains semaphore-based coordination and an expiry duration used when a process fails to release its claim. A limit on simultaneous execution is distinct from a guarantee about job order.

Sharing a database changes transaction relationships

Keeping jobs in the same transactional database as application data can couple their commits. The Solid Queue documentation calls this useful but warns against making application behavior accidentally depend on a configuration that may later change. Moving the queue to another database changes that relationship even if the queue adapter remains Solid Queue.

This distinction is easy to miss when the headline emphasizes fewer dependencies. One engine, one server and one transaction are different deployment properties. An application can use the same SQL product for both stores while retaining separate transaction boundaries.

I prefer the explicit separate-database recommendation because it makes that boundary visible. A job system can be straightforward to operate without requiring every application action to assume its enqueue shares the primary database transaction. The architecture remains understandable when work later moves to dedicated processes or another database host.

Cable and cache use the database for different work

Solid Cable stores messages used to relay WebSocket updates across application processes. The announcement describes polling and a default one-day message retention period. This replaces the need for Redis as that coordination service while keeping cross-process delivery work in the deployment.

The transport to an already connected browser remains a WebSocket concern. Database-backed coordination supplies the information that different server processes need to forward. Storing messages and maintaining client connections are separate responsibilities, even though the framework presents a single live-update feature.

Solid Cache addresses stored cache entries, particularly HTML fragments. The announcement emphasizes disk capacity and longer retention compared with a RAM-resident cache service. It also describes encryption and explicit retention limits. The practical policy concerns how much cached material the deployment retains and for how long.

These adapters therefore share an infrastructure choice without sharing identical access patterns. Queue records coordinate execution, Cable records distribute messages, and cached fragments avoid repeated rendering. The database still receives the corresponding writes and reads, which is where the removed accessory services' responsibilities now reside.

Kamal and Thruster operate at different HTTP layers

Kamal 2 deploys the application's container images to Linux machines reached through SSH. The announcement describes a generated Dockerfile and an external container registry account as parts of that workflow. A deployable image is an artifact the tool can install, rather than a requirement to rebuild a server manually for each release.

Kamal Proxy handles routing during deployment, automated certificates through Let's Encrypt and multiple applications on a server. Thruster sits in front of Puma inside the application setup, providing X-Sendfile handling and asset caching and compression. Their different placement explains why the release includes two proxies without assigning them the same job.

The release does not mean an existing application must immediately replace all of its infrastructure. It supplies a preconfigured starting path. The relevant adoption work depends on which generated files and adapters the application uses, and whether its existing deployment has requirements outside those defaults.

The generated application includes more of its own behavior

Propshaft becomes the default asset pipeline, providing asset lookup and digest stamping for long-lived caching. The announcement says Sprockets remains supported for existing applications. The new default narrows the asset pipeline's ordinary responsibilities while leaving more elaborate JavaScript builds to separate tools where needed.

Rails 8 also adds an authentication generator. bin/rails generate authentication produces Session and User models, a sessions controller and authentication concern, alongside password-reset mail support. The announcement explicitly leaves user signup to the application.

That boundary is useful because a generated starting point is code the application can inspect and maintain. It does not imply that every account-lifecycle requirement is implemented. Rails 8's wider deployment direction follows the same pattern, supplying concrete defaults for a small application while retaining visible places to choose its execution processes, persistence boundaries and application-specific behavior.