The 2 AM Wake-Up Call
It was 2:17 AM when I got the Slack message: “MongoDB throwing errors, can’t connect.” After half an hour of digging, the real problem wasn’t a connectivity issue — our self-hosted MongoDB instance had hit memory limits on a small VPS, and the on-call engineer had accidentally stopped the service trying to restart it. What got me thinking wasn’t the incident itself. It was the conversation that followed at 9 AM: “Why are we even running MongoDB? We already have PostgreSQL.”
One week of digging later, I had a name: FerretDB.
I’ve used MySQL, PostgreSQL, and MongoDB across projects of different sizes. Each earns its place. MySQL for transactional workloads.
PostgreSQL for complex queries and extension-heavy stacks. MongoDB for flexible schemas when you’re moving fast early on. But MongoDB carries real costs — not just licensing concerns since the SSPL switch in 2018, but the operational overhead of running a separate database system on a small team. FerretDB splits the difference: keep your existing MongoDB drivers and tooling, move your data into PostgreSQL.
What FerretDB Actually Is
FerretDB is not a MongoDB fork. It’s a proxy — a translation layer that speaks MongoDB’s wire protocol on one end and talks to PostgreSQL on the other. Your application connects to FerretDB exactly as it would to MongoDB, using the same drivers, same connection strings, same queries. Under the hood, FerretDB converts those MongoDB operations into SQL and executes them against a standard PostgreSQL database.
Two concrete wins fall out of this design:
- You get 100% open-source licensing (Apache 2.0). No SSPL, no vendor lock-in concerns.
- Your data lives in PostgreSQL — ACID transactions, point-in-time recovery, and the entire PostgreSQL extension ecosystem come for free.
How the Translation Works
When you run db.users.find({age: {$gt: 25}}), FerretDB receives the MongoDB wire protocol message, parses the BSON query, and generates a PostgreSQL query against a JSONB column. Documents are stored as JSONB rows. Collections map to tables. The translation is invisible to application code — your app has no idea it’s talking to PostgreSQL.
FerretDB is still maturing — full MongoDB feature parity isn’t there yet. Aggregation support is improving, but operators like $facet and $graphLookup aren’t fully implemented. Performance also differs from native MongoDB’s storage engine. For straightforward workloads, though, it works reliably.
Getting FerretDB Running with Docker
You need two containers: PostgreSQL (the actual storage) and FerretDB (the wire protocol translator). Here’s the docker-compose.yml I’ve tested and use:
version: '3.8'
services:
postgres:
image: postgres:16
container_name: ferretdb-postgres
environment:
POSTGRES_USER: ferretdb
POSTGRES_PASSWORD: secret123
POSTGRES_DB: ferretdb
volumes:
- postgres_data:/var/lib/postgresql/data
networks:
- ferretdb-net
restart: unless-stopped
ferretdb:
image: ghcr.io/ferretdb/ferretdb:latest
container_name: ferretdb
environment:
FERRETDB_POSTGRESQL_URL: postgres://ferretdb:secret123@postgres:5432/ferretdb
ports:
- "27017:27017"
networks:
- ferretdb-net
depends_on:
- postgres
restart: unless-stopped
volumes:
postgres_data:
networks:
ferretdb-net:
driver: bridge
Start the stack:
docker compose up -d
Give PostgreSQL about 15 seconds to initialize, then check FerretDB’s output:
docker compose logs ferretdb
Look for Listening on 0.0.0.0:27017. That’s your signal it’s accepting connections.
Connecting with mongosh
Connect exactly as you would to a real MongoDB instance:
mongosh mongodb://localhost:27017
No local mongosh? Run it directly from Docker without installing anything:
docker run --rm -it --network ferretdb-net mongo:6 mongosh mongodb://ferretdb:27017
Once connected, run a few operations to confirm everything works:
use testdb
db.servers.insertMany([
{ hostname: 'web-01', role: 'frontend', region: 'us-east' },
{ hostname: 'db-01', role: 'database', region: 'us-east' },
{ hostname: 'cache-01', role: 'cache', region: 'eu-west' }
])
db.servers.find({ region: 'us-east' })
db.servers.updateOne(
{ hostname: 'web-01' },
{ $set: { status: 'active' } }
)
db.servers.countDocuments()
All of that works. Now open a PostgreSQL shell and see where that data actually lives:
docker exec -it ferretdb-postgres psql -U ferretdb -d ferretdb
-- Discover the generated tables
\dt
-- Query stored documents as JSONB
SELECT * FROM testdb.servers_9c4bc50e LIMIT 5;
Your MongoDB documents are JSONB rows in a PostgreSQL table. It’s oddly satisfying to see.
Connecting from Python
Existing PyMongo code connects to FerretDB with zero changes:
from pymongo import MongoClient
client = MongoClient('mongodb://localhost:27017')
db = client['myapp']
collection = db['events']
collection.insert_one({
'event': 'deployment',
'service': 'api',
'status': 'success',
'timestamp': '2024-01-15T02:17:00Z'
})
for doc in collection.find({'status': 'success'}):
print(doc)
Same driver. Same connection string. Same query API. FerretDB is invisible to application code.
Things That Will Trip You Up
Authentication: The config above has no MongoDB-level auth enforced. For anything beyond local dev, keep FerretDB off public-facing ports — bind it to a private Docker network. FerretDB’s docs cover username/password configuration via environment variables if you need auth at the application layer.
Aggregation pipeline gaps: Complex stages like $lookup across collections or deeply nested $group operations may not behave as expected. Test your specific aggregation pipelines against FerretDB before committing to a migration — don’t assume coverage.
Indexes matter more here: Without explicit indexes, JSONB queries can be slow. Create them the same way as in MongoDB:
db.servers.createIndex({ region: 1 })
db.servers.createIndex({ hostname: 1 }, { unique: true })
These translate to PostgreSQL B-tree indexes on JSONB paths. Don’t skip this step — performance drops significantly on large collections without them.
Table naming: FerretDB generates PostgreSQL table names with hash suffixes (e.g., servers_9c4bc50e). When querying PostgreSQL directly for monitoring or debugging, run \dt in psql to discover the actual names before writing any direct SQL.
When FerretDB Is the Right Call
Nine months running FerretDB in production across two internal services. Here’s where I’d reach for it:
- Teams already running PostgreSQL who want to consolidate infrastructure rather than maintain a second database system
- Projects where MongoDB’s SSPL licensing is a compliance or philosophical blocker
- Internal tools and microservices with moderate data volumes and straightforward query patterns
- New projects that want a document model with PostgreSQL’s proven backup, recovery, and replication tooling
Skip it if you’re depending on MongoDB Atlas-specific features, time-series collections, advanced full-text search, or high-throughput workloads tuned specifically for MongoDB’s storage engine. The gaps are real for complex use cases.
Those two services we migrated after that 2 AM incident? Twelve months in, no issues. The engineer who asked “Why are we running MongoDB?” now calls it the best accidental architecture decision we made that year. A 2 AM page that actually improved the stack. Beats the alternative.

