---
title: Install & configure NRDOT for PostgreSQL monitoring with Helm
source: https://docs.newrelic.com/docs/opentelemetry/db360/postgresql/helm
---

You can install and configure PostgreSQL monitoring on Kubernetes using the `postgresql-otel` Helm chart. Optionally, the chart can also run a setup job that creates the monitoring role, its grants, and the `pg_stat_statements` extension for you.

> #### ⚠️ IMPORTANT
>
> The chart deploys the collector as a Kubernetes Deployment that reaches PostgreSQL remotely over the network, so it reports PostgreSQL metrics only: no host or infrastructure metrics for the machine PostgreSQL runs on. It monitors one PostgreSQL _instance_ per release (multiple _databases_ on that instance are supported), and it doesn't configure TLS or `db_auth` credential providers such as AWS IAM authentication.

## Server-level prerequisites [#helm-server-prereqs]

PostgreSQL **14 or newer** is required. Before installing this chart (or enabling the setup job), the following server parameters must be in place. `pg_stat_statements` can't be created without `shared_preload_libraries`, and that parameter requires a **restart**, so nothing running inside your Kubernetes cluster can do this for you.

| Parameter                   | Value                |
| --------------------------- | -------------------- |
| `shared_preload_libraries`  | `pg_stat_statements` |
| `pg_stat_statements.track`  | `ALL`                |
| `pg_stat_statements.max`    | `10000`              |
| `pg_stat_statements.save`   | `on`                 |
| `track_activity_query_size` | `4096`               |
| `track_functions`           | `all`                |

-   **Self-hosted**: set these in `postgresql.conf`, then restart PostgreSQL. Some distributions need the `postgresql-contrib` package installed for `pg_stat_statements` to be available.
-   **AWS RDS or Aurora**: set these in the DB **parameter group**, not a config file. `track_activity_query_size` and `pg_stat_statements.max` are static parameters; apply them with `apply_method: pending-reboot`, then reboot the instance. Applying them immediately fails with `InvalidParameterCombination`. For Aurora, the parameter-group family name must match your engine's major version exactly, for example `aurora-postgresql16`.

## Prerequisites [#helm-prerequisites]

