Skip to content
Development

Setting Up a Rails 8 App in 2026

By Victor Da Luz
rails ruby tailwind viewcomponent hotwire sqlite solid-queue

I started a side project recently and decided to finally give Rails 8 a proper shot. I had been keeping up with the release notes but had not actually scaffolded a fresh app on the new defaults. This post covers what the setup looked like, the frontend choices I made, and the small things that burned time.

What Rails 8 includes by default

The defaults have shifted a lot from what I remembered. A fresh rails new in 2026 gives you:

  • Solid Queue for background jobs - database-backed, no Redis required
  • Solid Cache and Solid Cable on the same principle - SQLite handles it all
  • Propshaft as the asset pipeline, replacing Sprockets
  • Importmap for JavaScript - no Node, no webpack, no bundler
  • Hotwire (Turbo + Stimulus) for interactivity, wired in from the start

The most meaningful part of this list is the Solid* trio. Redis used to be an implicit dependency for any serious Rails app. Now a SQLite-backed single-server setup is a first-class option. For a homelab side project with one user, that is exactly what I wanted.

There is also a built-in authentication generator now:

bin/rails generate authentication

It scaffolds a User model, a Session model, login and password-reset views, and an Authentication concern you can include in ApplicationController. Not a full-featured auth library, but enough to protect a single-user app without pulling in Devise.

Setting up Ruby with asdf

I manage Ruby with asdf. One thing to know if you are on asdf 0.18+: the local subcommand is gone. What used to be asdf local ruby 3.3.6 is now:

asdf set ruby 3.3.6

It still writes a .tool-versions file in the current directory. Same result, different verb.

Also: when you first install a new Ruby version and run gem update --system, double-check which gem binary you are actually calling. On a Mac, the system Ruby lives at /usr/bin/ruby and has its own gem directory you cannot write to. If you see a FilePermissionError about /Library/Ruby/Gems/2.6.0, you are hitting the wrong one. Open a new terminal tab after asdf install so the shims pick up correctly, then retry.

Scaffolding into an existing repo

I ran rails new . inside an existing Git repository rather than creating a fresh directory. The --skip-git flag prevents Rails from reinitialising Git, which is what you want:

rails new . --database=sqlite3 --skip-git

This creates everything in place and leaves your existing .gitignore alone (though Rails does append /config/*.key to it, which is correct).

Adding Tailwind CSS v4

Tailwind CSS v4 dropped tailwind.config.js entirely. Configuration now lives in CSS:

bundle add tailwindcss-rails
bin/rails tailwindcss:install

The installer creates app/assets/tailwind/application.css with @import "tailwindcss" and builds output to app/assets/builds/tailwind.css. It also wraps your layout body in a <main class="container mx-auto ..."> element.

What the installer does not do is add a stylesheet link tag to your layout <head>. The built CSS sits in app/assets/builds/ and Propshaft will serve it, but the browser will never see it unless you link it explicitly. Add this next to your existing stylesheet_link_tag:

<%= stylesheet_link_tag "tailwind", "data-turbo-track": "reload" %>

I only noticed it was missing when I reloaded the page and nothing had changed visually. Fifteen minutes of head-scratching.

In development, Tailwind runs as a watcher process. Add it to your Procfile.dev if it is not already there:

web: bin/rails server
worker: bin/jobs start
css: bin/rails tailwindcss:watch

(bin/jobs is a binstub Rails 8 generates for Solid Queue - same as bin/rails solid_queue:start, just shorter.)

Then bin/dev starts all three.

ViewComponent for UI structure

ViewComponent adds a component model on top of ERB. Each component is a Ruby class paired with a template. It works well with Tailwind because each component owns its own classes rather than scattering them across partials.

bundle add view_component

No install task needed. You can start using it immediately and adopt it incrementally - start with plain ERB views, extract components when you notice yourself repeating the same markup.

Swapping Selenium for Cuprite

The Rails scaffold puts selenium-webdriver in the test group for system tests. Cuprite is a cleaner alternative. It talks to Chrome directly over the Chrome DevTools Protocol, no Java runtime involved:

# Gemfile
group :test do
  gem "capybara"
  gem "cuprite"  # replaces selenium-webdriver
end

Wire it up in test/application_system_test_case.rb:

require "test_helper"
require "capybara/cuprite"

class ApplicationSystemTestCase < ActionDispatch::SystemTestCase
  driven_by :cuprite, using: :chrome, screen_size: [1400, 1400]
end

You still need Chrome installed. On Ubuntu (for CI), add google-chrome-stable to the apt-get install step.

The Solid Queue gotcha in development

This one took a while. Rails 8 configures separate SQLite databases for Solid Queue, Solid Cache, and Solid Cable in production. In development, everything runs in a single database. The problem is that bin/rails db:migrate only loads db/schema.rb - it does not touch db/queue_schema.rb, db/cache_schema.rb, or db/cable_schema.rb.

Start the worker in development and you immediately get:

ActiveRecord::StatementInvalid: Could not find table 'solid_queue_processes'

The fix is to load those schemas explicitly. I wrote a small Rake task that hooks into db:test:prepare so the test database stays correct automatically:

# lib/tasks/solid_schemas.rake
namespace :db do
  task load_solid_schemas: :environment do
    load Rails.root.join("db/queue_schema.rb")
    load Rails.root.join("db/cache_schema.rb")
    load Rails.root.join("db/cable_schema.rb")
  end
end

Rake::Task["db:test:prepare"].enhance do
  Rake::Task["db:load_solid_schemas"].invoke
end

For first-time development setup, I also run the same three loads in bin/setup after db:prepare.

The missing root route

After running rails generate authentication, the login redirect works fine. But visiting http://localhost:3000 shows the default Rails welcome page instead of the login form.

That page is served by a built-in Rails controller that bypasses your ApplicationController entirely, so the authentication before_action never runs. You need an actual root route pointing at something you own:

# config/routes.rb
root "dashboard#index"

With a DashboardController that inherits from ApplicationController (which includes the Authentication concern), an unauthenticated request to / will redirect to the login page.

What I would change next time

The Solid Queue schema situation should probably live in a Rails generator or at least in the getting-started docs. I understand why it works the way it does in production, but a fresh development setup tripping over missing tables is friction that should not exist.

The Tailwind stylesheet omission feels like a bug in the installer. It writes the watcher process into Procfile.dev but does not link the output file in the layout. I would expect those two things to go together.

Outside of those two, the defaults are genuinely good. Solid Queue replacing Redis-backed ActiveJob is the kind of change that removes an entire category of ops overhead for small apps. Hotwire being present from the start means you are writing server-rendered HTML and adding interactivity where you actually need it, not because the framework pushed you toward it.

Rails is in a good place right now. Worth picking up again if you dropped it a few years ago.

Related reading

Infrastructure

Deploying Homebox, a self-hosted home inventory, in the homelab

I wanted one place to track what I own, what it cost, and where the warranty paperwork lives. Homebox fit, and a SQLite-backed deploy meant no extra database to babysit. Here is the Docker-in-LXC setup and the admin-account trick that is easy to miss.

Read

Ready to Transform Your Career?

Let's work together to unlock your potential and achieve your professional goals.