When logging into the Ubuntu server where Stalwart runs as a Docker, I see:
=> There are 6456 zombie processes.
Reproduction Steps
Leave Stalwart 0.16.x running for a few weeks
ps -eo stat=,ppid= | awk '$1 ~ /^Z/ {c[$2]++} END {for (p in c) print c[p], p}' | sort -rn | head
6469 3184418
# ps -o pid,ppid,user,lstart,args -p 3184418
PID PPID USER STARTED COMMAND
3184418 3184395 2000 Wed Jul 15 08:59:13 2026 /usr/local/bin/stalwart --config /etc/sta...
Relevant Log Output
In the logs, I see lots of
2026-08-10T19:50:29Z ERROR RocksDB error (store.rocksdb-error) reason = "IO error: While open a file for appending: /var/lib/stalwart//000499.log: Too many open files", details = "Failed to lock event.", causedBy = "crates/store/src/dispatch/lookup.rs:382", details = "Failed to lock event.", causedBy = "crates/smtp/src/queue/spool.rs:236"
I have reviewed the documentation and FAQ and confirm that my issue is NOT addressed there.
on
I have searched this support forum (open and closed topics) and confirm this is not a duplicate.
on
I understand that topics in this category are triaged by a bot first but a human reply will follow up. If I’d prefer a human-only reply, I’ll add the no-ai tag to my topic.
Those aren’t processes Stalwart started. There’s exactly one place in the whole codebase that spawns a child (the Sieve exec plugin), and it waits on it properly; nothing in the mail, queue or filtering paths forks at all.
What’s happening is that Stalwart is PID 1 in your container, because the image uses a plain entrypoint with no init process. Anything that gets executed inside the container namespace and then exits is reparented to PID 1, and PID 1 here is an application that never calls wait(), so the entries pile up as zombies with Stalwart listed as the parent. Our own image is a good candidate for the source: it ships a healthcheck that runs a shell and curl every 30 seconds.
Fix is to add --init to your docker run, or init: true under the service if you use compose, then recreate the container. That inserts a proper init as PID 1 which reaps orphans. Existing zombies go away with the restart. This is on us to document, and I’ll get the docker page updated.
The open files error is a separate problem and not caused by the zombies, since those release their descriptors. RocksDB keeps a lot of SST files open, so you want to raise the limit: --ulimit nofile=65535:65535, or the compose equivalent.
If you want to confirm what’s generating them, docker exec <container> ps -eo stat,pid,ppid,comm and look at the zombie entries. Names like curl or sh confirm the healthcheck; anything else points at a monitoring or backup agent.
The docker run block on that page is what people copy, and it has no --init; the compose snippet needs init: true for the same reason. I will get both fixed.
Add --ulimit nofile=65535:65535 to the same command while you are recreating the container. The systemd unit sets LimitNOFILE=65536 and the Docker path has no equivalent, which is what produced the RocksDB “Too many open files” errors you saw. Neither flag can be applied to a running container, so both go in the same docker rm and docker run cycle.
If you want to know what was spawning them before you recreate, docker exec stalwart ps -eo stat,pid,ppid,comm will show you: sh and curl entries are the image’s own healthcheck, anything else is arriving from outside.