Haraka mail relay for Elektrine.
Elektrine owns mailbox storage, webmail, IMAP/POP3/JMAP, user authentication, and the Phoenix API endpoints. This repo owns the Haraka side: public MX intake, inbound queueing, outbound internet delivery, DKIM signing, and the internal HTTP send API Elektrine calls for external mail.
Docker Compose is the supported deployment path.
The stack is split into small roles:
haraka-inbound: public MX listener on port25.haraka-outbound: internal HTTP send and ops API on port8080.haraka-worker: Redis consumer that parses inbound mail and posts it to Phoenix.haraka-submission: optional Haraka-managed SMTP submission role, not published by default.redis,clamav,spamassassin: supporting services for queueing and scanning.
Normal inbound path:
- Remote mail arrives at
haraka-inboundon port25. - Haraka verifies recipients against Phoenix.
- Haraka queues the raw RFC 5322 message in Redis.
haraka-workerparses the queued message and posts it to Phoenix.- Elektrine stores and displays the message.
Normal outbound path from Elektrine:
- Elektrine accepts mail from webmail, SMTP submission, or another Elektrine mail interface.
- Elektrine calls
haraka-outboundatPOST /api/v1/send. - Haraka builds or accepts the RFC 5322 message.
- Haraka signs with DKIM and queues outbound SMTP delivery.
Client SMTP submission usually lives in Elektrine. Only publish
haraka-submission if you explicitly want Haraka to handle authenticated client
submission.
cp deployment/.env.example deployment/.env
# edit deployment/.env
cd deployment
./start.shFor manual Compose control:
cd deployment
docker compose up -dFor local image builds, change the Haraka services in
deployment/docker-compose.yml from image: to build: ../, then run:
docker compose up -d --buildWhen Elektrine and Haraka run on the same host, use the same-server deployment:
cp deployment/.env.same-server.example deployment/.env
# edit deployment/.env
scripts/deploy/docker_deploy.shThis mode:
- publishes public MX SMTP on port
25. - binds Haraka's HTTP API to
127.0.0.1:18080. - joins the main Elektrine Docker network so Haraka can call Phoenix directly.
- copies TLS certificates from the main Elektrine Caddy volume.
- avoids running a second public
80/443reverse proxy.
Configure Elektrine with a matching Haraka URL, commonly:
HARAKA_BASE_URL=http://127.0.0.1:18080If both containers share a Docker network, use the service URL instead:
HARAKA_BASE_URL=http://haraka-outbound:8080Required environment values:
HARAKA_DOMAIN: mail host name, for examplemail.example.com.PHOENIX_WEBHOOK_URL: inbound webhook endpoint.PHOENIX_VERIFY_URL: recipient verification endpoint.PHOENIX_DOMAINS_URL: local-domain cache endpoint.PHOENIX_API_KEY: key Haraka uses when calling Phoenix.HARAKA_HTTP_API_KEY: key Elektrine uses when calling Haraka.
Legacy fallback:
HARAKA_API_KEY: accepted as a shared fallback when directional keys are not set.
Common optional values:
REDIS_URL, defaultredis://redis:6379.ELEKTRINE_QUEUE_NAME, defaultelektrine:inbound.ELEKTRINE_DLQ_NAME, defaultelektrine:inbound:dlq.WEBHOOK_MAX_RETRIES.WEBHOOK_RETRY_BASE_MS.HARAKA_IMAGE.HARAKA_IMAGE_TAG.OPS_ALLOWED_CIDRS.METRICS_ALLOWED_CIDRS.HARAKA_TRUSTED_PROXY_CIDRS.
See deployment/.env.example and deployment/.env.same-server.example for
the deployment templates.
The HTTP API is served by haraka-outbound on port 8080.
Endpoints:
POST /api/v1/sendGET /statusGET /healthzGET /metrics
POST /api/v1/send requires X-API-Key: <HARAKA_HTTP_API_KEY>.
Structured send payload:
{
"from": "sender@example.com",
"to": "recipient@example.net",
"subject": "Hello",
"text_body": "Plain text body",
"html_body": "<p>HTML body</p>"
}Required fields for structured mail:
fromtosubject
Accepted plain-text body fields:
text_bodytextbody
Accepted HTML body fields:
html_bodyhtml
When text and HTML are both present, Haraka builds a multipart/alternative
message. Attachments use the attachments array with base64 data.
For prebuilt RFC 5322 messages, send raw_base64 plus from and to. Prefer
structured fields unless you intentionally need to preserve a supplied MIME tree.
Ops endpoints accept X-API-Key by default. OPS_ALLOWED_CIDRS and
METRICS_ALLOWED_CIDRS can allow keyless access from trusted networks.
# from deployment/
docker compose ps
docker compose logs -f haraka-inbound
docker compose logs -f haraka-outbound
docker compose logs -f haraka-worker
docker compose logs -f haraka-submission
# queue depth
docker compose exec redis redis-cli LLEN elektrine:inbound
# dead-letter queue depth
docker compose exec redis redis-cli LLEN elektrine:inbound:dlq
# queue alarm helper
../scripts/check-queues.sh
# local API checks
curl -s -H 'X-API-Key: <key>' http://127.0.0.1:18080/status
curl -s -H 'X-API-Key: <key>' http://127.0.0.1:18080/metricsDKIM keys live under config/dkim/<domain>/ and are normalized at container
start. Use the helper when provisioning a local key:
scripts/generate-dkim-keys.sh example.comPublish the generated DNS record before relying on outbound delivery for that domain.
421or other temporary inbound failures: check Redis,haraka-inbound, andharaka-workerlogs.- Mail is accepted but never reaches Elektrine: check worker logs and
elektrine:inbound:dlqdepth. - Outbound mail has an empty body: check the
POST /api/v1/sendpayload containstext_body,text,body,html_body, orhtml; then checkharaka-outboundlogs. ENETUNREACH ... :25for IPv6 MX targets: this stack defaults outbound delivery to IPv4 first withinet_prefer=v4because many Docker hosts do not have working outbound IPv6.ERR_TLS_CERT_ALTNAME_INVALIDon the internalharaka-inboundhop: local-domain routing disables STARTTLS for that container hop because the Docker hostname does not match the public certificate.- Outbound delivery is rejected for authentication: verify
HARAKA_HTTP_API_KEYmatches Elektrine's Haraka API key. - Recipient verification fails: verify
PHOENIX_VERIFY_URL,PHOENIX_DOMAINS_URL, andPHOENIX_API_KEY. - TLS issues: check the
ssl-certsvolume and same-server certificate copier logs.
- Redeploy all Haraka roles and the worker together after plugin or shared library changes.
- Use immutable
HARAKA_IMAGE_TAGvalues for reproducible deploys. - Plugin role profiles are documented in
docs/Plugins.md.