PostgreSQL 15+ Permissions: Why Your Helm Deployment Cannot Create Tables#

Starting with PostgreSQL 15, only the database owner and superusers can create objects in the public schema by default. This breaks a common Helm pattern where you create a user, grant privileges, and expect it to create tables. The application connects fine but fails on its first CREATE TABLE.

The Symptom#

Your application pod logs show something like:

Error: permission denied for schema public

Or from an ORM like Mattermost’s:

Failed to create table: pq: permission denied for schema public

The confusing part is that the connection itself works. The user can SELECT from existing tables. It only fails on CREATE TABLE, CREATE INDEX, or other DDL operations.

Why GRANT ALL Is Not Enough#

Before PostgreSQL 15, this was sufficient:

CREATE USER mmuser WITH PASSWORD 'secret';
CREATE DATABASE mattermost OWNER postgres;
GRANT ALL PRIVILEGES ON DATABASE mattermost TO mmuser;

In PostgreSQL 15+, GRANT ALL PRIVILEGES ON DATABASE grants CONNECT, CREATE, and TEMPORARY on the database itself, but the public schema inside that database has a separate permission layer. By default, only the schema owner (usually postgres) can create objects in it.

The Fix#

You need to change the owner of both the database and the public schema to the application user:

-- Create the user and database
CREATE USER mmuser WITH PASSWORD 'secret';
CREATE DATABASE mattermost;

-- Transfer ownership (this is the critical part)
ALTER DATABASE mattermost OWNER TO mmuser;

-- Connect to the new database and fix schema ownership
-- (must be done while connected to the mattermost database)
ALTER SCHEMA public OWNER TO mmuser;

The ALTER SCHEMA public OWNER TO mmuser is the line that most guides miss. Without it, mmuser can connect to the database but cannot create tables in the default schema.

Helm Init Script: The Right Way#

With the Bitnami PostgreSQL chart, you configure init scripts via initdb.scripts. There are two important constraints:

  1. These scripts run only on first boot. A semaphore file prevents re-execution. If you change the script, you must delete the PVC.
  2. The \c psql metacommand is unreliable in heredocs passed through kubectl exec. Use separate psql commands instead.

Here is a working values.yaml configuration:

auth:
  postgresPassword: "adminpass"
  database: mattermost
  username: mmuser
  password: "secret"

initdbScripts:
  init-permissions.sh: |
    #!/bin/bash
    set -e

    # The Bitnami chart already creates the database and user from the auth.* values.
    # We just need to fix ownership for PostgreSQL 15+ compatibility.

    PGPASSWORD="$POSTGRES_PASSWORD" psql -U postgres -d mattermost -c \
      "ALTER SCHEMA public OWNER TO mmuser;"

    PGPASSWORD="$POSTGRES_PASSWORD" psql -U postgres -d mattermost -c \
      "ALTER DATABASE mattermost OWNER TO mmuser;"

    echo "Permissions configured for PostgreSQL 15+"

Note the use of a shell script (.sh extension) rather than a raw SQL file. This gives you access to environment variables and lets you run multiple psql commands with explicit -d database targeting.

Why Not Use a .sql File?#

A .sql file tempts you to use \c mattermost to switch databases. But \c is a psql client metacommand, not SQL. When the Bitnami chart pipes SQL files into psql, \c sometimes fails silently, especially through shell heredocs or kubectl exec chains. Using separate psql -d <database> calls is reliable every time.

Debugging Permission Issues#

1. Verify the connection works – if this fails, the problem is authentication, not permissions:

kubectl exec -it dt-postgresql-0 -n dream-team -- \
  psql -U mmuser -d mattermost -c "SELECT 1;"

2. Check schema ownership – if Owner shows postgres instead of your app user, that is the problem:

kubectl exec -it dt-postgresql-0 -n dream-team -- \
  psql -U postgres -d mattermost -c "\dn+"
#  Name  |  Owner   | Access privileges |      Description
# public | postgres | postgres=UC/...   | standard public schema

3. Fix it live:

kubectl exec -it dt-postgresql-0 -n dream-team -- \
  psql -U postgres -d mattermost -c "ALTER SCHEMA public OWNER TO mmuser;"
kubectl exec -it dt-postgresql-0 -n dream-team -- \
  psql -U postgres -d mattermost -c "ALTER DATABASE mattermost OWNER TO mmuser;"

4. Force init script re-run (destroys data – dev only):

kubectl delete pvc data-dt-postgresql-0 -n dream-team
kubectl delete pod dt-postgresql-0 -n dream-team

Summary#

PostgreSQL Version GRANT ALL ON DATABASE Needs ALTER SCHEMA public OWNER TO
14 and earlier Sufficient for DDL No
15+ Only grants CONNECT/CREATE/TEMP Yes

The one-line fix is ALTER SCHEMA public OWNER TO <your_user>. Put it in a .sh init script with explicit psql -d <database> calls, and your Helm-deployed PostgreSQL 15+ will work correctly on first boot.