-   Valid New Relic [license key](https://docs.newrelic.com/docs/apis/intro-apis/new-relic-api-keys/#ingest-license-key).
-   Helm 3.0 or later, and `kubectl` configured to access your Kubernetes cluster.
-   Network connectivity from your Kubernetes cluster to the PostgreSQL host (or RDS endpoint) and port:
    -   Self-hosted: a routed path to the PostgreSQL host (VPN, peering, or shared network), DNS resolution if it's a hostname, and the PostgreSQL-side `pg_hba.conf` and firewall must allow the connection's actual source IP (which may be a NAT gateway, not the pod IP itself).
    -   AWS RDS: VPC peering, a transit gateway, or shared-VPC placement with the RDS instance, and the RDS security group must allow the PostgreSQL port (`5432` by default) from the cluster's egress source.
-   Either a monitoring role you've already created with its per-database grants, or PostgreSQL admin credentials so the chart's setup job can create them for you. See [Configure monitoring credentials](#helm-credentials) below.

## Create the namespace [#helm-namespace]

Create the namespace you'll install into. It must exist before you create any secrets in the next step, since `helm install --create-namespace` only creates the namespace during the final install step:

```bash
kubectl create namespace newrelic
```

> #### 💡 TIP
>
> Set it as the default namespace for your current `kubectl` context so you don't need to pass `-n` on every command below:
>
> ```bash
> kubectl config set-context --current --namespace=newrelic
> ```

## Configure monitoring credentials [#helm-credentials]

Decide whether you want the chart's setup job to create the PostgreSQL monitoring role for you, and if not, how you'll supply its credentials:

**No setup job: plain username and password**

With this method there's no setup job, so nothing here creates the monitoring role for you. Before installing the chart, create it yourself as a superuser. Run this once at the cluster level:

```sql
CREATE USER newrelic WITH LOGIN PASSWORD '<password>';
ALTER ROLE newrelic INHERIT;   -- required on PG15+, a no-op on PG14
```

Then run this in **every** database you plan to list in `postgresql.databases`:

```sql
CREATE SCHEMA IF NOT EXISTS otel;
GRANT USAGE ON SCHEMA otel TO newrelic;
GRANT USAGE ON SCHEMA public TO newrelic;
GRANT SELECT ON ALL TABLES IN SCHEMA public TO newrelic;
GRANT pg_monitor TO newrelic;
CREATE EXTENSION IF NOT EXISTS pg_stat_statements;
```

For more detail, refer to the **Configure database user and grants** step for [self-hosted PostgreSQL](https://docs.newrelic.com/docs/opentelemetry/db360/postgresql/hosted/#database-user) or [PostgreSQL on RDS](https://docs.newrelic.com/docs/opentelemetry/db360/postgresql/rds/#database-user).

You'll set the monitoring username and password directly in `values.yaml` in the next step.

**No setup job: secret-based credentials**

With this method there's also no setup job. Create the monitoring role and its per-database grants yourself first, exactly as in the plain-credentials method above. The only difference is where its credentials live: instead of plaintext in `values.yaml`, store them in a Kubernetes secret:

```bash
kubectl create secret generic postgres-monitor-creds \
  --from-literal=username=<MONITORING_USERNAME> \
  --from-literal=password='<MONITORING_PASSWORD>' \
  -n newrelic
```

**Automated setup job**

With this method, the chart's setup job creates the monitoring role for you and, for every database in `postgresql.databases`, creates the `otel` schema, grants `USAGE`/`SELECT`/`pg_monitor`, and creates the `pg_stat_statements` extension. Both the monitoring and admin credentials must be provided as Kubernetes secrets: the setup job never accepts plaintext admin credentials, and enabling it requires secret-based monitoring credentials too.

1.  Create a secret with the monitoring username and password. This is the identity the setup job creates and the collector connects with:

    ```bash
    kubectl create secret generic postgres-monitor-creds \
      --from-literal=username=<MONITORING_USERNAME> \
      --from-literal=password='<MONITORING_PASSWORD>' \
      -n newrelic
    ```

2.  The setup job only accepts admin credentials via a Kubernetes secret (no plaintext username/password in `values.yaml`). This credential must be a superuser (self-hosted) or an `rds_superuser` equivalent (RDS), since it needs `CREATE USER`, `CREATE SCHEMA`, and `CREATE EXTENSION`:

    ```bash
    kubectl create secret generic postgres-admin-creds \
      --from-literal=username=<ADMIN_USERNAME> \
      --from-literal=password='<ADMIN_PASSWORD>' \
      -n newrelic
    ```

## Configure the Helm values [#helm-configure]

Set `postgresql.topology` to match your environment:

| Value         | Description                                       |
| ------------- | ------------------------------------------------- |
| `self-hosted` | Self-hosted PostgreSQL, reached over the network. |
| `rds`         | PostgreSQL or Aurora on AWS RDS.                  |

`postgresql.databases` is a **required, non-empty list**. Unlike MySQL or SQL Server, PostgreSQL has no monitor-every-database mode: a connection targets one database at a time, so the receiver has to be told which ones to scrape.

Use the `values.yaml` that matches the credential method you chose in the previous step:

**No setup job: plain username and password**

```yaml
licenseKey: <YOUR_LICENSE_KEY>
otlpEndpoint: otlp.nr-data.net:4317

postgresql:
  topology: <self-hosted|rds>
  server: <YOUR_DB_HOST>
  port: <YOUR_DB_PORT>
  username: <YOUR_MONITORING_USERNAME>
  password: <YOUR_MONITORING_PASSWORD>
  databases:
    - <YOUR_DATABASE>
    - <ANOTHER_DATABASE>

# No setup job runs, so create the monitoring role and its
# per-database grants yourself before installing the chart.
setupJob:
  enabled: false
```

**No setup job: secret-based credentials**

```yaml
licenseKey: <YOUR_LICENSE_KEY>
otlpEndpoint: otlp.nr-data.net:4317

postgresql:
  topology: <self-hosted|rds>
  server: <YOUR_DB_HOST>
  port: <YOUR_DB_PORT>
  existingSecret: postgres-monitor-creds
  databases:
    - <YOUR_DATABASE>
    - <ANOTHER_DATABASE>

# No setup job runs, so the monitoring role referenced by
# this secret must already exist - create it yourself
# before installing the chart.
setupJob:
  enabled: false
```

**Automated setup job**

```yaml
licenseKey: <YOUR_LICENSE_KEY>
otlpEndpoint: otlp.nr-data.net:4317

postgresql:
  topology: <self-hosted|rds>
  server: <YOUR_DB_HOST>
  port: <YOUR_DB_PORT>
  existingSecret: postgres-monitor-creds
  databases:
    - <YOUR_DATABASE>
    - <ANOTHER_DATABASE>

setupJob:
  enabled: true
  image:
    # -- psql-capable image. No default: the official postgres
    # image bundles psql and is a reasonable choice.
    repository: postgres
    tag: "16"
    pullPolicy: IfNotPresent
  postgresAdmin:
    existingSecret: postgres-admin-creds
  # -- Also creates the otel.explain_statement SECURITY DEFINER
  # function, so the receiver can EXPLAIN write/locking queries.
  enableExplainPermissions: false
  # -- Also creates the pgvector extension in each database.
  enablePgvector: false
```

| Parameter                                          | Description                                                                                                                                                                                                                                                                                                                               |
| -------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `licenseKey`                                       | Your New Relic license key.                                                                                                                                                                                                                                                                                                               |
| `otlpEndpoint`                                     | Your region's OTLP/gRPC endpoint, as a bare `host:port` with **no scheme**: `otlp.nr-data.net:4317` (US) or `otlp.eu01.nr-data.net:4317` (EU). For more information, refer to [New Relic OTLP endpoints documentation](https://docs.newrelic.com/docs/opentelemetry/best-practices/opentelemetry-otlp/).                                  |
| `postgresql.server` / `postgresql.port`            | Your PostgreSQL host (or RDS endpoint) and port, reachable from the cluster.                                                                                                                                                                                                                                                              |
| `postgresql.databases`                             | **Required, non-empty list** of databases to monitor.                                                                                                                                                                                                                                                                                     |
| `postgresql.excludeDatabases`                      | Databases excluded from cluster-wide top-query and query-sample scans. Defaults to `[rdsadmin]`, and is only rendered when `topology` is `rds`; on RDS the monitoring user can never reach `rdsadmin`, so excluding it avoids permission errors.                                                                                          |
| `setupJob.image.repository` / `setupJob.image.tag` | A `psql`-capable image used by the setup job. The chart ships **no default**, so you must supply one when the setup job is enabled. The official `postgres` image bundles `psql` and is a reasonable choice.                                                                                                                              |
| `setupJob.enableExplainPermissions`                | Optional. Also creates the `otel.explain_statement` `SECURITY DEFINER` function and grants `EXECUTE` on it, letting the receiver run `EXPLAIN` against locking and write queries without holding write grants. Leaving it `false` is safe: the receiver falls back to inline `EXPLAIN`, so those statements simply don't get query plans. |
| `setupJob.enablePgvector`                          | Optional. Also creates the `vector` extension in each monitored database, for vector metrics. The `l1`, `hamming`, and `jaccard` distance functions need pgvector 0.7.0 or later.                                                                                                                                                         |

> #### 💡 TIP
>
> You usually don't need to set `postgresql.topQueryCollection.explainFunctionName`. The receiver's own default is already `otel.explain_statement` (the exact function `setupJob.enableExplainPermissions` creates), and the chart omits the key when it's empty, so the two line up with no extra configuration. Set it only to point the receiver at a differently-named function.

> #### 💡 TIP
>
> See the chart's [values.yaml](https://github.com/newrelic/helm-charts/blob/master/charts/postgresql-otel/values.yaml) for all available configuration options, including `postgresql.collectionInterval`, the `metrics` toggles, the `topQueryCollection`/`querySampleCollection` blocks, and the `additionalReceiverConfig` escape hatch.

## (Optional) Monitor multiple instances from one release [#helm-multi]

Instead of `postgresql.*`, set `postgresqlMulti.enabled: true` to monitor several PostgreSQL instances (self-hosted or RDS) from a single collector pod in one release. The two modes are mutually exclusive: don't set `postgresql.server` and `postgresqlMulti.enabled: true` in the same release.

Entries are called `instances`, not `databases`: each instance is a separate PostgreSQL server, and each one carries its own nested `databases` list (the individual databases to monitor on that instance), the same way `postgresql.databases` works in single-instance mode.

Multi-instance mode has no plain-username/password path: every instance needs its own Kubernetes secret with monitoring credentials. Decide whether you also want the setup job to create each monitoring role for you:

**No setup job**

Create each instance's monitoring-role secret yourself before installing (repeat per instance, matching the `name` you give it in `values.yaml`):

```bash
kubectl create secret generic db1-monitor-creds \
  --from-literal=username=<MONITORING_USERNAME> \
  --from-literal=password='<MONITORING_PASSWORD>' \
  -n newrelic
# Repeat for every instance (db2-monitor-creds, ...)
```

```yaml
licenseKey: <YOUR_LICENSE_KEY>
otlpEndpoint: otlp.nr-data.net:4317

# EDIT the two example entries below (or add more) to match
# your real instances - each `name` must be unique within
# this release and match the Secret you created for it.
postgresqlMulti:
  enabled: true
  topology: <self-hosted|rds>
  instances:
    - name: db1
      server: <YOUR_DB_1_HOST>
      port: 5432
      existingSecret: db1-monitor-creds
      databases:
        - appdb1
    - name: db2
      server: <YOUR_DB_2_HOST>
      port: 5432
      existingSecret: db2-monitor-creds
      databases:
        - appdb2

# No admin credential is configured here since the setup
# job is disabled - create each instance's monitoring role
# yourself first.
setupJob:
  enabled: false
```

**Automated setup job**

Each instance needs both its own monitoring-role secret and its own admin-credentials secret (repeat per instance):

```bash
kubectl create secret generic db1-monitor-creds \
  --from-literal=username=<MONITORING_USERNAME> \
  --from-literal=password='<MONITORING_PASSWORD>' \
  -n newrelic
kubectl create secret generic db1-admin-creds \
  --from-literal=username=<ADMIN_USERNAME> \
  --from-literal=password='<ADMIN_PASSWORD>' \
  -n newrelic
# Repeat for every instance (db2-monitor-creds, db2-admin-creds, ...)
```

```yaml
licenseKey: <YOUR_LICENSE_KEY>
otlpEndpoint: otlp.nr-data.net:4317

# EDIT the two example entries below (or add more) to match
# your real instances - each `name` must be unique within
# this release and match the Secrets you created for it.
postgresqlMulti:
  enabled: true
  topology: <self-hosted|rds>
  instances:
    - name: db1
      server: <YOUR_DB_1_HOST>
      port: 5432
      existingSecret: db1-monitor-creds
      databases:
        - appdb1
      postgresAdmin:
        existingSecret: db1-admin-creds
    - name: db2
      server: <YOUR_DB_2_HOST>
      port: 5432
      existingSecret: db2-monitor-creds
      databases:
        - appdb2
      postgresAdmin:
        existingSecret: db2-admin-creds

setupJob:
  enabled: true
  image:
    # -- psql-capable image. No default: the official postgres
    # image bundles psql and is a reasonable choice.
    repository: postgres
    tag: "16"
```

| Parameter                   | Description                                                                                                                                                                                                                                                                                        |
| --------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `postgresqlMulti.enabled`   | Set to `true` to monitor multiple instances from one release instead of `postgresql.*`. Defaults to `false`.                                                                                                                                                                                       |
| `postgresqlMulti.topology`  | `self-hosted` or `rds`, same meaning as `postgresql.topology`, shared by every instance in this release.                                                                                                                                                                                           |
| `postgresqlMulti.instances` | List of instances to monitor. Each entry requires a unique `name`, `server` (`port` defaults to `5432`), `existingSecret`, and a **required, non-empty** `databases` list of database names on that instance; add `postgresAdmin.existingSecret` per entry only when `setupJob.enabled` is `true`. |

> #### 💡 TIP
>
> `setupJob.enableExplainPermissions` and `setupJob.enablePgvector` are single toggles shared across every database in every instance; they can't be set per-instance. Scrape settings (collection interval, metrics, `topQueryCollection`/`querySampleCollection`) likewise apply identically to every instance in `postgresqlMulti.instances`; there's no per-instance override. Use `additionalReceiverConfig` for anything that needs to differ. The setup job runs once per instance (`<release>-setup-<name>`) rather than once per release.

## Install the Helm chart [#helm-install]

1.  Add the New Relic Helm repository:

    ```bash
    helm repo add newrelic https://helm-charts.newrelic.com
    helm repo update
    ```

2.  Install the chart using your `values.yaml` file:

    ```bash
    helm upgrade --install postgresql-otel newrelic/postgresql-otel \
      -n newrelic \
      --create-namespace \
      -f values.yaml
    ```

## Verify the installation [#helm-verify]

1.  Check that the setup job completed (if enabled) and the collector pod is running:

    ```bash
    kubectl get jobs,pods -n newrelic --watch
    ```

2.  Run this query in the [query builder](https://docs.newrelic.com/docs/query-your-data/explore-query-data/query-builder/introduction-query-builder/) to confirm data is arriving:

    ```sql
    SELECT count(*) FROM Metric
    WHERE metricName LIKE 'postgresql.%'
    AND instrumentation.provider = 'opentelemetry'
    SINCE 10 minutes ago
    ```

## Find and use your data [#find]

Once your data is being collected, you can access comprehensive PostgreSQL database monitoring through the New Relic UI.

To find your PostgreSQL database entity in New Relic:

1.  Go to **[one.newrelic.com](https://one.newrelic.com) > All capabilities > Databases**.
2.  From the **Entity type** dropdown, select **PostgreSQL instance**, then click **Apply**.
3.  Select your PostgreSQL database from the list of entities.

## Related documentation [#related]

[Introduction to PostgreSQL monitoring with NRDOT](https://docs.newrelic.com/docs/opentelemetry/db360/postgresql/introduction)

Learn about all the available installation methods for PostgreSQL monitoring with New Relic.

[CLI install](https://docs.newrelic.com/docs/opentelemetry/db360/postgresql/cli)

Learn how to install and configure PostgreSQL monitoring with a single New Relic CLI command.

[Ansible install](https://docs.newrelic.com/docs/opentelemetry/db360/postgresql/ansible)

Learn how to install and configure PostgreSQL monitoring at scale with the newrelic.newrelic_install Ansible role.

[Chef install](https://docs.newrelic.com/docs/opentelemetry/db360/postgresql/chef)

Learn how to install and configure PostgreSQL monitoring at scale with the newrelic-install Chef cookbook.
