todos resource.
Prerequisites
- Elixir 1.15 or later and Erlang/OTP. This guide was tested with Elixir 1.20.4, Erlang/OTP 29, Phoenix 1.8.15,
ecto_sql3.14.0, and Postgrex 0.22.4. - A ClickHouse Cloud account
Create a ClickHouse Managed Postgres service
In the ClickHouse Cloud console, click New service and select Postgres. The instance is ready in a few minutes. See the quickstart for a walkthrough.Get your connection details
Open your service and click Connect in the left sidebar. The modal shows your username, password, server, and port, and it has a Directly / via PgBouncer toggle. Keep Directly selected and turn on Use SSL. Copy the password and the 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 Phoenix app is a long-running process, and Ecto keeps its own pool of connections (pool_size, ten by default). This guide connects directly to Postgres on port 5432. If many app instances together approach the Postgres connection limit, see Using PgBouncer.
Create a Phoenix app
Install the Phoenix project generator and create an app. Postgres is the default database:Configure the database connection
Move the CA certificate you downloaded into the app’spriv directory and rename it to ca-certificate.pem. The downloaded file is named after your service, for example your-service-ca-certificate.pem:
priv are also included in releases.
In config/dev.exs, replace the username, password, hostname, and database lines of the Hello.Repo configuration with url and ssl:
config/dev.exs
ssl is a keyword list, Postgrex turns on certificate verification (verify: :verify_peer) and hostname checking, and it sends the hostname from the URL as the TLS server name. You only need to pass the CA certificate. ssl: true doesn’t work, because it checks the server certificate against your operating system’s CA store, which doesn’t contain your service’s CA.
Set DATABASE_URL to your service, using a new database name. This guide uses guide_phoenix:
mix ecto.create connects to the default postgres database with the same TLS settings to run CREATE DATABASE.
Generate a JSON resource and run the migration
Generate a context, schema, migration, and JSON controller for todos:lib/hello_web/router.ex, uncomment the /api scope and add the resource to it:
lib/hello_web/router.ex
schema_migrations table and only runs new ones on later calls.
Run and verify
Check that the connection is encrypted. The query readspg_stat_ssl for the current connection:
3116:
PATCH request returns the updated todo:
http://localhost:3116/api/todos in your browser to list the remaining todos:
guide_phoenix and then public, and click the todos table. The public schema also contains schema_migrations:
Configure production
In production, Phoenix reads the database settings fromconfig/runtime.exs. In the Hello.Repo configuration inside if config_env() == :prod do, replace the # ssl: true, line with the CA certificate path:
config/runtime.exs
Application.app_dir resolves the path inside your build or release, so the setting works no matter which directory the app starts in. Set DATABASE_URL in your deployment environment, again without a query string.
If your app started on SQLite, config/runtime.exs reads DATABASE_PATH instead. Replace the database_path variable and its config :hello, Hello.Repo call with:
config/runtime.exs
--database sqlite3 also run pending migrations when a release starts, through the Ecto.Migrator child in lib/<your_app>/application.ex. That keeps working with Postgres.
Using PgBouncer
If your app instances together open too many direct connections, connect through the bundled PgBouncer instead. Select via PgBouncer in the Connect modal, use port6432 in DATABASE_URL, and add prepare: :unnamed to the repo configuration, both in config/dev.exs and in config/runtime.exs:
config/dev.exs
-
Prepared statements: by default, Ecto runs every query as a named prepared statement, and the bundled PgBouncer supports named statements. Postgrex frees statements with a protocol-level
Closemessage, never with SQLDEALLOCATE. Still, with the defaultprepare: :named, two problems show up through PgBouncer:- A query whose SQL text is about 4 KB long can hang until it times out. The caller gets
DBConnection.ConnectionErrorafter about a minute. - After a migration changes a column’s type, queries that read the column keep failing with
cached plan must not change result type, even after you restart the app, because PgBouncer reuses its prepared statement on the server.
prepare: :unnamed, Postgrex prepares each query again on every run, and both problems go away. Each query takes one extra network round trip. - A query whose SQL text is about 4 KB long can hang until it times out. The caller gets
-
Migrations:
mix ecto.migrateworks through PgBouncer with Ecto’s default migration lock, which locks theschema_migrationstable inside a transaction. Don’t setmigration_lock: :pg_advisory_lockfor a PgBouncer connection. The session-level advisory lock can be released on a different Postgres connection, which fails withfailed to release advisory lockand leaves the lock held, so later runs, including runs on port5432, wait for it. To run migrations over the direct connection, set a5432DATABASE_URLfor themix ecto.migratecommand. -
Session state:
SETstatements, such as aSET search_pathin anafter_connectcallback, only apply to the current transaction’s Postgres connection. Asearch_pathin the Postgrexparametersoption fails withunsupported startup parameter: search_path.LISTENandNOTIFY, whichPostgrex.Notificationsuses, need a direct connection.
Troubleshooting
CLIENT ALERT: Fatal - Unknown CA:cacertfiledoesn’t point to your service’s CA certificate, or you setssl: true. Download the certificate again from Settings → CA Certificate. Each service has its own CA.CLIENT ALERT: Fatal - Bad Certificatewithhostname_check_failed: the host inDATABASE_URLdoesn’t match the server certificate, for example because you used an IP address. Use the hostname from the Connect modal.Invalid CA certificate file ... no such file or directory: thecacertfilepath is wrong. Check thatpriv/ca-certificate.pemexists.
Next steps
- Connection: connection strings, PgBouncer, and TLS
- Settings: Postgres and PgBouncer parameters, such as
max_connections - Read replicas: send reads to a replica with Ecto replica repositories
- Sync to ClickHouse: replicate your Phoenix tables to ClickHouse for analytics