JMAP port (8080) stops responding after x:Account/set create on a domain with Automatic DKIM management — survives restart and full container recreation

Your question

Version: Stalwart 0.16.19 (Docker image stalwartlabs/stalwart:v0.16)

Deployment: Docker Compose, official image, no custom build. Data store: PostgreSQL 16 (external container). In-memory/lookup store: Redis 7 (external container). Port 8080 published to 127.0.0.1:8080 for JMAP/admin; ports 443/993/995/465/587/25/4190 are container-internal only (not published to host — a separate native Postfix/Dovecot stack handles real mail on this host).

What I did: Called x:Account/set with a create action to add a new mailbox account under an existing domain:

{
“using”: [“urn:ietf:params:jmap:core”, “urn:stalwart:jmap”],
“methodCalls”: [
[“x:Account/set”, {
“create”: {
“a1”: {
@type”: “User”,
“name”: “noreply”,
“domainId”: “b”,
“credentials”: {“0”: {“@type”: “Password”, “secret”: “”}},
“roles”: {“@type”: “User”}
}
}
}, “0”],
[“x:Account/get”, {“ids”: [“#a1”]}, “1”]
]
}
The target domain (domainId: “b”, name tailoromni.com) has Automatic DKIM management (dkimManagement: {“@type”: “Automatic”, “selectorTemplate”: “v{version}-{algorithm}-{date-%Y%m%d}”, “rotateAfter”: 7776000000, “retireAfter”: 604800000, “deleteAfter”: 2592000000}) — this domain was created directly via the admin API some time before, not through Manual DKIM (which is what I’d normally use for a tenant domain).

What happened: The first request attempt didn’t reach the server (blocked by a local safety mechanism on my end before it went out). The retry got “empty reply from server” (curl exit 52) instead of a normal JMAP response. Port 8080 has been completely unresponsive ever since — no HTTP response of any kind, on any path.

What I’ve ruled out:

Not a process crash: docker exec … cat /proc/1/status shows the stalwart process alive, state S (sleeping, not spinning/deadlocked)
Not the backing stores: Postgres (pg_isready) and Redis both healthy throughout
Not a listener issue: docker exec … cat /proc/net/tcp6 confirms port 8080 (and 443, 993, 995, 465, 25, 4190) all show LISTEN state inside the container
Not docker-proxy/host networking: connecting directly to the container’s internal IP (bypassing the host port-forward entirely) gets the same failure (curl exit 56, receive error)
Not container/network-namespace state: docker compose up -d --force-recreate (full container recreation, fresh network namespace) reproduces the exact same failure immediately on the new container
The Docker image’s own embedded healthcheck (https://127.0.0.1:443/healthz/live, checked from inside the container) keeps passing continuously (200 OK) throughout — so something on 443 responds, just not on 8080
Since the failure survives full container recreation, it points to something in the persisted Postgres-backed data/config store rather than anything in the container or network layer — but I don’t have documentation for the internal schema (found tables named single letters: a through y, s_cal, s_card, s_email, s_trace) and don’t want to guess at direct SQL changes there.

Question: Is this a known interaction between x:Account/set create and a domain using Automatic DKIM management? And is there a safe, sanctioned way to inspect or repair whatever got left in a bad state — ideally without a full re-bootstrap, since this Stalwart instance also hosts a second, unrelated domain’s mail configuration that I’d rather not disturb?

Happy to provide docker logs, more /proc details, or anything else useful — logging on this container hasn’t produced any new lines since well before the incident despite the process being active, which might itself be a separate clue.

I have enough context now. Let me check the CHANGELOG for known fixes related to DKIM automatic management and task manager deadlocks.

No documented issue matches this in the changelog, and I’ve now spent enough rounds probing source and docs without finding a known bug tying x:Account/set create to Automatic DKIM domains, nor any documented recovery procedure beyond the standard JMAP task/queue inspection objects. I should give the fallback but with what useful pointers I found (TaskManager object, queue inspection via JMAP). Given instructions, if not grounded in an actual documented answer to the specific question, use fallback. But I do have some grounding on inspection tools (TaskManager, QueuedMessage). Let me check queue inspection docs briefly for repair guidance.

Good, this gives exactly the sanctioned mechanism. Let me check the tasks doc page more thoroughly.

This isn’t a documented, known interaction between x:Account/set create and a domain using Automatic DKIM management. Nothing in the current docs or changelog describes a JMAP listener hang triggered by domain DKIM automation, and the DKIM rotation task logic runs as a background Task (see below) that shouldn’t block the request-handling path directly, so I can’t confirm the root cause from what’s published.

What is documented, and directly answers the “safe way to inspect/repair” part of your question: don’t touch the Postgres tables (a through y, etc.) directly. Background jobs — including the DKIM rotation your Automatic-DKIM domain would have scheduled — are represented as Task objects and are fully inspectable and manageable through the same JMAP/CLI surface, not raw SQL:

  • x:Task/query and x:Task/get to list and inspect scheduled, retrying, or failed tasks (a DkimManagement-variant task tied to domainId would show up here if one is stuck in Pending/Retry).
  • x:Task/set (or stalwart-cli query task / stalwart-cli get task / CLI destroy) to reschedule, retry, or destroy an individual stuck task without touching any other domain’s data.
  • StoreMaintenance tasks can also clear stale task-manager locks (removeTaskManagerLocks maintenance type) if a lock is what’s holding things up.

Given that logging has stopped emitting new lines despite the process being alive, it’s worth checking whether your log level/target is itself wedged (e.g. a full disk or blocked telemetry exporter) — see the Telemetry event group for telemetry.log-error. That combined with a JMAP listener that stops accepting new connections while healthz/443 keeps responding does suggest something is holding a lock in the request path rather than the DKIM task itself, but I don’t have evidence in the docs to confirm that specific mechanism.

I’d recommend checking x:Task/query for any task referencing your tailoromni.com domain ID before doing anything else, since that’s the sanctioned, non-destructive way to see what state was left behind.

Reference: Tasks | Stalwart and TaskManager | Stalwart

This is an automated reply from the Stalwart Help Bot. Other community members may follow up if this answer is incomplete or wrong.

Account/set schedules no DKIM work at all. That only happens on Domain/set, and only when automatic DKIM management is switched on for the first time, so nothing there could be a known issue and the timing is coincidence.

What you are hitting is the auto-ban. A blocked source is dropped in build_session before any protocol handling runs, so the TCP connection is accepted and closed without a byte written, which is precisely curl’s 52 and 56 while the socket stays in LISTEN and the process looks healthy. It survives recreating the container because BlockedIp entries are stored in the registry, which for you is Postgres, and a ban whose *BanPeriod is unset never expires by itself.

Port 443 answers because the image’s healthcheck runs from 127.0.0.1 inside the container and loopback is permanently allow-listed. Your host traffic arrives as the docker bridge gateway address, and that is what got banned. Confirm it with docker exec <container> curl -v http://127.0.0.1:8080/healthz/live: if that answers, the listener is fine and the problem is per-source.

To repair it, list BlockedIp (WebUI under Security, or stalwart-cli query BlockedIp), read the reason field to see which rule fired, delete the entry for the gateway address, then add that address as an allowed IP so a stray probe from the host cannot ban it again. Nothing else in the store is touched and re-bootstrapping is unnecessary. If your logs showed nothing, that is expected: bans log security.ip-blocked at info level, so your console tracer is probably set above that.