/ Infrastructure · Cloud-native deployment
Deploying KumoMTA on Docker and Kubernetes: a production guide.
KumoMTA ships as a multi-arch Docker image and runs naturally on Kubernetes for horizontal scaling. This guide covers the single-node Docker pattern, Docker Compose for multi-component local stacks, the Kubernetes architecture with separate kumod pods plus TSA daemon plus Redis, the egress IP challenge that Kubernetes networking does not solve cleanly, storage persistence, and the rolling-deploy patterns that keep mail flowing through updates.
In short
The official Docker image lives at ghcr.io/kumocorp/kumomta and supports x64 and arm64 in a single multi-arch tag. Single-node Docker is the right pattern for evaluation and small production deployments. Multi-node production on Kubernetes uses three components: kumod pods that handle SMTP, a TSA daemon StatefulSet that coordinates traffic shaping decisions across pods, and a Redis layer (typically DragonflyDB) that shares throttle counters between kumod replicas. The honest weakness is the egress IP problem: Kubernetes networking does not naturally provide stable per-pod sending IPs, which means the proxy layer that anchors your sender reputation usually lives outside the K8s cluster on dedicated bare-metal or VM proxies.
-
Multi-arch
Since Summer 2024 release, the official Docker image at ghcr.io/kumocorp/kumomta supports x64 and arm64 in a single tag.
-
3 layers
A production K8s deployment has kumod pods, a TSA daemon StatefulSet, and a Redis layer for shared throttle state.
-
Egress IP
Stable sending IPs do not fit cleanly inside Kubernetes networking. The proxy layer typically lives outside the cluster.
-
RocksDB spool
The message spool is RocksDB-backed and lives on a persistent volume. Pods can restart without losing queued messages.
/ 01 — Decision
When does containerized KumoMTA actually make sense?
KumoMTA runs equally well on bare metal, in a virtual machine, in Docker, or on Kubernetes. The choice is operational, not performance-driven. The honest answer to which one fits is shaped by team size, deployment pattern, and what you already operate.
Stay on bare metal or VM when
- One or two long-lived nodes carry your traffic
- Team is comfortable with traditional package management
- You do not operate other workloads in containers yet
- Network configuration is already locked down at the OS level
Choose Docker when
- You want consistent images across dev, staging, prod
- You already run other services as containers
- You need x64 and arm64 from one image
- Small fleet — three to five nodes that you operate manually
- Intermediate step toward eventual Kubernetes adoption
Choose Kubernetes when
- Horizontal scaling is a real requirement, not aspirational
- The rest of your infrastructure lives on K8s already
- You have a team that operates clusters daily
- You can solve or sidestep the egress IP challenge
- You want rolling deploys and self-healing pods
Performance is not the differentiator
Docker containers on Linux share the host kernel and impose essentially no overhead on the CPU and network paths KumoMTA uses. A Docker-hosted kumod and a bare-metal kumod on the same hardware produce comparable throughput. If you read otherwise, the difference is configuration, not the container layer itself. The benchmarks the KumoMTA team has published from AWS Ubuntu, Azure Rocky Linux, and bare-metal Fedora hosts confirm the pattern: hardware and tuning explain the variance, not the runtime envelope. Choose containers for operational reasons — image consistency, multi-arch builds, integration with your existing tooling — and treat performance as a wash between options.
/ Interactive — the deployment chooser
Which topology fits your situation?
Topology
\u2014
\u2014
The egress-IP catch
\u2014
The orchestration choice is the easy half. For a sending MTA the source IP carries the reputation, so whichever topology you pick, the egress IP is the thing to design first \u2014 not discover later.
/ 02 — Docker
How do you run KumoMTA in single-node Docker for production?
The official documentation shows a one-line evaluation command that maps port twenty-five and a single volume. The production version adds volume mounts for the spool and logs, port mapping for SMTP, submission, and the HTTP API, a restart policy, resource limits matched to the workload, and a health check the orchestrator reads. Both appear below: the evaluation version to confirm the image works, the production one for a host reboot.
Evaluation · throw-away kumo-sink
# From the official KumoMTA docs
sudo docker run --rm \
-p 2025:25 \
-v .:/opt/kumomta/etc/policy \
--name kumo-sink \
ghcr.io/kumocorp/kumomta:latest
# Inject a test message
sudo docker exec kumo-sink \
/opt/kumomta/sbin/kcli inject \
--from [email protected] \
--to [email protected] \
--subject "Test" \
--body "Hello from KumoMTA" Production · persistent spool, logs, limits
sudo docker run -d \
--name kumomta-prod \
--restart unless-stopped \
-p 25:25 -p 587:587 -p 8000:8000 \
-v /srv/kumomta/policy:/opt/kumomta/etc/policy:ro \
-v /srv/kumomta/spool:/var/spool/kumomta \
-v /srv/kumomta/logs:/var/log/kumomta \
--cpus="4" --memory=8g \
--health-cmd="kcli ping" \
--health-interval=30s \
ghcr.io/kumocorp/kumomta:latest Volumes
Mount the policy directory read-only, the spool read-write, and the logs directory read-write. The spool is the only one whose loss means lost mail.
Ports
25 for inbound SMTP relay, 587 for submission, 8000 for the HTTP API. TLS on submission requires you to mount the certificate paths the policy expects.
Resources
Four CPU cores is the production minimum the KumoMTA team specifies. Memory depends on queue depth and tenant count; eight gigabytes is comfortable for a mid-volume single-node deployment.
/ 03 — Kubernetes
The Kubernetes architecture: three components and what they each do.
The production-grade pattern shipping in the community demo at github.com/dschaaff/kumomta-k8s-demo divides the deployment into three layers. The MTA itself runs as a fleet of kumod pods. Traffic Shaping Automation runs as a separate StatefulSet of TSA daemon pods. Throttle state shared between the kumod replicas lives in Redis, typically DragonflyDB because it ships with redis-cell compatibility out of the box. The split exists because the three components have different scaling characteristics, different state requirements, and different failure modes. Lumping them together produces a deployment that scales poorly and recovers worse from individual component failures.
kumod pods
The MTA itself. Deploy as a standard Deployment with PersistentVolumeClaim for the spool. Each pod handles SMTP listener, HTTP API, and queue management. Scale horizontally based on throughput needs.
TSA daemon
StatefulSet rather than Deployment because the daemon needs stable DNS identity for the kumod pods to publish events to. Hostnames look like kumomta-tsa-0.kumomta-tsa-headless.default.svc.cluster.local:8008.
Redis (DragonflyDB)
Shares moment-to-moment throttle counters between kumod replicas. DragonflyDB is preferred because it implements the redis-cell CL_THROTTLE command natively. KumoMTA falls back to a Lua implementation if redis-cell is unavailable.
/ 04 — Egress IP
Why is the egress IP problem so hard to solve in Kubernetes?
This is the single hardest problem in running KumoMTA on Kubernetes, openly acknowledged by both the community demo author and the KumoMTA documentation. It is the difference between delivering from known IPs with stable reputation and from whatever NAT egress is active now. Teams that ignore it until production discover the answer through reputation drops that are expensive to undo.
Why this is hard
Sender reputation lives on the public IP that initiates the SMTP connection to the receiver. Gmail, Yahoo, Microsoft, and Apple all track reputation per-IP and use it to make placement decisions. For shared-pool senders this is irritating; for dedicated-IP senders it is the whole point.
Kubernetes networking is designed to abstract away the underlying network identity of workloads. Pods get cluster-internal IPs, and traffic leaves through a shared NAT gateway whose public IP can rotate on a node restart or autoscale. None of that matches the need for tenant A's mail to leave from a stable IP it has warmed.
The community demo at github.com/dschaaff/kumomta-k8s-demo names this explicitly: "The Kumo proxy layer, responsible for public sending IP on the packets, is not included in this demo setup. The networking paradigms of Kubernetes are not a good fit for this component and implementation is left as an exercise for the reader."
The patterns that actually work
Pattern 1 · External proxy fleet
A small fleet of bare-metal or VM proxies sit outside Kubernetes, anchored to your warmed sending IPs. The K8s kumod pods route outbound traffic through this proxy fleet. Most common production solution in 2026.
Pattern 2 · BYO IP cloud networking
AWS BYOIP, Azure custom IP prefixes, GCP bring-your-own-IP. Lets you assign your warmed IPs to specific cluster egress NAT gateways. Works at meaningful scale but vendor-specific and adds cloud bill complexity.
Pattern 3 · Host-networked pods on dedicated nodes
Run kumod pods with hostNetwork true on nodes that have your sending IPs as secondary addresses. Bypasses K8s networking entirely for outbound. Works but loses much of what K8s gives you.
Choosing between the three patterns
Pattern one is the default recommendation for teams that already operate some bare-metal or VM infrastructure alongside Kubernetes. It separates concerns cleanly and the proxy fleet can be tiny — two or three VMs are enough for most mid-volume senders. Pattern two pays off when your cloud spend is large enough to absorb the BYOIP setup cost and you do not want any bare-metal footprint. Pattern three exists for teams that need the simplest possible architecture and can accept the operational trade-off of treating specific nodes as special. The KumoMTA team's preferred direction in 2026 conversations is pattern one because it preserves the cluster's general-purpose nature while moving the IP problem outside it.
/ 05 — Storage
Spool persistence, RocksDB, and what survives a pod restart.
KumoMTA stores messages in a RocksDB-backed spool. Without persistence, pod restarts lose every queued message, which is unacceptable for any production deployment. With persistence configured correctly, restarts are graceful and queue continuity holds across upgrades.
What needs persistent storage
- Spool directory: RocksDB data for queued messages and their metadata. Mounted at
/var/spool/kumomtaby default. Loss equals lost mail. - Accounting database: if you configure one, it persists delivery history. Useful for audit but not strictly required for operation.
- Log directory: structured logs that downstream consumers may not have processed yet. PVC recommended; emptyDir acceptable if logs ship to Kafka or webhooks promptly.
What can be ephemeral
- Policy directory: read-only mount from ConfigMap or Secret. The Lua policy lives in version control, not on the pod's writable filesystem.
- TLS certificates: read-only Secret mount. Rotate via Kubernetes mechanisms rather than writing to the pod.
- DKIM keys: Secret mount or, better, dynamically fetched from Vault per the Lua policy patterns. Never written to the spool volume.
PVC storage class matters
Choose a storage class with adequate IOPS — RocksDB does sustained random writes. AWS gp3 with provisioned IOPS, Azure Premium SSD v2, or GCP balanced SSD are typical production choices. NFS-backed PVCs are not appropriate for the spool; RocksDB expects local-disk semantics. Size the volume for peak queue depth rather than steady state: a four-hour outage at a major receiver can produce queue depths an order of magnitude higher than normal. Fifty to one hundred gigabytes per kumod replica is a reasonable starting point for mid-volume senders; high-volume deployments routinely allocate several hundred gigabytes per pod to handle the worst-case backlog without running out of space at the worst possible moment.
/ 06 — Operations
How do you roll out a KumoMTA upgrade without dropping mail?
Rolling deploys of an MTA are trickier than deploys of stateless web services. Messages are in flight, queues are non-empty, and graceful shutdown matters in a way that web service deploys can usually ignore. The patterns below come from operating containerized KumoMTA for multiple production senders across cloud and on-premise clusters, and they reduce the number of avoidable incidents during upgrade windows.
Pattern 1
preStop hook for drainage
Use a Kubernetes preStop hook that calls the KumoMTA admin endpoint to stop accepting new mail, then waits for the active queue to drain or a timeout to expire. Pod terminates gracefully rather than dropping in-flight work.
Pattern 2
Readiness gates the listener
Readiness probe checks the HTTP API endpoint, not the SMTP listener. The kumod must finish loading policy, opening the spool, and registering with TSA before traffic flows. Readiness false during this window prevents premature traffic.
Pattern 3
PodDisruptionBudget protects continuity
Set a PDB requiring at least N minus one pods available during voluntary disruptions like node drain. Prevents the cluster from terminating all kumod replicas simultaneously during upgrades.
Pattern 4
Validation in CI before deploy
Run kumod --validate against the new policy in CI. Configuration changes that fail validation never reach the cluster, regardless of how convincing the diff looks in the pull request.
Pattern 5
Prometheus + Grafana wired from day one
Use the KumoMTA Prometheus endpoint and the official Grafana dashboard published on grafana.com. Deploy the monitoring before the kumod pods, not after — debugging a Kubernetes deploy without metrics is an unforced error.
Pattern 6
Image pinning, not latest
Pin to a specific KumoMTA version tag in your manifests. The latest tag is convenient for evaluation and dangerous for production. Bump versions deliberately as part of regular maintenance.
/ 07 — Anti-patterns
Container-specific anti-patterns that bite in production.
Anti-pattern 1 · emptyDir spool volume
What happens: the spool is mounted on an emptyDir, which lives only for the pod's lifetime. The pod restarts during a node upgrade and every queued message is lost. Better: always use a PersistentVolumeClaim for the spool, with a storage class that survives node failures.
Anti-pattern 2 · Ignoring egress IP requirements
What happens: kumod pods deploy with default Kubernetes networking, mail leaves the cluster from the shared NAT gateway IP, sender reputation is ruined within days. Better: design the proxy or BYOIP layer before any production traffic flows. Egress identity is not a detail to figure out later.
Anti-pattern 3 · Stateless deploy with no drainage
What happens: rolling deploy treats kumod like a stateless service. SIGTERM arrives, the pod terminates within thirty seconds, in-flight SMTP transactions are aborted. Better: preStop hooks that drain the active queue, terminationGracePeriodSeconds long enough to accommodate that drain.
Anti-pattern 4 · Co-locating Redis with kumod pods
What happens: Redis runs as a sidecar in the kumod pod. Redis dies when the pod dies, taking shared throttle state with it. Better: Redis or DragonflyDB runs as a separate Deployment or StatefulSet with its own lifecycle, accessed by kumod pods through a ClusterIP service.
Anti-pattern 5 · Treating the community Helm demo as production-ready
What happens: the dschaaff demo is applied unchanged to a production cluster and operated as if it were a supported product. Better: fork the demo, adapt it to your environment, version-control it in your infrastructure repository, and treat it like any other piece of operational code that you own.
/ 08 — FAQ
Questions engineering teams ask before deploying.
Should I run KumoMTA in Docker or directly on a Linux host?
Run on a Linux host when your deployment is one or two nodes, the team is comfortable with traditional package management, and you have stable physical or virtual machines you intend to keep. Run in Docker when you want consistent images across environments, you are already using containers, you need multi-arch support across x64 and arm64, or you intend to move to Kubernetes later. Performance under Docker on Linux is essentially identical to bare metal because the container shares the host kernel.
Can KumoMTA really run on Kubernetes given the egress IP problem?
Yes, with caveats that the community demo and the KumoMTA documentation both acknowledge openly. The MTA pods and the TSA daemon pods run cleanly. The challenge is the egress IP layer: messages need to leave from specific dedicated IPs for reputation purposes, and Kubernetes networking does not naturally provide stable per-pod egress IPs. In production, the typical solution is to deploy KumoMTA proxy pods behind a fixed set of public IPs managed outside Kubernetes.
What is the official KumoMTA Docker image?
The official image is published at ghcr.io/kumocorp/kumomta on GitHub Container Registry. Since the Summer 2024 release the image is multi-arch, supporting both x64 and arm64 from a single tag. The image is rebuilt with every KumoMTA release, alongside the Amazon Linux, Rocky Linux, and Ubuntu repository packages.
How is throttle state shared between pods?
Through Redis. The KumoMTA throttle implementation uses the CL_THROTTLE command from the redis-cell module when available, which DragonflyDB ships with out of the box. When redis-cell is not available, KumoMTA falls back to a Lua-based implementation. The TSA daemon coordinates high-level shaping decisions across pods; Redis coordinates the moment-to-moment throttle counters that need sub-millisecond consistency.
What happens to messages during a rolling deploy?
The spool is RocksDB-backed and lives on a persistent volume in any production-grade deployment. When a pod restarts, the RocksDB spool survives, and the new pod picks up where the old one left off. The operational pattern is to drain the pod before terminating it: stop accepting new mail via the SMTP listener, let in-flight deliveries complete or fail, then terminate. KumoMTA exposes admin commands for graceful shutdown that integrate with Kubernetes preStop hooks.
Is there an official Helm chart?
There is no official Helm chart from KumoCorp as of mid-2026. The community demo at github.com/dschaaff/kumomta-k8s-demo provides a working chart covering the kumod StatefulSet, the TSA daemon StatefulSet, and the DragonflyDB-based Redis layer. The demo is explicitly labeled a demo, which means production deployments fork it and adapt it rather than using it as-is.
/ Next step
Designing a containerized KumoMTA deployment? We can help solve the egress IP problem with you.
Email Delivery Platform operates KumoMTA on Kubernetes for multiple tenants, with proxy fleets that anchor stable sending IPs outside the cluster. If you are designing a cloud-native deployment and want a review of the architecture before you commit to it, the discovery call ends with a concrete recommendation.