WildDuck: an IMAP server that stores mail in MongoDB
Opinionated email server
At a glance
- What is it?
- WildDuck is an opinionated IMAP and POP3 server that keeps every message in a sharded and replicated MongoDB rather than in a maildir, aimed at operators who already run MongoDB and Redis. Its own compose file ships one database container and a literal placeholder API token, which is where the gap between the design claim and the first boot opens.
- Who is it for?
- WildDuck suits operators who already run MongoDB and Redis and want mailbox state in the same database, and it suits nobody who expects one container to accept and serve mail on its own.
- Can I use it commercially?
- Yes, with conditions. EUPL-1.2 is a weak copyleft licence: you can use it inside commercial and closed-source software, but if you distribute changes to its own files, you must publish those changes under the same licence.
- Is it still maintained?
- Yes. The repository last received commits 1 day ago.
- What is it written in?
- Mainly JavaScript, according to GitHub's language statistics.
Answers come from the project's GitHub data, last synced on October 22, 2026, and from our analysis. They are not legal advice.
Editorial analysis
Mailboxes live in MongoDB, not in a maildir
WildDuck keeps every message, every flag and every mailbox index inside MongoDB, sharded and replicated, and the repository root shows where that decision lands in code: `indexer.js`, an `imap-core/` tree, an `indexes.yaml` at the top level and a `migrations/` directory. With no maildir tree to walk, index definitions have to live somewhere explicit, which is what that top level file is for. Mailbox bookkeeping becomes index and migration work instead of file work, and `package.json` shows how heavily the parsing core is exercised: `test:indexer` selects the attachment codec, both MIME tree parsers, the indexer suites, body structure, envelope creation, contents and query response tests, all under `imap-core/test/`. The internal library keeps its own test tree apart from the server's `test/` directory, so parsing behaviour can be checked without opening a listener.
One mongo service against a no-SPOF claim
The README calls WildDuck a "scalable no-SPOF IMAP/POP3 mail server" on top of a sharded and replicated MongoDB, and then the compose file at the repository root ships the opposite topology: a single `mongo` image and a single `redis:alpine`, both under `restart: always`, joined to the WildDuck container through `depends_on`. No line in that file starts a replica set, a config server or a mongos, so a first run gives you one MongoDB process on one host with one Redis beside it. `depends_on` orders startup but does not wait until either database accepts connections, which is the most common way a fresh bring-up stalls. Read the file as an evaluation harness rather than the no-SPOF topology it describes, and build the replica set yourself before pointing real mailboxes at it.
The API binds 0.0.0.0 with a placeholder token
`api.js` is a first class entry point in the root, next to `worker.js`, `lmtp.js`, `imap.js` and `pop3.js`, and the compose file publishes that management API on port 8080. What ships is a placeholder rather than a credential: `APPCONF_api_host: 0.0.0.0` binds the API to every interface, and `APPCONF_api_accessToken: PLEASE_CHANGE_ME` is the literal string in the file. Change both before the stack is reachable from anywhere but your own machine, since that second value is a constant anyone holding the file already knows. The README links API Documentation at https://docs.wildduck.email/docs/category/wildduck-api, yet it says nothing about rotating the token once a container is running, which leaves rotation as an operation you have to design.
Starting from the compose file and the default TOML
The image on the compose path is `ghcr.io/zone-eu/wildduck`, and the build behind it is the Dockerfile in the root. Its base is `node:lts-alpine`; apk adds make, git, dumb-init and openssl, and dumb-init becomes the entrypoint so node runs as a child process and receives signals. Dependencies install in two steps, with `package*.json` copied first, `npm install --production` executed, and only then `COPY . .` bringing in the rest of the tree, so editing source does not force a dependency reinstall. Three build arguments are declared, TARGETPLATFORM, TARGETARCH and TARGETVARIANT, and the build prints them next to `uname -s` and `uname -mm`; no FROM or RUN line consumes them, so they record the target instead of driving a cross build. The shortest path to a running server:
version: "3.7"
services:
wildduck:
restart: always
image: ghcr.io/zone-eu/wildduck
depends_on:
- redis
- mongoThe database and API values arrive as environment overrides:
environment:
APPCONF_dbs_mongo: mongodb://mongo:27017/wildduck
APPCONF_dbs_redis: redis://redis:6379/3
APPCONF_api_host: 0.0.0.0
APPCONF_api_accessToken: PLEASE_CHANGE_METhose five published ports, 8080, 143, 110, 993 and 995, cover the HTTP API, IMAP, POP3 and their implicit TLS variants. Start the stack, then confirm MongoDB actually took `mongodb://mongo:27017/wildduck` before looking at any protocol error.
Config keys travel as prefixed environment overrides
The container is started against an explicit config file, `--config=/wildduck/config/default.toml`, which points into the `config/` directory in the root and makes the TOML layout the primary interface. The compose file then overrides individual keys through `APPCONF_` variables whose suffixes repeat the key path: `APPCONF_dbs_mongo` and `APPCONF_dbs_redis` carry the two database URLs, `APPCONF_api_host` binds the management API, and `APPCONF_api_accessToken` guards it. A value such as `redis://redis:6379/3` is a Redis URL with a specific logical database, which matters on a host where Redis already serves something else. Certificates are the gap in this surface: 993 and 995 are published, so implicit TLS is expected from the first boot, yet the README documents no provisioning step, and `acme.js` plus a `certs/` directory in the root are the only clues about where that happens.
npm test drops a database and flushes Redis
Running the suite is destructive by design. The `test` script in `package.json` wipes both stores before a single assertion runs:
"test": "mongosh --eval 'db.dropDatabase()' wildduck-test && redis-cli -n 13 flushdb && npm run runtest",The drop and the flush are chained ahead of `npm run runtest`, so both are empty by the time the first test file loads, and both defaults point at a local machine: the script names a test database `wildduck-test` and clears Redis logical database 13. Point either at a shared instance and the suite takes data with it. Past that gate the scripts split by concern, with `test:proto` driving protocol work through grunt, `test:attachments` and `test:indexer` running separate mocha selections, and coverage variants wrapping the same selections in c8 with `--include` filters. Those filters name the modules the project treats as core: `lib/attachment-storage.js`, `lib/attachments/**`, `scripts/migrate-attachments-to-s3.js`, `imap-core/lib/indexer/**` and `imap-core/lib/length-limiter.js`. An S3 migration script sitting inside a coverage include means attachments can be moved out of MongoDB into object storage, and `test/on-copy-uid-shift-test.js` guards UID renumbering when a message is copied.
Gmail is the tie-breaker for product decisions
Product choices here get settled by reference to another product: the README says WildDuck "tries to follow Gmail in product design", with most open questions answered by copying what Gmail does. The repository is arranged to serve that goal. `indexer.js` and the `imap-core/lib/indexer/` suites build a searchable body index, `test/body-structure-test.js` and `test/create-envelope-test.js` pin the structures clients read back, and `examples/push-message.js` shows mail arriving as a push instead of a poll. That is a deliberate preference for client familiarity over protocol minimalism. The cost is documentation: no IMAP extension list, no capability announcement and no version matrix appear anywhere, so an operator deciding which extensions a client may use has nothing to read and has to read source instead.
No transfer port, and no documented spam filtering
Return to the port list: 8080, 143, 110, 993 and 995. There is no transfer port, even though the repository topics include `lmtp` and the root holds `lmtp.js`, so the compose file starts a server that stores and serves mail but does not, as shipped, accept mail from the internet. WildDuck is not a whole mail system in one container; the README places it inside Zone Mail Suite (ZMS), described as a suite of programs and modules for an email server, and that is where the transfer path sits. Spam filtering, rate limiting and reputation handling are absent from the documentation too, and nothing in the root points at them. Licensing deserves the same scrutiny before you build tooling on top: the code is under EUPL-1.2 or later, with both `LICENSE` and `LICENSE_ET` in the root and a copyright line naming Zone Media OÜ. That boundary is the real design comparison. A conventional IMAP server reads a tree of maildir files, so backups are file copies and the schema is the filesystem. WildDuck keeps the same data in MongoDB, so backups become a logical dump of collections, the schema becomes `indexes.yaml` plus `migrations/`, and bulk attachments can move to object storage through `scripts/migrate-attachments-to-s3.js`. For one domain with backups that already understand maildir, the trade buys little.
Editorial conclusion
WildDuck suits operators who already run MongoDB and Redis and want mailbox state in the same database, and it suits nobody who expects one container to accept and serve mail on its own. Before adopting, replace `APPCONF_api_accessToken`, work out how certificates reach the `certs/` directory for the published 993 and 995 ports, and budget for the transfer path the compose file never maps. `scripts/migrate-attachments-to-s3.js` is worth reading before the first large mailbox lands, since attachment storage can leave the database. The last push was on 2026-10-06, so the code is still moving.
Frequently asked questions
Where does WildDuck store mail?
Every message, flag and mailbox index goes into a sharded and replicated MongoDB rather than into files on disk. Index definitions live in `indexes.yaml` at the repository root and schema changes live in the `migrations/` directory.
How do I start WildDuck for a first look?
The compose file at the repository root defines three services, wildduck, redis and mongo, pulling the image `ghcr.io/zone-eu/wildduck`. Connections to both databases come from `APPCONF_dbs_mongo` and `APPCONF_dbs_redis`.
Which ports does the bundled compose file publish?
Five of them: 8080 for the HTTP API, 143 for IMAP, 110 for POP3, and 993 and 995 for the implicit TLS variants of those two protocols. No mail transfer port appears in that list.
Is WildDuck a complete mail server on its own?
Not as the compose file ships it, since it maps no port for receiving mail. The README places WildDuck inside Zone Mail Suite (ZMS), a suite of programs and modules for an email server.
What does the test script do before it runs any test?
The `test` script drops a database and flushes a Redis logical database before `npm run runtest` starts. It names the test database `wildduck-test` and clears Redis database 13, so it expects a local development machine.
Under what licence is WildDuck available?
The code is under the European Union Public License 1.2 or later, and the root ships both `LICENSE` and `LICENSE_ET` with a copyright line naming Zone Media OÜ.
Official sources
Add this badge to your README
If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.
[](https://hysenlabs.com/projects/zone-eu-wildduck)