Docs
Skip to content

MySQL

Rails_

Use Ruby on Rails and ActiveRecord with an Appwrite native MySQL database. Configure database.yml, run migrations against the direct database port, and size the ActiveRecord pool.

4 min read

Raw

A native MySQL database is a standard MySQL engine, so Ruby on Rails works against it through ActiveRecord with no Appwrite-specific configuration. Point config/database.yml at the connection details from the Connections page and use ActiveRecord, migrations, and the rest of the Rails toolchain exactly as you would against any MySQL server.

Install the database driver

ActiveRecord talks to MySQL through the mysql2 driver gem. Add it to your Gemfile:

Ruby
# Gemfile
gem "mysql2", "~> 0.5"

The mysql2 gem builds against MySQL client libraries, so the MySQL or MariaDB client headers must be available at install time.

Then install:

Bash
bundle install

Set the connection string

Fetch the connection string with the API. Keep it in an environment variable and do not commit it:

.env
DATABASE_URL="mysql://admin:<password>@db-<hash>.<region>.appwrite.center:3306/<database>"
DB_SSL_MODE="required"

Rails maps the mysql:// URL scheme to the mysql2 adapter by default. Set ssl_mode in database.yml so mysql2 requires TLS on Appwrite Cloud. For certificate verification (verify_identity) or mTLS, see the Network page.

Configure database.yml

Rails reads DATABASE_URL automatically. The simplest configuration points the url at the environment variable and lets ActiveRecord parse the host, port, database, and credentials out of it:

YAML
# config/database.yml
production:
adapter: mysql2
url: <%= ENV["DATABASE_URL"] %>
ssl_mode: <%= ENV.fetch("DB_SSL_MODE", "required") %>
max_connections: <%= ENV.fetch("RAILS_MAX_THREADS", 5) %>

If you'd rather set the fields explicitly, the discrete keys map one-to-one to the values from the Connections response. Use ERB to read each value from the environment so no secret lands in source control:

YAML
# config/database.yml
production:
adapter: mysql2
encoding: utf8mb4
host: <%= ENV["DB_HOST"] %> # db-<hash>.<region>.appwrite.center
port: <%= ENV.fetch("DB_PORT", 3306) %>
database: <%= ENV["DB_NAME"] %> # <database>
username: <%= ENV.fetch("DB_USER", "admin") %>
password: <%= ENV["DB_PASSWORD"] %>
ssl_mode: <%= ENV.fetch("DB_SSL_MODE", "required") %>
max_connections: <%= ENV.fetch("RAILS_MAX_THREADS", 5) %>

When both DATABASE_URL and explicit keys are present, Rails merges them. ssl_mode and max_connections can still be set in database.yml, so a url-based config can keep secrets in the environment and tune the connection pool in YAML.

Size the connection pool

ActiveRecord manages a per-process connection pool. In current Rails apps, max_connections: caps how many backend connections a single Rails process holds. Older apps may use the pool: name for the same setting. The value defaults to 5 and must be large enough for every thread that checks out a connection, your Puma worker threads plus any background job threads in the same process.

YAML
production:
adapter: mysql2
url: <%= ENV["DATABASE_URL"] %>
ssl_mode: <%= ENV.fetch("DB_SSL_MODE", "required") %>
max_connections: <%= ENV.fetch("RAILS_MAX_THREADS", 5) %>

Tying max_connections to RAILS_MAX_THREADS keeps it aligned with Puma's thread count. Each Puma worker is a separate process with its own pool, so the backend connection count is roughly max_connections × workers × server instances. Keep that product within your specification's maxConnections, see specifications.

Run migrations

Generate and apply migrations the usual way:

Bash
bin/rails db:migrate

Migrations issue DDL and need a session-level connection, so run them against the direct MySQL port 3306, not the transaction-mode pooler. Point DATABASE_URL (or a separate migration URL) at the direct port when you run db:migrate. The primary admin user owns the database and can run schema changes.

Use ActiveRecord

Once database.yml is configured, models work with no further setup. Define a migration and model, then query through ActiveRecord:

Ruby
# db/migrate/20240101000000_create_rails_users.rb
class CreateRailsUsers < ActiveRecord::Migration[8.1]
def change
create_table :rails_users do |t|
t.string :email, null: false
t.timestamps
end
add_index :rails_users, :email, unique: true
end
end

Define the matching model:

Ruby
# app/models/rails_user.rb
class RailsUser < ApplicationRecord
validates :email, presence: true, uniqueness: true
end

Create and query records as usual:

Ruby
RailsUser.create!(email: 'ada@example.com')
recent = RailsUser.order(created_at: :desc).limit(10)

ActiveRecord opens connections lazily and reuses them from the pool, so a long-running Puma server keeps a small, stable set of backend connections rather than opening one per request.

Pooling for a long-running server

A Rails app under Puma is a long-running process: it holds an ActiveRecord pool for its lifetime. That pairs naturally with the direct MySQL port 3306, sized so max_connections × workers stays within your connection budget. This is the recommended setup for a persistent server.

If you instead route through the connection pooler to absorb spikes or many app instances, prefer session mode, which keeps a backend connection for the whole client session and behaves like a direct connection to ActiveRecord. The pooler defaults to transaction mode, which hands out a different backend connection per transaction. If your Rails configuration enables server-side prepared statements, disable them for transaction-mode pooling or use session mode:

YAML
production:
adapter: mysql2
url: <%= ENV["DATABASE_URL"] %> # pooler host, port 6033
ssl_mode: <%= ENV.fetch("DB_SSL_MODE", "required") %>
max_connections: <%= ENV.fetch("RAILS_MAX_THREADS", 5) %>
prepared_statements: false

See the pooler page for the mode trade-offs. Whichever runtime connection you choose, always run bin/rails db:migrate against the direct MySQL port.

Use a branch for previews and CI

Branches are instant, isolated copies of a database with their own hostname and connection string. They're ideal for running migrations against throwaway data in a pull-request preview or an integration-test job:

  1. Create a branch from the API and read its connectionString.
  2. Export it as DATABASE_URL for the job.
  3. Run bin/rails db:migrate and your test suite against the branch.
  4. Delete the branch when the job finishes.

A branch has no pooler and exposes the MySQL port directly, which gives migrations the session-level connection they need. Because a branch starts from a storage snapshot, the schema and data match the source database at branch time, so migrations run against realistic data without touching production.

For ActiveRecord and migration details beyond this guide, see the Rails configuration guide.

Was this page helpful?

Share what worked or what we should fix. Once approved, our agents automatically apply suggested updates to the docs.