· Ismaël Mejía (@iemejia)

MongoDB on PostgreSQL: DocumentDB with pglayers-azure

DocumentDB is a MongoDB-compatible document database built on PostgreSQL. It adds the BSON data type and a full CRUD API to Postgres, and ships a gateway that speaks the MongoDB wire protocol -- so existing MongoDB clients (mongosh, pymongo, the Node.js driver) can connect to a PostgreSQL server as if it were MongoDB. It's the same engine behind Azure DocumentDB.

DocumentDB is included in the pglayers-azure profile image, which mirrors the open-source extensions available in Azure Database for PostgreSQL. This post walks through running the image and talking to it from a MongoDB client end to end -- including a password gotcha that trips people up.

1. Start the container

Run the pglayers-azure image, exposing PostgreSQL on 5432 and the DocumentDB gateway on 10260 (the port the wire protocol listens on):

docker run -d --name pglayers-docdb \
  -e POSTGRES_PASSWORD=secret \
  -p 5432:5432 -p 10260:10260 \
  ghcr.io/pglayers/pglayers-azure:18

The profile image auto-configures everything at boot: it sets shared_preload_libraries (including pg_documentdb_gw_host, the gateway worker), appends the required GUCs, and auto-creates the documentdb extension on first init. Within a second or two the log shows TCP listener(s) bound to port 10260 and the gateway is ready. You don't need to run CREATE EXTENSION yourself.

Use PG 18 (isolated layout) or PG 17. DocumentDB is built only for 17 and 18 -- not 19 -- so don't use pglayers-azure:19 for this.

2. Create a MongoDB user

The gateway uses native SCRAM authentication, and its configuration blocks a set of role name prefixes (documentdb, citus, pg, internal_role). That means you can't reuse the postgres superuser -- you need a fresh role with a password.

The intuitive approach is DocumentDB's own documentdb_api.create_user() function, but on a stock image it fails:

ERROR:  password type is not a plain text
CONTEXT:  ... CREATE ROLE mongoadmin WITH LOGIN PASSWORD 'SCRAM-SHA-256$...'

The reason: the server runs with password_encryption = scram-sha-256, so create_user() pre-hashes the password, but DocumentDB installs a check_password hook that wants the plaintext value (it builds the SCRAM verifier itself). The two disagree and the call is rejected.

The path that works is a direct CREATE ROLE with a plaintext password, granting DocumentDB's built-in roles. The hook then computes the verifier the gateway expects:

docker exec pglayers-docdb psql -U postgres -c \
  "CREATE ROLE mongoadmin WITH LOGIN INHERIT PASSWORD 'Secret_123' \
   IN ROLE documentdb_admin_role, documentdb_readonly_role;"

Upstream's standalone documentdb-local image sidesteps this by taking --username/--password at container start and bootstrapping the first user itself. In pglayers the gateway runs as a PostgreSQL background worker, so you create the user via SQL as above.

3. Connect from a MongoDB client

The gateway serves TLS with an auto-generated self-signed certificate, so clients connect with TLS enabled and certificate validation relaxed. The connection string is:

mongodb://mongoadmin:Secret_123@localhost:10260/?tls=true&tlsAllowInvalidCertificates=true&authMechanism=SCRAM-SHA-256

Here's a full round-trip with mongosh (run from a throwaway container on the host network) -- insert, filtered find, and an aggregation:

docker run --rm --network host mongodb/mongodb-community-server:latest mongosh --quiet \
  "mongodb://mongoadmin:Secret_123@localhost:10260/?tls=true&tlsAllowInvalidCertificates=true&authMechanism=SCRAM-SHA-256" \
  --eval 'const db = db.getSiblingDB("shop");
          db.products.insertMany([{name:"Widget",price:9.99},{name:"Gadget",price:19.5}]);
          printjson(db.products.find({price:{$gte:10}}).toArray());
          printjson(db.products.aggregate([{$group:{_id:null,avg:{$avg:"$price"}}}]).toArray());'

This inserts two documents, returns Gadget for the $gte filter, and computes an average price of 14.745. The exact same URI works from pymongo or the Node.js driver:

from pymongo import MongoClient

client = MongoClient(
    "mongodb://mongoadmin:Secret_123@localhost:10260/"
    "?tls=true&tlsAllowInvalidCertificates=true&authMechanism=SCRAM-SHA-256"
)
db = client["shop"]
db.products.insert_one({"name": "Sprocket", "price": 4.5})
print(db.products.find_one({"name": "Sprocket"}))

Because it's all PostgreSQL underneath, the same data is queryable from SQL -- for example via documentdb_api.count_query('shop', ...) -- so you can mix document workloads and relational queries against one database.

Production notes

Try it

Everything above is a single docker run plus one CREATE ROLE away. Pull the image and connect your favorite MongoDB client:

docker run -d -p 5432:5432 -p 10260:10260 \
  -e POSTGRES_PASSWORD=secret \
  ghcr.io/pglayers/pglayers-azure:18

See the pglayers repository for the full extension list and profile documentation, and the DocumentDB project for the supported MongoDB API surface.