ERROR: permission denied for schema public on a CREATE TABLE means your role may connect and read, but not create objects in public. Since PostgreSQL 15 only the database owner can, so grant CREATE ON SCHEMA public to the role or make it the database owner.

The quickest fix is one statement run as the owner or a superuser: GRANT CREATE ON SCHEMA public TO maria. For an application database, making the application role the owner of the whole database is usually cleaner.

The error

docker exec fix4-pg18 psql -U maria -d appdb -v VERBOSITY=verbose -c "CREATE TABLE t(id int)"
ERROR:  42501: permission denied for schema public
LINE 1: CREATE TABLE t(id int)
                     ^
LOCATION:  aclcheck_error, aclchk.c:2793

Why it happens

PostgreSQL 15 changed the default privileges of the public schema. Before it, every role (the pseudo-role PUBLIC) could create tables there. From 15 on the schema belongs to pg_database_owner, a role that always means "whoever owns this database", and everyone else only gets USAGE. This is what a fresh PostgreSQL 18 database shows:

docker exec fix4-pg18 psql -U postgres -d postgres -c "\dn+ public"
  Name  |       Owner       |           Access privileges            |      Description
--------+-------------------+----------------------------------------+------------------------
 public | pg_database_owner | pg_database_owner=UC/pg_database_owner+| standard public schema
        |                   | =U/pg_database_owner                   |

UC is usage and create for the owner; =U is usage only for everyone else. So a role that is neither the owner nor a superuser can select from tables it was granted, but its first migration fails with code 42501, "insufficient privilege". Tutorials written for PostgreSQL 14 and older skip this step because it was not needed then, which is why the error shows up after an upgrade or a new container.

The fix

docker exec fix4-pg18 psql -U postgres -d appdb -c "GRANT CREATE ON SCHEMA public TO maria"
GRANT

docker exec fix4-pg18 psql -U maria -d appdb -v VERBOSITY=verbose -c "CREATE TABLE t(id int)"
CREATE TABLE

The grant has to run inside the database that owns the schema (-d appdb), because each database has its own public. fix4-pg18, appdb and maria are the test's own names. The alternative is ownership: on a second database on the same server, ALTER DATABASE appdb2 OWNER TO david let the role david create a table with no grant at all, because the owner of the database is pg_database_owner for its public schema. That suits an application role that runs its own EF Core migrations; the grant suits a second role that only needs to add tables next to someone else's.

How it was reproduced

A fresh postgres:18 container (PostgreSQL 18.4, Debian build) on Docker Desktop 4.91.0 under Windows 11. The superuser created the database appdb, so postgres owned it, and a login role maria with no other privileges. Connected as maria, CREATE TABLE failed as shown; after the grant the same command succeeded, and \dt listed the table as owned by maria.

Frequently asked

Why do I get permission denied for schema public in PostgreSQL 15?
PostgreSQL 15 removed the CREATE privilege on the public schema from PUBLIC. Only the database owner and superusers can create objects there unless you grant CREATE ON SCHEMA public to a role.
How do I grant CREATE on schema public?
Connect to the database in question as its owner or a superuser and run GRANT CREATE ON SCHEMA public TO rolename. Each database has its own public schema, so the grant applies to that database only.
Should my application role own the database?
For a database that belongs to one application and is migrated by it, yes: ALTER DATABASE name OWNER TO approle gives it create rights on public without extra grants. Shared databases are better served by explicit grants.

More decoded errors in the Fixes category. For how databases, schemas and roles fit together if you come from SQL Server, see databases, schemas and roles in PostgreSQL.