> ## Documentation Index
> Fetch the complete documentation index at: https://clickhouse.com/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# Use Ruby on Rails with ClickHouse Managed Postgres

> Connect a Ruby on Rails application to ClickHouse Managed Postgres with verified TLS, run Active Record migrations, and serve a scaffold

export const BetaBadge = ({link, galaxyTrack, galaxyEvent}) => {
  if (link) {
    return <a href={link} target="_blank" rel="noopener noreferrer" className="betaBadge" onClick={galaxyTrack && galaxyEvent ? galaxyOnClick(galaxyEvent) : undefined}>
                <span>Beta</span>
            </a>;
  }
  return <a href="https://clickhouse.com/docs/reference/settings/beta-and-experimental-features#beta-features" className="betaBadge">
            <span>Beta feature</span>
        </a>;
};

<BetaBadge link="https://clickhouse.com/cloud/postgres" galaxyTrack={true} galaxyEvent="docs.managed-postgres.guides-ruby-on-rails-beta" />

[Ruby on Rails](https://rubyonrails.org/) is a full-stack web framework whose ORM, Active Record, supports PostgreSQL through the `pg` gem.
In this guide, you create a new Rails app, connect it to ClickHouse Managed Postgres over verified TLS, run a migration, and serve a scaffolded `Post` resource.

<h2 id="prerequisites">
  Prerequisites
</h2>

* Ruby 3.2 or later. This guide was tested with Ruby 4.0.7, Rails 8.1.4, and `pg` 1.6.3.
* A ClickHouse Cloud account.

The `pg` gem ships precompiled builds for Linux, macOS, and Windows that include `libpq`, so you don't need a local PostgreSQL installation.

<h2 id="create-service">
  Create a ClickHouse Managed Postgres service
</h2>

In the ClickHouse Cloud console, click **New service** and select **Postgres**. The instance is ready in a few minutes. See the [quickstart](/docs/products/managed-postgres/quickstart) for a walkthrough.

<h2 id="connection-details">
  Get your connection details
</h2>

Open your service and click **Connect** in the left sidebar. Keep **Directly** selected and turn on **Use SSL**.
Copy the password and server hostname, then click **Download CA certificate**.

The CA certificate is unique to your service. You can also download it later from **Settings → CA Certificate**.

**Direct or PgBouncer?** A Rails server is a long-lived process that keeps its own connection pool (`max_connections` in `config/database.yml`, five per process by default), so this guide connects directly to Postgres on port `5432`.
If you run many processes and hit the connection limit, see [Using PgBouncer](#pgbouncer).

<h2 id="create-app">
  Create a Rails app
</h2>

Install Rails and create an app that uses PostgreSQL:

```bash theme={null}
gem install rails
rails new blog --database=postgresql
cd blog
```

<Tip>
  **Adding to an existing app?**

  Skip `gem install rails` and `rails new`. In your app's directory, run `bundle add pg`, then continue with [Configure the database connection](#configure-connection), where the new `development` section replaces the SQLite one.
</Tip>

<h2 id="configure-connection">
  Configure the database connection
</h2>

Move the CA certificate you downloaded into the app's `config` directory and rename it to `ca-certificate.pem`.
The downloaded file is named after your service, for example `your-service-ca-certificate.pem`:

```bash theme={null}
mv ~/Downloads/your-service-ca-certificate.pem config/ca-certificate.pem
```

The CA certificate isn't a secret, so you can commit it with your app.

In `config/database.yml`, replace the `development` section with:

```yaml theme={null}
development:
  <<: *default
  url: <%= ENV["DATABASE_URL"] %>
  sslmode: verify-full
  sslrootcert: <%= Rails.root.join("config/ca-certificate.pem") %>
```

`sslmode: verify-full` makes `libpq` check that the server certificate is signed by your service's CA and matches the hostname. `Rails.root.join` gives an absolute path, so the setting works no matter which directory you start Rails from.

Set `DATABASE_URL` to your service, using a new database name. This guide uses `guide_rails`:

```bash theme={null}
export DATABASE_URL="postgresql://postgres:<PASSWORD>@your-instance.pg.clickhouse.cloud:5432/guide_rails"
```

<Note>
  Leave the query string off `DATABASE_URL`. Parameters in the URL override the keys in `database.yml`, so a copied `sslrootcert=your-service-ca-certificate.pem` would replace your setting with a path relative to the current directory.
</Note>

Create the database:

```bash theme={null}
bin/rails db:create
```

```text theme={null}
Created database 'guide_rails'
```

`db:create` only creates the database named in `DATABASE_URL`. It connects to the default `postgres` database to run `CREATE DATABASE`, but doesn't change it. When `DATABASE_URL` is set, Rails also skips creating the local test database.

<h2 id="migrations">
  Generate a model and run the migration
</h2>

Generate a scaffold for a `Post` resource and apply its migration. In an existing app, `bin/rails db:migrate` also creates your app's existing tables in the new database. They start empty, because data in SQLite isn't copied:

```bash theme={null}
bin/rails generate scaffold Post title:string body:text
bin/rails db:migrate
```

```text theme={null}
== 20260930165011 CreatePosts: migrating ======================================
-- create_table(:posts)
   -> 0.0973s
== 20260930165011 CreatePosts: migrated (0.0973s) =============================
```

<h2 id="query">
  Query the database
</h2>

Create `script/posts_demo.rb` with a few Active Record calls:

```ruby theme={null}
post = Post.create!(title: "Hello from Rails", body: "Stored in ClickHouse Managed Postgres.")
Post.create!(title: "Second post", body: "Created with Active Record.")
post.update!(body: "Updated with Active Record.")
Post.create!(title: "Temporary post", body: "Deleted right away.").destroy

Post.order(:id).each { |p| puts "#{p.id}: #{p.title} - #{p.body}" }

tls = ActiveRecord::Base.connection.select_one(
  "SELECT version, cipher FROM pg_stat_ssl WHERE pid = pg_backend_pid()"
)
puts "Connected with #{tls["version"]} (#{tls["cipher"]})"
```

Run it with `bin/rails runner`:

```bash theme={null}
bin/rails runner script/posts_demo.rb
```

```text theme={null}
1: Hello from Rails - Updated with Active Record.
2: Second post - Created with Active Record.
Connected with TLSv1.3 (TLS_AES_256_GCM_SHA384)
```

The last line reads `pg_stat_ssl` for the current connection, which confirms that the session is encrypted.

<h2 id="verify">
  Run the app
</h2>

Start the development server:

```bash theme={null}
bin/rails server
```

Open [http://localhost:3000/posts](http://localhost:3000/posts) to list, create, edit, and delete posts. Every request reads from and writes to ClickHouse Managed Postgres.

The page lists each post with its title and body and a **Show this post** link, followed by a **New post** link. Edit and delete links are on each post's page.

To see the table in the console, open **SQL console** in the left sidebar, expand the `guide_rails` database, and select the `posts` table.
Rails also creates `schema_migrations` and `ar_internal_metadata` to track migrations.

The `posts` table contains the two rows from `script/posts_demo.rb`:

| id | title | body | created\_at | updated\_at |
| - | - | - | - | - |
| 1 | Hello from Rails | Updated with Active Record. | 2026-09-30 16:50:15.862005 | 2026-09-30 16:50:16.150928 |
| 2 | Second post | Created with Active Record. | 2026-09-30 16:50:16.010488 | 2026-09-30 16:50:16.010488 |

<h2 id="pgbouncer">
  Using PgBouncer
</h2>

Each Rails process opens up to `max_connections` direct connections. If many processes together approach the Postgres connection limit, connect through the bundled [PgBouncer](/docs/products/managed-postgres/connection#pgbouncer) instead: select **via PgBouncer** in the **Connect** modal and use port `6432` in `DATABASE_URL`.

PgBouncer runs in transaction pooling mode:

* **Prepared statements** work with Rails defaults if the `pg` gem is 1.6 or later and uses `libpq` 17 or later. The precompiled `pg` 1.6 gems bundle `libpq` 18. With these versions, when Rails evicts a statement from its cache, it releases it with a protocol-level `Close` message, which the bundled PgBouncer handles. Older `pg` or `libpq` versions send SQL `DEALLOCATE` instead, which fails through PgBouncer with `prepared statement "a1" does not exist`. If that happens inside a transaction, the transaction is aborted (`PG::InFailedSqlTransaction`). To check your versions, run:

  ```bash theme={null}
  bin/rails runner 'puts PG::VERSION, PG.library_version'
  ```

  You need `1.6.0` or later and `170000` or later. If you can't upgrade, add `prepared_statements: false` to the `development` section. Rails turns prepared statements on in `production`, but in `development` they're off by default because `query_log_tags_enabled` is on.
* **Migrations** take a session-level advisory lock (`pg_try_advisory_lock`) to prevent concurrent runs. In transaction pooling, the unlock can reach a different Postgres connection, which fails with `Failed to release advisory lock` and leaves the lock held, so later runs fail with `Cannot run migrations because another migration process is currently running`. Either run `bin/rails db:migrate` with a direct `DATABASE_URL` on port `5432`, or turn off the lock:

```yaml theme={null}
development:
  <<: *default
  url: <%= ENV["DATABASE_URL"] %>
  sslmode: verify-full
  sslrootcert: <%= Rails.root.join("config/ca-certificate.pem") %>
  advisory_locks: false
```

With `advisory_locks: false`, make sure only one deploy runs migrations at a time.

<h2 id="troubleshooting">
  Troubleshooting
</h2>

* **`SSL error: certificate verify failed`**: `sslrootcert` doesn't point to your service's CA certificate. Download it again from **Settings → CA Certificate**. Each service has its own CA, and public CA bundles don't work.
* **`root certificate file "..." does not exist`**: the path in `sslrootcert` is wrong. Check that `config/ca-certificate.pem` exists.

<h2 id="next-steps">
  Next steps
</h2>

* Use the same `sslmode` and `sslrootcert` settings in your `production` configuration.
* [Connection](/docs/products/managed-postgres/connection): connection strings, PgBouncer, and TLS.
* [Settings](/docs/products/managed-postgres/settings): change Postgres and PgBouncer parameters such as `max_connections`.
* [Read replicas](/docs/products/managed-postgres/read-replicas): scale reads, for example with Rails [multiple databases](https://guides.rubyonrails.org/active_record_multiple_databases.html).
* [Sync to ClickHouse](/docs/products/managed-postgres/sync-to-clickhouse/clickpipes): replicate your Rails tables to ClickHouse for analytics.
