FerretDB is an open-source proxy that runs MongoDB workloads on PostgreSQL, eliminating dedicated document database bills without changing client drivers.
Production teams running microservices against MongoDB Atlas frequently watch cloud spend escalate as cluster tiers, IOPS, and backup retention scale up. FerretDB redirects those application connections to standard PostgreSQL clusters by translating MongoDB wire protocol requests in real time. This proxy design lets engineering teams retain their established document schemas and official client drivers while retiring dedicated document database clusters.
MongoDB Atlas charges recurring fees for dedicated instances, RAM-heavy tiers, and replication. Every staging environment provisioned on Atlas adds another monthly invoice and separate operational tooling. FerretDB eliminates this split by turning PostgreSQL into a document database backend. Instead of funding isolated Atlas clusters, teams run the proxy against existing PostgreSQL databases, unifying backups, recovery, and failover under familiar tooling like CloudNativePG or Patroni.
The trade-off balances feature completeness against operational overhead. FerretDB matches standard MongoDB 5.0+ query semantics, BSON encoding, indexes, aggregations, and Atlas Data API endpoints, requiring only an updated connection string. However, teams exchange managed cloud convenience for self-hosted maintenance. MongoDB Atlas provides automated global sharding, full-text search, and managed multi-document transactions. FerretDB delegates durability to PostgreSQL via the DocumentDB extension, but lacks wire-level transactions and bulk write commands. For standard CRUD workloads, avoiding Atlas cluster bills easily justifies operating the proxy.
commitTransaction and abortTransaction, remain unimplemented, preventing use in workloads requiring multi-collection ACID boundaries.pg_cron tasks every two seconds to check index builds, creating persistent disk writes on idle systems (tracked in GitHub issue #5422).bulkWrite, capped collections (convertToCapped), and custom role management are unsupported, requiring batch operations to execute as discrete single queries.Is FerretDB free for commercial use? Yes. The software is published under the Apache-2.0 license with no runtime fees, seat pricing, or commercial self-hosting restrictions.
FerretDB vs MongoDB Atlas — which should teams choose? Choose MongoDB Atlas when applications require managed multi-region clustering, built-in vector search, or vendor SLAs. Choose FerretDB when you operate PostgreSQL infrastructure and want to eliminate external document database hosting bills for standard CRUD workloads.
Can FerretDB run on managed cloud PostgreSQL services? Only on platforms supporting custom extensions, because FerretDB requires the DocumentDB extension inside PostgreSQL. Standard services like Amazon RDS do not support this extension, so teams deploy on virtual machines, Docker, or Kubernetes.
FerretDB is a practical choice for teams seeking to trim infrastructure costs by running MongoDB workloads on existing PostgreSQL clusters, provided their applications do not require multi-document transactions. Active development with commits through June 2026 and an Apache-2.0 license give engineering organizations the freedom to self-host or embed the proxy without facing proprietary software restrictions.
We started this exact setup from scratch in a clean Linux machine on Nov 2, 2026 and checked it through FerretDB's own API: every check passed. Recipes are re-checked regularly and removed if they stop working.
mkdir -p ferretdb && cd ferretdb
curl -fsSL https://open-source.store/tools/ferretdb/compose.yml -o compose.ymlcat > .env <<EOF
POSTGRES_PASSWORD=$(openssl rand -hex 12)
POSTGRES_USER=ferretdb
POSTGRES_DB=postgres
EOFPOSTGRES_PASSWORD — Password for the PostgreSQL database user. Generated above; keep it secret.POSTGRES_USER — Username for the PostgreSQL database user.POSTGRES_DB — PostgreSQL database name with DocumentDB extension pre-installed.docker compose up -dservices:
postgres:
image: ghcr.io/ferretdb/postgres-documentdb:17-0.107.0-ferretdb-2.7.0
restart: always
environment:
POSTGRES_USER: ${POSTGRES_USER}
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
POSTGRES_DB: ${POSTGRES_DB}
PGPASSWORD: ${POSTGRES_PASSWORD}
volumes:
- ferretdb-postgres-data:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U $${POSTGRES_USER} -d $${POSTGRES_DB}"]
interval: 5s
timeout: 5s
retries: 10
ferretdb:
image: ghcr.io/ferretdb/ferretdb:2.7.0
restart: always
ports:
- "127.0.0.1:8088:8088"
- "127.0.0.1:27017:27017"
environment:
FERRETDB_POSTGRESQL_URL: postgres://${POSTGRES_USER}:${POSTGRES_PASSWORD}@postgres:5432/${POSTGRES_DB}
FERRETDB_LISTEN_ADDR: 0.0.0.0:27017
FERRETDB_DEBUG_ADDR: 0.0.0.0:8088
depends_on:
postgres:
condition: service_healthy
volumes:
ferretdb-postgres-data:
Lint, codegen, breaking-change checks and dependency management in one Git-native CLI. Open source, no registry lock-in.
Show that FerretDB is listed as an open-source alternative. Paste into your README:
Ubuntu 24.04 · Apple container, Docker 29.1.3, Compose 2.40.3+ds1-0ubuntu1~24.04.1. Images pulled in 72 s: ghcr.io/ferretdb/postgres-documentdb:17-0.107.0-ferretdb-2.7.0, ghcr.io/ferretdb/ferretdb:2.7.0.
Based on the upstream setup. Official FerretDB Docker Compose setup. Pinned ferretdb to 2.7.0 and postgres-documentdb to 17-0.107.0-ferretdb-2.7.0, moved credentials to .env, exposed debug HTTP port 8088 and MongoDB wire protocol port 27017, and bound both to 127.0.0.1 per step 4a.
postgres-1 | 2026-11-02T04:28:15.900430963Z postgres-1 | 2026-11-02T04:28:15.900732713Z The database cluster will be initialized with locale "en_US.utf8". postgres-1 | 2026-11-02T04:28:15.900735838Z The default database encoding has accordingly been set to "UTF8". postgres-1 | 2026-11-02T04:28:15.900736797Z The default text search configuration will be set to "english". postgres-1 | 2026-11-02T04:28:15.900737672Z postgres-1 | 2026-11-02T04:28:15.900738422Z Data page checksums are disabled. postgres-1 | 2026-11-02T04:28:15.900758922Z postgres-1 | 2026-11-02T04:28:15.900760422Z fixing permissions on existing directory /var/lib/postgresql/data ... ok postgres-1 | 2026-11-02T04:28:15.902115713Z creating subdirectories ... ok postgres-1 | 2026-11-02T04:28:15.902411547Z selecting dynamic shared memory implementation ... posix postgres-1 | 2026-11-02T04:28:15.912026380Z selecting default "max_connections" ... 100 postgres-1 | 2026-11-02T04:28:15.922252672Z selecting default "shared_buffers" ... 128MB postgres-1 | 2026-11-02T04:28:15.926976630Z selecting default time zone ... Etc/UTC postgres-1 | 2026-11-02T04:28:15.927326255Z creating configuration files ... ok postgres-1 | 2026-11-02T04:28:15.990471755Z running bootstrap script ... ok postgres-1 | 2026-11-02T04:28:16.158259005Z performing post-bootstrap initialization ... ok postgres-1 | 2026-11-02T04:28:16.239478964Z initdb: warning: enabling "trust" authentication for local connections postgres-1 | 2026-11-02T04:28:16.239495505Z initdb: hint: You can change this by editing pg_hba.conf or using the option -A, or --auth-local and --auth-host, the next time you run initdb. postgres-1 | 2026-11-02T04:28:16.239513589Z syncing data to disk ... ok postgres-1 | 2026-11-02T04:28:16.239518630Z postgres-1 | 2026-11-02T04:28:16.239519589Z postgres-1 | 2026-11-02T04:28:16.239520380Z Success. You can now start the database server using: postgres-1 | 2026-11-02T04:28:16.239522172Z postgres-1 | 2026-11-02T04:28:16.239523714Z pg_ctl -D /var/lib/postgresql/data -l logfile start postgres-1 | 2026-11-02T04:28:16.239524547Z postgres-1 | 2026-11-02T04:28:16.262232755Z waiting for server to start....2026-11-02 04:28:16.262 UTC [47] LOG: starting PostgreSQL 17.6 (Debian 17.6-2.pgdg12+1) on aarch64-unknown-linux-gnu, compiled by gcc (Debian 12.2.0-14+deb12u1) 12.2.0, 64-bit postgres-1 | 2026-11-02T04:28:16.263012047Z 2026-11-02 04:28:16.262 UTC [47] LOG: listening on Unix socket "/var/run/postgresql/.s.PGSQL.5432" postgres-1 | 2026-11-02T04:28:16.265431339Z 2026-11-02 04:28:16.265 UTC [50] LOG: database system was shut down at 2026-11-02 04:28:16 UTC postgres-1 | 2026-11-02T04:28:16.267931130Z 2026-11-02 04:28:16.267 UTC [47] LOG: database system is ready to accept connections postgres-1 | 2026-11-02T04:28:16.352328380Z done postgres-1 | 2026-11-02T04:28:16.352339297Z server started postgres-1 | 2026-11-02T04:28:16.371663547Z postgres-1 | 2026-11-02T04:28:16.371670672Z /usr/local/bin/docker-entrypoint.sh: running /docker-entrypoint-initdb.d/10-preload.sh postgres-1 | 2026-11-02T04:28:16.373381214Z waiting for server to shut down...2026-11-02 04:28:16.373 UTC [47] LOG: received fast shutdown request postgres-1 | 2026-11-02T04:28:16.374182339Z .2026-11-02 04:28:16.374 UTC [47] LOG: aborting any active transactions postgres-1 | 2026-11-02T04:28:16.375578047Z 2026-11-02 04:28:16.375 UTC [47] LOG: background worker "logical replication launcher" (PID 53) exited with exit code 1 postgres-1 | 2026-11-02T04:28:16.375775005Z 2026-11-02 04:28:16.375 UTC [48] LOG: shutting down postgres-1 | 2026-11-02T04:28:16.376312255Z 2026-11-02 04:28:16.376 UTC [48] LOG: checkpoint starting: shutdown immediate postgres-1 | 2026-11-02T04:28:16.379042214Z 2026-11-02 04:28:16.379 UTC [48] LOG: checkpoint complete: wrote 3 buffers (0.0%); 0 WAL file(s) added, 0 removed, 0 recycled; write=0.001 s, sync=0.001 s, total=0.004 s; sync files=2, longest=0.001 s, average=0.001 s; distance=0 kB, estimate=0 kB; lsn=0/14E4F98, redo lsn=0/14E4F98 postgres-1 | 2026-11-02T04:28:16.380498214Z 2026-11-02 04:28:16.380 UTC [47] LOG: database system is shut down postgres-1 | 2026-11-02T04:28:16.474035755Z done postgres-1 | 2026-11-02T04:28:16.474054505Z server stopped postgres-1 | 2026-11-02T04:28:16.489837380Z waiting for server to start....2026-11-02 04:28:16.488 UTC [64] LOG: Initialized documentdb_core extension postgres-1 | 2026-11-02T04:28:16.489847339Z 2026-11-02 04:28:16.489 UTC [64] WARNING: invalid configuration parameter name "documentdb.enableSortbyIdPushDownToPrimaryKey", removing it postgres-1 | 2026-11-02T04:28:16.489848839Z 2026-11-02 04:28:16.489 UTC [64] DETAIL: "documentdb" is now a reserved prefix. postgres-1 | 2026-11-02T04:28:16.489852589Z 2026-11-02 04:28:16.489 UTC [64] LOG: Loading RUM handler with DocumentDBRumLibraryLoadOption: 0 postgres-1 | 2026-11-02T04:28:16.489853630Z 2026-11-02 04:28:16.489 UTC [64] LOG: Loaded documentdb_rum handler successfully via rum postgres-1 | 2026-11-02T04:28:16.489854589Z 2026-11-02 04:28:16.489 UTC [64] LOG: RUM index array state functions already registered, skipping registration postgres-1 | 2026-11-02T04:28:16.489855505Z 2026-11-02 04:28:16.489 UTC [64] WARNING: could not open directory "pg_documentdb_cursor_files": No such file or directory postgres-1 | 2026-11-02T04:28:16.489856505Z 2026-11-02 04:28:16.489 UTC [64] LOG: Initialized pg_documentdb extension postgres-1 | 2026-11-02T04:28:16.494417380Z 2026-11-02 04:28:16.494 UTC [64] LOG: starting PostgreSQL 17.6 (Debian 17.6-2.pgdg12+1) on aarch64-unknown-linux-gnu, compiled by gcc (Debian 12.2.0-14+deb12u1) 12.2.0, 64-bit postgres-1 | 2026-11-02T04:28:16.495055380Z 2026-11-02 04:28:16.494 UTC [64] LOG: listening on Unix socket "/var/run/postgresql/.s.PGSQL.5432" postgres-1 | 2026-11-02T04:28:16.496853672Z 2026-11-02 04:28:16.496 UTC [67] LOG: database system was shut down at 2026-11-02 04:28:16 UTC postgres-1 | 2026-11-02T04:28:16.500434214Z 2026-11-02 04:28:16.499 UTC [64] LOG: database system is ready to accept connections postgres-1 | 2026-11-02T04:28:16.502354089Z 2026-11-02 04:28:16.502 UTC [70] LOG: pg_cron scheduler started postgres-1 | 2026-11-02T04:28:16.579910714Z done postgres-1 | 2026-11-02T04:28:16.580058297Z server started postgres-1 | 2026-11-02T04:28:16.580299922Z postgres-1 | 2026-11-02T04:28:16.580303589Z /usr/local/bin/docker-entrypoint.sh: running /docker-entrypoint-initdb.d/20-install.sql postgres-1 | 2026-11-02T04:28:16.596269839Z psql:/docker-entrypoint-initdb.d/20-install.sql:1: NOTICE: installing required extension "documentdb_core" postgres-1 | 2026-11-02T04:28:16.599877172Z psql:/docker-entrypoint-initdb.d/20-install.sql:1: NOTICE: installing required extension "pg_cron" postgres-1 | 2026-11-02T04:28:16.605111880Z psql:/docker-entrypoint-initdb.d/20-install.sql:1: NOTICE: installing required extension "tsm_system_rows" postgres-1 | 2026-11-02T04:28:16.605228755Z psql:/docker-entrypoint-initdb.d/20-install.sql:1: NOTICE: installing required extension "vector" postgres-1 | 2026-11-02T04:28:16.611309797Z psql:/docker-entrypoint-initdb.d/20-install.sql:1: NOTICE: installing required extension "postgis" postgres-1 | 2026-11-02T04:28:16.830848464Z psql:/docker-entrypoint-initdb.d/20-install.sql:1: NOTICE: installing required extension "rum" postgres-1 | 2026-11-02T04:28:16.896887089Z CREATE EXTENSION postgres-1 | 2026-11-02T04:28:16.897021005Z postgres-1 | 2026-11-02T04:28:16.897038339Z postgres-1 | 2026-11-02T04:28:16.897927255Z waiting for server to shut down...2026-11-02 04:28:16.897 UTC [64] LOG: received fast shutdown request postgres-1 | 2026-11-02T04:28:16.898564755Z .2026-11-02 04:28:16.898 UTC [64] LOG: aborting any active transactions postgres-1 | 2026-11-02T04:28:16.900416464Z 2026-11-02 04:28:16.900 UTC [70] LOG: pg_cron scheduler shutting down postgres-1 | 2026-11-02T04:28:16.901137589Z 2026-11-02 04:28:16.901 UTC [64] LOG: background worker "logical replication launcher" (PID 71) exited with exit code 1 postgres-1 | 2026-11-02T04:28:16.901453714Z 2026-11-02 04:28:16.901 UTC [64] LOG: background worker "pg_cron launcher" (PID 70) exited with exit code 1 postgres-1 | 2026-11-02T04:28:16.902142672Z 2026-11-02 04:28:16.902 UTC [65] LOG: shutting down postgres-1 | 2026-11-02T04:28:16.902717630Z 2026-11-02 04:28:16.902 UTC [65] LOG: checkpoint starting: shutdown immediate postgres-1 | 2026-11-02T04:28:16.941320672Z 2026-11-02 04:28:16.941 UTC [65] LOG: checkpoint complete: wrote 1466 buffers (8.9%); 0 WAL file(s) added, 0 removed, 1 recycled; write=0.010 s, sync=0.028 s, total=0.040 s; sync files=186, longest=0.012 s, average=0.001 s; distance=13074 kB, estimate=13074 kB; lsn=0/21A9898, redo lsn=0/21A9898 postgres-1 | 2026-11-02T04:28:16.943808381Z 2026-11-02 04:28:16.943 UTC [64] LOG: database system is shut down postgres-1 | 2026-11-02T04:28:16.998567256Z done postgres-1 | 2026-11-02T04:28:16.998584506Z server stopped postgres-1 | 2026-11-02T04:28:16.999035256Z postgres-1 | 2026-11-02T04:28:16.999048422Z PostgreSQL init process complete; ready for start up. postgres-1 | 2026-11-02T04:28:16.999049839Z postgres-1 | 2026-11-02T04:28:17.006607131Z 2026-11-02 04:28:17.006 UTC [1] LOG: Initialized documentdb_core extension postgres-1 | 2026-11-02T04:28:17.007491214Z 2026-11-02 04:28:17.007 UTC [1] WARNING: invalid configuration parameter name "documentdb.enableSortbyIdPushDownToPrimaryKey", removing it postgres-1 | 2026-11-02T04:28:17.007496464Z 2026-11-02 04:28:17.007 UTC [1] DETAIL: "documentdb" is now a reserved prefix. postgres-1 | 2026-11-02T04:28:17.007497547Z 2026-11-02 04:28:17.007 UTC [1] LOG: Loading RUM handler with DocumentDBRumLibraryLoadOption: 0 postgres-1 | 2026-11-02T04:28:17.007636881Z 2026-11-02 04:28:17.007 UTC [1] LOG: Loaded documentdb_rum handler successfully via rum postgres-1 | 2026-11-02T04:28:17.007646256Z 2026-11-02 04:28:17.007 UTC [1] LOG: RUM index array state functions already registered, skipping registration postgres-1 | 2026-11-02T04:28:17.007761964Z 2026-11-02 04:28:17.007 UTC [1] LOG: Initialized pg_documentdb extension postgres-1 | 2026-11-02T04:28:17.011362172Z 2026-11-02 04:28:17.011 UTC [1] LOG: starting PostgreSQL 17.6 (Debian 17.6-2.pgdg12+1) on aarch64-unknown-linux-gnu, compiled by gcc (Debian 12.2.0-14+deb12u1) 12.2.0, 64-bit postgres-1 | 2026-11-02T04:28:17.011525464Z 2026-11-02 04:28:17.011 UTC [1] LOG: listening on IPv4 address "0.0.0.0", port 5432 postgres-1 | 2026-11-02T04:28:17.011528172Z 2026-11-02 04:28:17.011 UTC [1] LOG: listening on IPv6 address "::", port 5432 postgres-1 | 2026-11-02T04:28:17.012909381Z 2026-11-02 04:28:17.012 UTC [1] LOG: listening on Unix socket "/var/run/postgresql/.s.PGSQL.5432" postgres-1 | 2026-11-02T04:28:17.015117756Z 2026-11-02 04:28:17.014 UTC [78] LOG: database system was shut down at 2026-11-02 04:28:16 UTC postgres-1 | 2026-11-02T04:28:17.016860422Z 2026-11-02 04:28:17.016 UTC [1] LOG: database system is ready to accept connections postgres-1 | 2026-11-02T04:28:17.019312797Z 2026-11-02 04:28:17.019 UTC [81] LOG: pg_cron scheduler started postgres-1 | 2026-11-02T04:28:19.030066173Z 2026-11-02 04:28:19.029 UTC [81] LOG: cron job 2 starting: CALL documentdb_api_internal.build_index_concurrently(1); postgres-1 | 2026-11-02T04:28:19.031640965Z 2026-11-02 04:28:19.031 UTC [81] LOG: cron job 3 starting: CALL documentdb_api_internal.build_index_concurrently(2); postgres-1 | 2026-11-02T04:28:19.034515173Z 2026-11-02 04:28:19.034 UTC [81] LOG: cron job 2 COMMAND completed: CALL postgres-1 | 2026-11-02T04:28:19.036040132Z 2026-11-02 04:28:19.035 UTC [81] LOG: cron job 3 COMMAND completed: CALL postgres-1 | 2026-11-02T04:28:21.035074799Z 2026-11-02 04:28:21.034 UTC [81] LOG: cron job 2 starting: CALL documentdb_api_internal.build_index_concurrently(1); postgres-1 | 2026-11-02T04:28:21.036442341Z 2026-11-02 04:28:21.036 UTC [81] LOG: cron job 3 starting: CALL documentdb_api_internal.build_index_concurrently(2); postgres-1 | 2026-11-02T04:28:21.042854091Z 2026-11-02 04:28:21.042 UTC [81] LOG: cron job 2 COMMAND completed: CALL postgres-1 | 2026-11-02T04:28:21.044896716Z 2026-11-02 04:28:21.044 UTC [81] LOG: cron job 3 COMMAND completed: CALL postgres-1 | 2026-11-02T04:28:23.040904717Z 2026-11-02 04:28:23.040 UTC [81] LOG: cron job 2 starting: CALL documentdb_api_internal.build_index_concurrently(1); postgres-1 | 2026-11-02T04:28:23.041425967Z 2026-11-02 04:28:23.041 UTC [81] LOG: cron job 3 starting: CALL documentdb_api_internal.build_index_concurrently(2); postgres-1 | 2026-11-02T04:28:23.046218592Z 2026-11-02 04:28:23.044 UTC [81] LOG: cron job 2 COMMAND completed: CALL postgres-1 | 2026-11-02T04:28:23.047307967Z 2026-11-02 04:28:23.047 UTC [81] LOG: cron job 3 COMMAND completed: CALL postgres-1 | 2026-11-02T04:28:25.047879759Z 2026-11-02 04:28:25.047 UTC [81] LOG: cron job 2 starting: CALL documentdb_api_internal.build_index_concurrently(1); postgres-1 | 2026-11-02T04:28:25.050103551Z 2026-11-02 04:28:25.049 UTC [81] LOG: cron job 3 starting: CALL documentdb_api_internal.build_index_concurrently(2); postgres-1 | 2026-11-02T04:28:25.057455176Z 2026-11-02 04:28:25.057 UTC [81] LOG: cron job 2 COMMAND completed: CALL postgres-1 | 2026-11-02T04:28:25.058860968Z 2026-11-02 04:28:25.058 UTC [81] LOG: cron job 3 COMMAND completed: CALL postgres-1 | 2026-11-02T04:28:27.059141427Z 2026-11-02 04:28:27.058 UTC [81] LOG: cron job 2 starting: CALL documentdb_api_internal.build_index_concurrently(1); postgres-1 | 2026-11-02T04:28:27.060850969Z 2026-11-02 04:28:27.060 UTC [81] LOG: cron job 3 starting: CALL documentdb_api_internal.build_index_concurrently(2); postgres-1 | 2026-11-02T04:28:27.070225260Z 2026-11-02 04:28:27.069 UTC [81] LOG: cron job 3 COMMAND completed: CALL postgres-1 | 2026-11-02T04:28:27.070240594Z 2026-11-02 04:28:27.070 UTC [81] LOG: cron job 2 COMMAND completed: CALL postgres-1 | 2026-11-02T04:28:29.074704845Z 2026-11-02 04:28:29.074 UTC [81] LOG: cron job 2 starting: CALL documentdb_api_internal.build_index_concurrently(1); postgres-1 | 2026-11-02T04:28:29.076201220Z 2026-11-02 04:28:29.076 UTC [81] LOG: cron job 3 starting: CALL documentdb_api_internal.build_index_concurrently(2); postgres-1 | 2026-11-02T04:28:29.083270261Z 2026-11-02 04:28:29.083 UTC [81] LOG: cron job 3 COMMAND completed: CALL postgres-1 | 2026-11-02T04:28:29.083631553Z 2026-11-02 04:28:29.083 UTC [81] LOG: cron job 2 COMMAND completed: CALL