Initial commit on Forgejo
Some checks are pending
CI and deploy / Test (push) Waiting to run
CI and deploy / Build, push, and deploy (push) Blocked by required conditions

Fresh repository history for elektrine/elektrine-haraka hosted at
https://git.elektrine.com/elektrine/elektrine-haraka.
This commit is contained in:
Maxfield Luke 2026-07-29 04:43:42 -04:00
commit 67c1d41a0c
82 changed files with 8713 additions and 0 deletions

53
deployment/.env.example Normal file
View file

@ -0,0 +1,53 @@
# Haraka environment variables
# Copy to .env and set your values
# Domain Configuration
HARAKA_DOMAIN=mail.example.com
# SSL Configuration
SSL_EMAIL=admin@example.com
STAGING=0
# Shared key (optional)
HARAKA_API_KEY=your-secure-api-key-here
# Directional keys
# Haraka -> Phoenix
PHOENIX_API_KEY=replace-with-haraka-to-phoenix-key
# Phoenix -> Haraka
HARAKA_HTTP_API_KEY=replace-with-phoenix-to-haraka-key
# Phoenix App Integration
PHOENIX_WEBHOOK_URL=https://your-phoenix-app.com/api/haraka/inbound
PHOENIX_VERIFY_URL=https://your-phoenix-app.com/api/haraka/verify-recipient
PHOENIX_DOMAINS_URL=https://your-phoenix-app.com/api/haraka/domains
# Image tag
HARAKA_IMAGE=git.elektrine.com/elektrine/elektrine-haraka
HARAKA_IMAGE_TAG=latest
# Optional service images
CADDY_IMAGE=caddy:2-alpine
REDIS_IMAGE=redis:7-alpine
CLAMAV_IMAGE=clamav/clamav:stable
SPAMASSASSIN_IMAGE=dinkel/spamassassin:latest
FAIL2BAN_IMAGE=crazymax/fail2ban:latest
CERT_COPIER_IMAGE=alpine:3.20
# Redis + async queue
REDIS_URL=redis://redis:6379
ELEKTRINE_QUEUE_NAME=elektrine:inbound
ELEKTRINE_DLQ_NAME=elektrine:inbound:dlq
# Worker retry policy
WEBHOOK_MAX_RETRIES=5
WEBHOOK_RETRY_BASE_MS=1000
# Optional ops CIDRs for keyless access to /status, /healthz, and /metrics.
# Leave blank to require X-API-Key by default.
METRICS_ALLOWED_CIDRS=
OPS_ALLOWED_CIDRS=
# Queue thresholds
QUEUE_WARN_THRESHOLD=1000
DLQ_CRIT_THRESHOLD=10

View file

@ -0,0 +1,38 @@
# Same-server bare-metal deployment
# Run this beside the main elektrine Docker stack.
HARAKA_DOMAIN=mail.example.com
LOCAL_DOMAINS=example.com,mail.example.com
# Shared from the main Elektrine deployment
PHOENIX_WEBHOOK_URL=http://elektrine_app:8080/api/haraka/inbound
PHOENIX_VERIFY_URL=http://elektrine_app:8080/api/haraka/verify-recipient
PHOENIX_DOMAINS_URL=http://elektrine_app:8080/api/haraka/domains
# Haraka -> Phoenix
PHOENIX_API_KEY=replace-with-haraka-to-phoenix-key
# Phoenix -> Haraka
HARAKA_HTTP_API_KEY=replace-with-phoenix-to-haraka-key
# Optional legacy shared key
# HARAKA_API_KEY=
# Match the main Elektrine Caddy volume name.
MAIN_CADDY_VOLUME=elektrine_caddy_data
MAIN_APP_NETWORK=docker_elektrine_network
# Haraka outbound API stays loopback-only on the same host.
HARAKA_HTTP_BIND_PORT=18080
# Optional trusted networks that may read /status, /healthz, and /metrics without X-API-Key.
OPS_ALLOWED_CIDRS=
METRICS_ALLOWED_CIDRS=
HARAKA_IMAGE=git.elektrine.com/elektrine/elektrine-haraka
HARAKA_IMAGE_TAG=latest
REDIS_URL=redis://redis:6379
ELEKTRINE_QUEUE_NAME=elektrine:inbound
ELEKTRINE_DLQ_NAME=elektrine:inbound:dlq
WEBHOOK_MAX_RETRIES=5
WEBHOOK_RETRY_BASE_MS=1000

