Skip to content

/ 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.

Published 2026-06-18 By Matthias Krause, Principal Infrastructure Engineer ~15 min read

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.

KumoMTA on Kubernetes — three coordinated layers kumod (Deployment, n replicas) kumod-1 SMTP + HTTP spool: PVC kumod-2 SMTP + HTTP spool: PVC kumod-3 SMTP + HTTP spool: PVC ... TSA daemon (StatefulSet, 2+ replicas) kumomta-tsa-0 Stable network identity tsa-0.headless:8008 kumomta-tsa-1 Stable network identity tsa-1.headless:8008 Redis (shared throttle state) DragonflyDB redis-cell compatible CL_THROTTLE command Outside Kubernetes — KumoMTA proxies on dedicated IPs Bare-metal or VM fleet · public sending IPs · reputation lives here

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/kumomta by 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.