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:19for 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-localimage sidesteps this by taking--username/--passwordat 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
- TLS.
tlsAllowInvalidCertificates=trueis only appropriate for the bundled self-signed certificate. For production, mount your own certificate and a custom/etc/documentdb/gateway_config.jsonto set the port, TLS mode, and blocked role prefixes. - PostgreSQL version. Stick to PG 17 or 18; DocumentDB isn't built for 19 yet.
- Auto-created extensions. The gateway needs the
documentdbextension to exist, which the profile image creates for you on first init. To opt out, setPGLAYERS_CREATE_EXTENSIONS=none.
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.