71
deployment/README.md Normal file
View file

@ -0,0 +1,71 @@
# Deployment
Compose-based production deployment for the multi-role Haraka topology. This is
the supported deployment path for this repository.
## Services
- `haraka-inbound`: public MX listener on `:25`
- `haraka-outbound`: internal HTTP send/ops API on `:8080`
- `haraka-worker`: Redis consumer for inbound delivery to Phoenix
- `haraka-submission`: optional authenticated submission role, not published by default
- `redis`, `clamav`, `spamassassin`
## Setup
```bash
cd deployment
cp .env.example .env
# edit .env
./start.sh
```
## Same-server setup
For a single-host deployment beside the main Elektrine stack:
```bash
cp .env.same-server.example .env
# edit .env
../scripts/deploy/docker_deploy.sh
```
This uses `docker-compose.same-server.yml`, publishes MX on `25`, binds the
Haraka HTTP API to `127.0.0.1:18080`, and reads certificates from the main
Elektrine Caddy volume. It also joins the main Elektrine Docker network so
Haraka can call `http://elektrine_app:8080` directly.
## Useful Commands
```bash
# inspect all services
docker compose ps
# view role logs
docker compose logs -f haraka-inbound
docker compose logs -f haraka-submission
docker compose logs -f haraka-outbound
docker compose logs -f haraka-worker
# queue state
docker compose exec redis redis-cli LLEN elektrine:inbound
docker compose exec redis redis-cli LLEN elektrine:inbound:dlq
# queue alert check (non-zero exit when thresholds exceeded)
../scripts/check-queues.sh
# health checks from the host running Haraka
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/metrics
```
## Notes
- Elektrine should call the HTTP API through `HARAKA_BASE_URL`, commonly `http://127.0.0.1:18080` on same-host installs or `http://haraka-outbound:8080` on a shared Docker network.
- SMTP HTTP API traffic is served directly by `haraka-outbound:8080` inside Compose.
- Inbound SMTP processing is async: accept fast on `haraka-inbound`, parse/deliver from `haraka-worker`.
- Client SMTP submission normally lives in Elektrine. Publish `haraka-submission` only if you intentionally want Haraka-managed submission.
- `/status`, `/healthz`, and `/metrics` accept `X-API-Key` by default.
- Set `OPS_ALLOWED_CIDRS` and `METRICS_ALLOWED_CIDRS` in `.env` only if you also want keyless access from trusted networks.
- Use immutable `HARAKA_IMAGE_TAG` values for reproducible rollouts.
- Override `HARAKA_IMAGE` in `.env` if you need to pull from a different registry/repo.

View file

