Skip to main content

VillageSQL is a drop-in replacement for MySQL with extensions.

All examples in this guide work on VillageSQL. Install Now →
Running rails new with no flags gives you SQLite in development, not MySQL. Pointing a Rails 8 application at MySQL takes one explicit flag at scaffold time, a database.yml that names a real user and host, and a gem that needs a native MySQL client library to compile. This guide walks through all three against a live MySQL-compatible server, using VillageSQL as the database everything below actually ran against.

Confirm the Toolchain Before Scaffolding

Rails 8 requires Ruby 3.2 or later. Check what is on the machine before running rails new:
On a fresh macOS install with only the system Ruby, this can return something like:
Ruby 2.6 cannot run Rails 8. Install a current Ruby with Homebrew and put it ahead of the system Ruby on PATH:
This produced ruby 4.0.6 (2026-07-14 revision 03b6d3f889) +PRISM [arm64-darwin25], well above the Rails 8 floor. Install Rails itself with a version constraint rather than trusting whatever gem install rails resolves to:
This installed Rails 8.1.3.1. Rails 8.1 satisfies the ~> 8.0 constraint because that operator means “at least 8.0, less than 9.0.”

Scaffold with the MySQL Adapter

Rails 8 does not default to MySQL or PostgreSQL. Running rails new --help shows the actual default:
mysql is still the correct value in Rails 8, it selects the mysql2 gem and adapter. trilogy is a separate, pure-Ruby MySQL-protocol adapter Rails also supports, and is a different choice from the one this guide makes. Scaffold the app with the mysql value:
This writes mysql2 into the Gemfile and generates a config/database.yml preconfigured with adapter: mysql2. --skip-git is optional and only relevant if the app directory is not going to be its own git repository. The mysql2 gem has a native extension and needs a MySQL client library with headers to compile against. Without one, bundle install fails at the mysql2 step. On macOS with Homebrew:
mysql-client is keg-only on Homebrew (it is not symlinked into /opt/homebrew because it conflicts with the full mysql formula), which is exactly why bundle config needs to be told where to find it explicitly rather than relying on PATH. With that set, bundle install compiled and installed mysql2 0.5.7 alongside the rest of the Rails 8.1.3.1 dependency set.

Create a Dedicated Database and User

Do not point a new application at the root account. Create a database and a scoped user on the running server:
Point config/database.yml at this user over TCP rather than the Unix socket, since rails_app was granted access from 127.0.0.1, not from localhost:

What rails db:create Actually Needs

rails db:create does not create only the database named under the current RAILS_ENV. With rails_app granted privileges on rails_guide.* alone, running it produced this:
rails_guide was created successfully: GRANT ALL PRIVILEGES ON rails_guide.* is enough to create a database that does not exist yet, because the grant is scoped to the database name, not to an existing object. But rails db:create also tries to create the test environment’s database in the same run, and rails_guide_test was never granted. The task creates both development and test databases by default, only production is excluded. Granting the second database name resolves it:
A scoped, non-privileged user is sufficient for rails db:create as long as it is granted on every database name Rails will try to create, not just the one named in RAILS_ENV. No superuser or global CREATE privilege is required.

Migrate and Verify Against the Server Directly

Generate a Post model and migrate:
The generated migration:
Confirm the table exists by querying the server directly, not by trusting Rails’ own migration output:

Wire Up a Route, a Controller, and Test Against the Server

Add a resourceful route for index and create:
A minimal JSON controller:
Start the server and exercise both actions with curl:
Do not stop at the HTTP response. Confirm the write landed in the actual table:
The row is present with the same content ActiveRecord reported, and the timestamp confirms created_at round-tripped through mysql2 correctly.

Connection Pool Size and Puma’s Thread Count Are the Same Number

Rails 8.1’s generated database.yml no longer uses the pool key you may know from older Rails versions. It uses max_connections:
A pool: key still works and raises no warning: max_connections falls back to it when max_connections is absent, as a plain lookup with no deprecation path. What Rails 8.1 did deprecate is the Ruby method HashConfig#pool, now an alias for HashConfig#max_connections, which is a different thing from the database.yml key. Setting both keys to different values is not silently resolved. It raises at boot:
This matters because the same environment variable also sizes Puma’s thread pool. config/puma.rb reads it directly, with a comment that says so explicitly:
The two defaults are not the same number: database.yml falls back to 5 connections if RAILS_MAX_THREADS is unset, while puma.rb falls back to 3 threads. Left at their defaults, the pool has more connections than Puma has threads to use them, which is safe but slightly wasteful. Set RAILS_MAX_THREADS explicitly once you know your target thread count, and both settings move together. A pool smaller than the thread count is the dangerous direction: threads will queue waiting for a free connection, and under load that shows up as request latency with no error in the log to explain it.

Frequently Asked Questions

Does --database=mysql install the same gem in every Rails 8 version?

Within Rails 8, yes. It resolves to the mysql2 gem and the mysql2 adapter, confirmed against rails new --help on Rails 8.1.3.1. trilogy is a separate, valid value for the same flag: it is a pure-Ruby MySQL wire-protocol client with no native extension to compile, which removes the mysql-client header dependency at the cost of using a different gem. Choose one at scaffold time; they are not interchangeable adapters you can swap later without touching database.yml and the Gemfile.

Why does rails db:create need a database-specific grant instead of just CREATE privilege?

A MySQL GRANT ... ON dbname.* TO user works even when dbname does not exist yet, and it authorizes creating that specific database. It does not authorize creating a database with a different name. Since rails db:create creates both the development and test databases from config/database.yml in one run, a scoped user needs a grant naming each one, not a single grant on the environment you expect to use day to day.

Do I need to run migrations against VillageSQL any differently than against stock MySQL?

No. Every command in this guide, rails db:create, rails db:migrate, rails generate model, ran unmodified against a VillageSQL server. VillageSQL is a drop-in replacement for MySQL, so mysql2 and ActiveRecord’s MySQL adapter see the same wire protocol and the same SHOW/DESCRIBE output as they would against MySQL itself.

Troubleshooting

See also