Setting Up a Rails 8 App in 2026
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
How I Redesigned this site with Astro and Tailwind CSS
The story of migrating from a bloated WordPress site on DreamHost to a lightning-fast static site built with Astro and Tailwind CSS, hosted for free on Cloudflare Pages.
Why Trello Extractor Was Built and How It Works
Learn how Trello Extractor transforms JSON exports into organized, portable archives that preserve your digital work history and ensure long-term data accessibility.
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.
Ready to Transform Your Career?
Let's work together to unlock your potential and achieve your professional goals.