@ -0,0 +1,223 @@
services:
haraka-inbound:
image: ${HARAKA_IMAGE:-git.elektrine.com/elektrine/elektrine-haraka}:${HARAKA_IMAGE_TAG:-latest}
ports:
- "25:25"
volumes:
- ../logs:/app/logs
- ../config:/app/config
- ../config/dkim:/data/haraka/dkim
- ../plugins:/app/plugins:ro
- ../lib:/app/lib:ro
- ../scripts:/app/scripts:ro
- ssl-certs:/app/ssl
environment:
- NODE_ENV=production
- HARAKA_ROLE=inbound-mx
- HARAKA_API_KEY=${HARAKA_API_KEY:-}
- PHOENIX_API_KEY=${PHOENIX_API_KEY:-}
- PHOENIX_WEBHOOK_URL=${PHOENIX_WEBHOOK_URL:-}
- PHOENIX_VERIFY_URL=${PHOENIX_VERIFY_URL:-}
- PHOENIX_DOMAINS_URL=${PHOENIX_DOMAINS_URL:-}
- LOCAL_DOMAINS=${LOCAL_DOMAINS:-}
- REDIS_URL=${REDIS_URL:-redis://redis:6379}
- ELEKTRINE_QUEUE_NAME=${ELEKTRINE_QUEUE_NAME:-elektrine:inbound}
- ELEKTRINE_DLQ_NAME=${ELEKTRINE_DLQ_NAME:-elektrine:inbound:dlq}
- HARAKA_DOMAIN=${HARAKA_DOMAIN:-mail.example.com}
restart: unless-stopped
extra_hosts:
- "host.docker.internal:host-gateway"
networks:
- haraka-network
- main-app-network
depends_on:
- redis
- clamav
- spamassassin
haraka-submission:
image: ${HARAKA_IMAGE:-git.elektrine.com/elektrine/elektrine-haraka}:${HARAKA_IMAGE_TAG:-latest}
volumes:
- ../logs:/app/logs
- ../config:/app/config
- ../config/dkim:/data/haraka/dkim
- ../plugins:/app/plugins:ro
- ../lib:/app/lib:ro
- ../scripts:/app/scripts:ro
- ssl-certs:/app/ssl
environment:
- NODE_ENV=production
- HARAKA_ROLE=submission
- HARAKA_API_KEY=${HARAKA_API_KEY:-}
- PHOENIX_API_KEY=${PHOENIX_API_KEY:-}
- PHOENIX_VERIFY_URL=${PHOENIX_VERIFY_URL:-}
- PHOENIX_DOMAINS_URL=${PHOENIX_DOMAINS_URL:-}
- LOCAL_DOMAINS=${LOCAL_DOMAINS:-}
- REDIS_URL=${REDIS_URL:-redis://redis:6379}
- HARAKA_DOMAIN=${HARAKA_DOMAIN:-mail.example.com}
restart: unless-stopped
extra_hosts:
- "host.docker.internal:host-gateway"
networks:
- haraka-network
- main-app-network
depends_on:
- redis
haraka-outbound:
image: ${HARAKA_IMAGE:-git.elektrine.com/elektrine/elektrine-haraka}:${HARAKA_IMAGE_TAG:-latest}
ports:
- "127.0.0.1:${HARAKA_HTTP_BIND_PORT:-18080}:8080"
volumes:
- ../logs:/app/logs
- ../config:/app/config
- ../config/dkim:/data/haraka/dkim
- ../plugins:/app/plugins:ro
- ../lib:/app/lib:ro
- ../scripts:/app/scripts:ro
- ssl-certs:/app/ssl
environment:
- NODE_ENV=production
- HARAKA_ROLE=outbound-relay
- HARAKA_API_KEY=${HARAKA_API_KEY:-}
- PHOENIX_API_KEY=${PHOENIX_API_KEY:-}
- HARAKA_HTTP_API_KEY=${HARAKA_HTTP_API_KEY:-}
- PHOENIX_WEBHOOK_URL=${PHOENIX_WEBHOOK_URL:-}
- PHOENIX_VERIFY_URL=${PHOENIX_VERIFY_URL:-}
- PHOENIX_DOMAINS_URL=${PHOENIX_DOMAINS_URL:-}
- LOCAL_DOMAINS=${LOCAL_DOMAINS:-}
- REDIS_URL=${REDIS_URL:-redis://redis:6379}
- HARAKA_HTTP_PORT=8080
- HARAKA_HTTP_HOST=0.0.0.0
- HARAKA_DOMAIN=${HARAKA_DOMAIN:-mail.example.com}
restart: unless-stopped
extra_hosts:
- "host.docker.internal:host-gateway"
networks:
- haraka-network
- main-app-network
depends_on:
- redis
haraka-worker:
image: ${HARAKA_IMAGE:-git.elektrine.com/elektrine/elektrine-haraka}:${HARAKA_IMAGE_TAG:-latest}
command: ["node", "/app/scripts/elektrine-worker.js"]
volumes:
- ../logs:/app/logs
- ../config:/app/config
- ../config/dkim:/data/haraka/dkim
- ../plugins:/app/plugins:ro
- ../lib:/app/lib:ro
- ../scripts:/app/scripts:ro
environment:
- NODE_ENV=production
- HARAKA_ROLE=worker
- HARAKA_API_KEY=${HARAKA_API_KEY:-}
- PHOENIX_API_KEY=${PHOENIX_API_KEY:-}
- PHOENIX_WEBHOOK_URL=${PHOENIX_WEBHOOK_URL:-}
- PHOENIX_VERIFY_URL=${PHOENIX_VERIFY_URL:-}
- PHOENIX_DOMAINS_URL=${PHOENIX_DOMAINS_URL:-}
- LOCAL_DOMAINS=${LOCAL_DOMAINS:-}
- REDIS_URL=${REDIS_URL:-redis://redis:6379}
- ELEKTRINE_QUEUE_NAME=${ELEKTRINE_QUEUE_NAME:-elektrine:inbound}
- ELEKTRINE_DLQ_NAME=${ELEKTRINE_DLQ_NAME:-elektrine:inbound:dlq}
- WEBHOOK_MAX_RETRIES=${WEBHOOK_MAX_RETRIES:-5}
- WEBHOOK_RETRY_BASE_MS=${WEBHOOK_RETRY_BASE_MS:-1000}
restart: unless-stopped
extra_hosts:
- "host.docker.internal:host-gateway"
networks:
- haraka-network
- main-app-network
depends_on:
- redis
cert-copier:
image: ${CERT_COPIER_IMAGE:-alpine:3.20}
volumes:
- main-caddy-data:/caddy-data:ro
- ssl-certs:/ssl-certs
environment:
- HARAKA_DOMAIN=${HARAKA_DOMAIN:-mail.example.com}
command: >
sh -c "
while true; do
if [ -f /caddy-data/caddy/certificates/acme-v02.api.letsencrypt.org-directory/wildcard_.elektrine.com/wildcard_.elektrine.com.crt ]; then
cp /caddy-data/caddy/certificates/acme-v02.api.letsencrypt.org-directory/wildcard_.elektrine.com/wildcard_.elektrine.com.crt /ssl-certs/cert.crt
cp /caddy-data/caddy/certificates/acme-v02.api.letsencrypt.org-directory/wildcard_.elektrine.com/wildcard_.elektrine.com.key /ssl-certs/cert.key
chmod 644 /ssl-certs/cert.crt
chmod 600 /ssl-certs/cert.key
elif [ -f /caddy-data/caddy/certificates/acme-v02.api.letsencrypt.org-directory/$$HARAKA_DOMAIN/$$HARAKA_DOMAIN.crt ]; then
cp /caddy-data/caddy/certificates/acme-v02.api.letsencrypt.org-directory/$$HARAKA_DOMAIN/$$HARAKA_DOMAIN.crt /ssl-certs/cert.crt
cp /caddy-data/caddy/certificates/acme-v02.api.letsencrypt.org-directory/$$HARAKA_DOMAIN/$$HARAKA_DOMAIN.key /ssl-certs/cert.key
chmod 644 /ssl-certs/cert.crt
chmod 600 /ssl-certs/cert.key
fi
sleep 30
done
"
networks:
- haraka-network
restart: unless-stopped
redis:
image: ${REDIS_IMAGE:-redis:7-alpine}
command:
- redis-server
- --appendonly
- yes
- --appendfsync
- everysec
- --save
- "900 1"
- --save
- "300 10"
- --save
- "60 10000"
ports:
- "127.0.0.1:6379:6379"
volumes:
- redis-data:/data
restart: unless-stopped
networks:
- haraka-network
clamav:
image: ${CLAMAV_IMAGE:-clamav/clamav:stable}
ports:
- "127.0.0.1:3310:3310"
volumes:
- clamav-data:/var/lib/clamav
environment:
- CLAMAV_NO_FRESHCLAM=false
restart: unless-stopped
networks:
- haraka-network
spamassassin:
image: ${SPAMASSASSIN_IMAGE:-dinkel/spamassassin:latest}
ports:
- "127.0.0.1:783:783"
volumes:
- spamassassin-data:/var/lib/spamassassin
environment:
- SA_UPDATE=1
restart: unless-stopped
networks:
- haraka-network
networks:
haraka-network:
driver: bridge
main-app-network:
external: true
name: ${MAIN_APP_NETWORK:-docker_elektrine_network}
volumes:
main-caddy-data:
external: true
name: ${MAIN_CADDY_VOLUME:-elektrine_caddy_data}
redis-data:
ssl-certs:
clamav-data:
spamassassin-data:

View file

@ -0,0 +1,240 @@
services:
haraka-inbound:
image: ${HARAKA_IMAGE:-git.elektrine.com/elektrine/elektrine-haraka}:${HARAKA_IMAGE_TAG:-latest}
# For local development, comment out image and uncomment build:
# build: ../
ports:
- "25:25"
volumes:
- ../logs:/app/logs
- ../config:/app/config
- ../config/dkim:/data/haraka/dkim
- ../plugins:/app/plugins:ro
- ../lib:/app/lib:ro
- ../scripts:/app/scripts:ro
- ssl-certs:/app/ssl
environment:
- NODE_ENV=production
- HARAKA_ROLE=inbound-mx
- HARAKA_API_KEY=${HARAKA_API_KEY:-}
- PHOENIX_API_KEY=${PHOENIX_API_KEY:-}
- PHOENIX_WEBHOOK_URL=${PHOENIX_WEBHOOK_URL:-}
- PHOENIX_VERIFY_URL=${PHOENIX_VERIFY_URL:-}
- PHOENIX_DOMAINS_URL=${PHOENIX_DOMAINS_URL:-}
- REDIS_URL=${REDIS_URL:-redis://redis:6379}
- ELEKTRINE_QUEUE_NAME=${ELEKTRINE_QUEUE_NAME:-elektrine:inbound}
- ELEKTRINE_DLQ_NAME=${ELEKTRINE_DLQ_NAME:-elektrine:inbound:dlq}
- HARAKA_DOMAIN=${HARAKA_DOMAIN:-mail.example.com}
restart: unless-stopped
networks:
- haraka-network
- main-app-network
depends_on:
- redis
- clamav
- spamassassin
deploy:
resources:
limits:
memory: 1G
cpus: '2.0'
healthcheck:
test: ["CMD", "nc", "-z", "localhost", "25"]
interval: 30s
timeout: 10s
retries: 5
start_period: 30s
logging:
driver: "json-file"
options:
max-size: "10m"
max-file: "3"
haraka-submission:
image: ${HARAKA_IMAGE:-git.elektrine.com/elektrine/elektrine-haraka}:${HARAKA_IMAGE_TAG:-latest}
volumes:
- ../logs:/app/logs
- ../config:/app/config
- ../config/dkim:/data/haraka/dkim
- ../plugins:/app/plugins:ro
- ../lib:/app/lib:ro
- ../scripts:/app/scripts:ro
- ssl-certs:/app/ssl
environment:
- NODE_ENV=production
- HARAKA_ROLE=submission
- HARAKA_API_KEY=${HARAKA_API_KEY:-}
- PHOENIX_API_KEY=${PHOENIX_API_KEY:-}
- PHOENIX_VERIFY_URL=${PHOENIX_VERIFY_URL:-}
- PHOENIX_DOMAINS_URL=${PHOENIX_DOMAINS_URL:-}
- REDIS_URL=${REDIS_URL:-redis://redis:6379}
- HARAKA_DOMAIN=${HARAKA_DOMAIN:-mail.example.com}
restart: unless-stopped
networks:
- haraka-network
- main-app-network
depends_on:
- redis
deploy:
resources:
limits:
memory: 768M
cpus: '1.0'
haraka-outbound:
image: ${HARAKA_IMAGE:-git.elektrine.com/elektrine/elektrine-haraka}:${HARAKA_IMAGE_TAG:-latest}
ports:
- "127.0.0.1:${HARAKA_HTTP_BIND_PORT:-18080}:8080"
volumes:
- ../logs:/app/logs
- ../config:/app/config
- ../config/dkim:/data/haraka/dkim
- ../plugins:/app/plugins:ro
- ../lib:/app/lib:ro
- ../scripts:/app/scripts:ro
- ssl-certs:/app/ssl
environment:
- NODE_ENV=production
- HARAKA_ROLE=outbound-relay
- HARAKA_API_KEY=${HARAKA_API_KEY:-}
- PHOENIX_API_KEY=${PHOENIX_API_KEY:-}
- HARAKA_HTTP_API_KEY=${HARAKA_HTTP_API_KEY:-}
- PHOENIX_WEBHOOK_URL=${PHOENIX_WEBHOOK_URL:-}
- PHOENIX_VERIFY_URL=${PHOENIX_VERIFY_URL:-}
- PHOENIX_DOMAINS_URL=${PHOENIX_DOMAINS_URL:-}
- REDIS_URL=${REDIS_URL:-redis://redis:6379}
- HARAKA_HTTP_PORT=8080
- HARAKA_HTTP_HOST=0.0.0.0
- HARAKA_DOMAIN=${HARAKA_DOMAIN:-mail.example.com}
restart: unless-stopped
networks:
- haraka-network
- main-app-network
depends_on:
- redis
deploy:
resources:
limits:
memory: 768M
cpus: '1.0'
healthcheck:
test:
[
"CMD",
"node",
"-e",
"const key = process.env.HARAKA_HTTP_API_KEY || process.env.HARAKA_API_KEY || ''; const req = require('http').request('http://127.0.0.1:8080/status', { headers: key ? { 'x-api-key': key } : {} }, (res) => process.exit(res.statusCode === 200 ? 0 : 1)); req.on('error', () => process.exit(1)); req.end();"
]
interval: 30s
timeout: 10s
retries: 5
start_period: 30s
haraka-worker:
image: ${HARAKA_IMAGE:-git.elektrine.com/elektrine/elektrine-haraka}:${HARAKA_IMAGE_TAG:-latest}
command: ["node", "/app/scripts/elektrine-worker.js"]
volumes:
- ../logs:/app/logs
- ../config:/app/config
- ../config/dkim:/data/haraka/dkim
- ../plugins:/app/plugins:ro
- ../lib:/app/lib:ro
- ../scripts:/app/scripts:ro
environment:
- NODE_ENV=production
- HARAKA_ROLE=worker
- HARAKA_API_KEY=${HARAKA_API_KEY:-}
- PHOENIX_API_KEY=${PHOENIX_API_KEY:-}
- PHOENIX_WEBHOOK_URL=${PHOENIX_WEBHOOK_URL:-}
- PHOENIX_VERIFY_URL=${PHOENIX_VERIFY_URL:-}
- PHOENIX_DOMAINS_URL=${PHOENIX_DOMAINS_URL:-}
- REDIS_URL=${REDIS_URL:-redis://redis:6379}
- ELEKTRINE_QUEUE_NAME=${ELEKTRINE_QUEUE_NAME:-elektrine:inbound}
- ELEKTRINE_DLQ_NAME=${ELEKTRINE_DLQ_NAME:-elektrine:inbound:dlq}
- WEBHOOK_MAX_RETRIES=${WEBHOOK_MAX_RETRIES:-5}
- WEBHOOK_RETRY_BASE_MS=${WEBHOOK_RETRY_BASE_MS:-1000}
restart: unless-stopped
networks:
- haraka-network
- main-app-network
depends_on:
- redis
redis:
image: ${REDIS_IMAGE:-redis:7-alpine}
command:
- redis-server
- --appendonly
- yes
- --appendfsync
- everysec
- --save
- "900 1"
- --save
- "300 10"
- --save
- "60 10000"
ports:
- "127.0.0.1:6379:6379"
volumes:
- redis-data:/data
restart: unless-stopped
networks:
- haraka-network
healthcheck:
test: ["CMD", "redis-cli", "PING"]
interval: 30s
timeout: 5s
retries: 5
clamav:
image: ${CLAMAV_IMAGE:-clamav/clamav:stable}
ports:
- "127.0.0.1:3310:3310"
volumes:
- clamav-data:/var/lib/clamav
environment:
- CLAMAV_NO_FRESHCLAM=false
restart: unless-stopped
networks:
- haraka-network
healthcheck:
test: ["CMD", "/usr/local/bin/clamdcheck.sh"]
interval: 60s
timeout: 120s
retries: 3
deploy:
resources:
limits:
memory: 2G
cpus: '1.0'
spamassassin:
image: ${SPAMASSASSIN_IMAGE:-dinkel/spamassassin:latest}
ports:
- "127.0.0.1:783:783"
volumes:
- spamassassin-data:/var/lib/spamassassin
environment:
- SA_UPDATE=1
restart: unless-stopped
networks:
- haraka-network
deploy:
resources:
limits:
memory: 512M
cpus: '0.5'
networks:
haraka-network:
driver: bridge
main-app-network:
external: true
name: ${MAIN_APP_NETWORK:-docker_elektrine_network}
volumes:
redis-data:
ssl-certs:
clamav-data:
spamassassin-data:

96
deployment/start.sh Executable file
View file

@ -0,0 +1,96 @@
#!/bin/bash
# Start elektrine-haraka step by step
set -euo pipefail
if command -v docker >/dev/null 2>&1 && docker compose version >/dev/null 2>&1; then
compose() { docker compose "$@"; }
elif command -v docker-compose >/dev/null 2>&1; then
compose() { docker-compose "$@"; }
else
echo "ERROR: docker compose or docker-compose is required"
exit 1
fi
echo "Starting elektrine-haraka deployment..."
# Validate required environment variables from shell and/or deployment/.env
required_vars=(
"HARAKA_DOMAIN"
"PHOENIX_WEBHOOK_URL"
"PHOENIX_VERIFY_URL"
"PHOENIX_DOMAINS_URL"
)
get_var() {
local var_name="$1"
local value="${!var_name:-}"
if [ -n "$value" ]; then
printf "%s" "$value"
return 0
fi
if [ -f .env ]; then
value=$(grep -E "^${var_name}=" .env | tail -n 1 | cut -d '=' -f 2-)
printf "%s" "$value"
return 0
fi
printf ""
}
for var_name in "${required_vars[@]}"; do
if [ -z "$(get_var "$var_name")" ]; then
echo "ERROR: ${var_name} is required (set in shell or deployment/.env)"
exit 1
fi
done
if [ -z "$(get_var "PHOENIX_API_KEY")" ] && [ -z "$(get_var "HARAKA_API_KEY")" ]; then
echo "ERROR: set PHOENIX_API_KEY (recommended) or HARAKA_API_KEY (legacy fallback)"
exit 1
fi
if [ -z "$(get_var "HARAKA_HTTP_API_KEY")" ] && [ -z "$(get_var "HARAKA_API_KEY")" ]; then
echo "ERROR: set HARAKA_HTTP_API_KEY (recommended) or HARAKA_API_KEY (legacy fallback)"
exit 1
fi
# Validate compose interpolation early
compose config >/dev/null
# 1. Start supporting services first
echo "Starting Redis, ClamAV, and SpamAssassin..."
compose up -d redis clamav spamassassin
# 2. Wait for services to be ready
echo "Waiting for services to initialize..."
sleep 10
# 3. Start Haraka roles and worker
echo "Starting Haraka inbound/submission/outbound roles and worker..."
compose up -d haraka-inbound haraka-submission haraka-outbound haraka-worker
# 4. Wait for Haraka roles to be ready
sleep 5
# 5. Start Caddy (handles HTTPS/TLS automatically)
echo "Starting Caddy reverse proxy..."
compose up -d caddy
# 6. Start cert-copier (copies Caddy certs to Haraka for STARTTLS)
echo "Starting certificate copier..."
compose up -d cert-copier
echo ""
echo "Deployment complete!"
echo ""
echo "Services running:"
compose ps
echo ""
echo "Test your setup:"
echo " SMTP: telnet $(hostname) 25"
echo " Submission: telnet $(hostname) 587"
echo " API: curl -H 'X-API-Key: <key>' https://$(hostname)/api/v1/send"