Skip to content

Environment Variables

How Variables Reach Services

Understanding the plumbing matters more here than in most stacks:

  • Compose interpolation is the only .env channel. No compose file uses env_file:, and no service loads .env with dotenv. A variable in .env reaches a container only if that service's environment: block in docker-compose.yml (or the prod overlay) names it. Setting something in .env that no block references does nothing.
  • There is no central Settings object. Each service reads its own os.getenv() calls — often with different defaults for the same name. Tables below note per-service defaults where they differ.
  • Secrets live in secrets/ and are bind-mounted read-only: MYSQL_ROOT_PASSWORD_FILE=/run/secrets/db_root_password (mysql), DB_ROOT_PASSWORD_FILE (secrets-check), ENCRYPTION_KEK_PATH (default /run/secrets/encryption_kek), ARCHIVE_AGE_IDENTITY_FILE (default /run/secrets/archive_age_identity).
  • The secrets-check gate runs before everything else (dev compose) and refuses to start the stack if any of DB_ROOT_PASSWORD, DB_PASSWORD, WEBHOOK_SECRET, OAUTH_TOKEN_SECRET, URL_HMAC_SECRET, ADMIN_PASSWORD, ADMIN_TOKEN_SECRET, GRAFANA_ADMIN_PASSWORD is missing, shorter than 16 chars, or a known-weak value. scripts/generate-secrets.sh produces a compliant .env.
  • Postfix pipe/spawn scripts (tracking injector, policy daemons) don't inherit container env; the entrypoint writes /etc/postfix/runtime.env with the DB_*, REDIS_*, TRACKING_*, DELIVERY_OPTIMIZER_*, WEBHOOK_SECRET, HOSTNAME set, and the scripts load that file.

Database

Consumed by nearly every service; set per-service in compose (DB_HOST=mysql, DB_PORT=3306).

Variable Default in code Required Description
DB_HOST mysql (most services; localhost in a few standalone tools) Yes MySQL hostname
DB_PORT 3306 No
DB_NAME mailserver No
DB_USER mailuser (some legacy paths default root) Yes
DB_PASSWORD — (weak fallbacks exist in code but are rejected by secrets-check) Yes
DB_ROOT_PASSWORD — Yes Root password; kept in sync with secrets/db_root_password by generate-secrets.sh
DB_POOL_SIZE / DB_MAX_OVERFLOW / DB_POOL_TIMEOUT / DB_POOL_RECYCLE 10 / 20 / 30 / 3600 No Pool tuning (storage_usage, rate_limiter)
DB_TIMEOUT 5 No Postfix tracking-injector connect timeout

Redis

Variable Default Description
REDIS_HOST / REDIS_PORT redis / 6379 Compose sets these on every consumer
REDIS_URL — Full URL override; compose sets redis://redis:6379/0 (rate_limiter) and /1 (queue_manager)
REDIS_PASSWORD / REDIS_SSL — / false
REDIS_DB varies: 0 most, 1 rate_limiter, 2 storage_usage & Dovecot auth policy, 4 delivery_optimizer Per-service DB separation

Server Identity & Mail

Variable Default Consumers Description
HOSTNAME mail.example.com postfix, autoconfig, cert_manager, api Server FQDN (must match PTR)
DOMAIN example.com cert_manager, autoconfig, compose interpolation Base domain for admin hostnames (api.$DOMAIN, …)
MAIL_HOSTNAME — (falls back to HOSTNAME) api (DNS records), autoconfig, cert_manager The advertised mail hostname when it differs from HOSTNAME
MAIL_SPF_HOST spf.<server hostname> api Host named in generated SPF include: records
ADMIN_EMAIL — api (alert sender fallback)
MTA_STS_MODE testing (code & dev compose); enforce in prod compose autoconfig MTA-STS policy mode
DEFAULT_ORGANIZATION_ID / DEFAULT_DOMAIN_ID default postfix tracking injector Fallback attribution for tracked mail

API Service

Variable Default Description
HOST / PORT 0.0.0.0 / 5000 (compose sets PORT=8080; host port 8083) Bind address/port — there are no API_HOST/API_PORT variables
ADMIN_PASSWORD / ADMIN_TOKEN_SECRET — Admin auth; validated by secrets-check
FLASK_ENV / FLASK_DEBUG development / 0 Environment mode flags (naming is legacy; the app is FastAPI)
CORS_ALLOWED_ORIGINS http://localhost:3000 Comma-separated origins
MAILYTE_COOKIE_SECURE true Set false only for plain-HTTP dev
MAILYTE_EDITION community in code; compose default enterprise Capability flags
TOTP_SERVICE_URL http://totp:8103 2FA service
SIEVE_HOST / SIEVE_PORT dovecot (via IMAP_HOST) / 4190 ManageSieve
MAIL_SUBMIT_HOST / MAIL_SUBMIT_PORT postfix / 10587 Internal submission listener
MONITORING_SERVICE_URL / QUEUE_SERVICE_URL / STORAGE_SERVICE_URL / RAG_SERVICE_URL / ANALYTICS_API_BASE / MIGRATION_SERVICE_URL / RATE_LIMITER_API_BASE / TRACKING_API_BASE / PROMETHEUS_URL in-network service URLs Gateway targets
AI_BASE_URL / AI_API_KEY / AI_MODEL / AI_TIMEOUT — / — / gpt-4o-mini / 45 Mailbox AI features (503 ai_not_configured when unset). Passed through to the api container by compose since 2026-08-31 — before that, a value in .env never reached the service
MIN_CLIENT_VERSION_<PLATFORM> / LATEST_CLIENT_VERSION_<PLATFORM> / UPDATE_URL_<PLATFORM> / FORCE_UPDATE_<PLATFORM> — (unset = unrestricted) Client version negotiation for ios/android/web/macos/windows/linux clients; answered as X-Min-Client-Version, X-Latest-Client-Version, X-Update-Url, X-Update-Required on every response. Advisory only — the client enforces. An unparseable version never forces; FORCE_UPDATE_*=1 always does

Dovecot / doveadm / IMAP

Variable Default Consumers Description
DOVEADM_URL http://dovecot:24180 api, log_ingestor doveadm HTTP API (auth-cache flush)
DOVEADM_API_KEY — (empty = flushing disabled with a warning) dovecot, api, log_ingestor Key for the doveadm listener
IMAP_HOST / IMAP_PORT dovecot / 993 api, jmap, migration, storage_usage
IMAP_MASTER_USER / IMAP_MASTER_PASSWORD — jmap, api, storage_usage Dovecot master-user credentials (<mailbox>*<master> login)

SSL / cert_manager

Variable Default Description
ACME_EMAIL admin@localhost Let's Encrypt account email (ACME_EMAILS for a comma-separated account pool)
ACME_STAGING true (prod compose sets false) Staging CA toggle
SSL_CERT_PATH / SSL_KEY_PATH /etc/ssl/certs / /etc/ssl/private Directories where cert_manager deploys certs (not file paths)
MAIL_SSL_CERT_PATH / MAIL_SSL_KEY_PATH /etc/ssl/certs/custom / /etc/ssl/private/custom Path prefixes as seen from Postfix/Dovecot mounts (used inside SNI maps)
SNI_CONFIG_PATH /etc/ssl/sni Where the Postfix/Dovecot/Traefik SNI outputs are written
CERT_RENEWAL_DAYS / CERT_CHECK_INTERVAL 30 / 21600 Renewal threshold / check loop seconds
CERT_WORKER_THREADS / CERT_SERVER_IPS / MAX_DOMAINS_PER_CERT 3 / — / 100 Issuance tuning
TRAEFIK_ADMIN_SUBDOMAINS api,autoconfig,jmap,caldav,docs,grafana,traefik,console Admin hostnames to certify under $DOMAIN
TRAEFIK_EXTRA_HOSTNAMES — Extra fully-qualified hostnames to certify
WILDCARD_DOMAIN / DNS_PROVIDER / USE_WILDCARD_CERTS — / — / false DNS-01 wildcard issuance
DOCKER_RELOAD_ENABLED compose default true (code default false) SIGHUP Postfix/Dovecot after deploys
DOCKER_PROXY_URL / POSTFIX_CONTAINER / DOVECOT_CONTAINER http://docker-proxy:2375 / postfix / dovecot Reload plumbing

Postfix Overrides (read by its entrypoint)

POSTFIX_MYNETWORKS (default 172.25.0.0/16 appended), POSTFIX_DEV_MODE, POSTFIX_TLS_SECURITY_LEVEL, POSTFIX_SMTP_TLS_SECURITY_LEVEL, POSTFIX_TLS_AUTH_ONLY, POSTFIX_TLS_PROTOCOLS, POSTFIX_TLS_CIPHERS, POSTFIX_TLS_EXCLUDE_CIPHERS, POSTFIX_TLS_LOG_LEVEL, POSTFIX_TLS_CACHE_TIMEOUT, POSTFIX_CONNECTION_RATE_LIMIT, POSTFIX_CONNECTION_COUNT_LIMIT, POSTFIX_MESSAGE_RATE_LIMIT, POSTFIX_RECIPIENT_RATE_LIMIT, POSTFIX_RATE_TIME_UNIT, POSTFIX_SMTP_TIMEOUT, POSTFIX_HELO_TIMEOUT, POSTFIX_MAIL_TIMEOUT, POSTFIX_RCPT_TIMEOUT, POSTFIX_DATA_TIMEOUT, POSTFIX_SOFT_ERROR_LIMIT, POSTFIX_HARD_ERROR_LIMIT, POSTFIX_JUNK_COMMAND_LIMIT, POSTFIX_DNS_TIMEOUT.

Dead variables in .env.example

The POSTFIX_MYHOSTNAME, POSTFIX_MYDOMAIN, POSTFIX_VIRTUAL_*, POSTFIX_MESSAGE_SIZE_LIMIT, POSTFIX_CONTENT_FILTER, POSTFIX_HEADER_CHECKS, and DOVECOT_* blocks in .env.example are not read by any code — the entrypoints use the different names above (or bake the value). Likewise DEFAULT_TENANT_ID is set in compose but read by nothing (the code reads DEFAULT_ORGANIZATION_ID).

Tracking

Variable Default Description
TRACKING_ENABLED true Open/click tracking master switch (compose default true)
OPEN_TRACKING_ENABLED / CLICK_TRACKING_ENABLED true / true Per-feature toggles
TRACKING_DOMAIN falls back to HOSTNAME Domain for tracking URLs
TRACKING_BASE_URL compose: https://api.${DOMAIN} Explicit base URL for pixel/click links
TRACKING_SERVICE_URL http://tracking:8086 Used by the Postfix injector
BODY_CAPTURE_ENABLED / BODY_CAPTURE_MAX_BYTES / BODY_RETENTION_DAYS true / 262144 / 30 Outbound body capture into email_bodies
~30 more TRACKING_* tuning vars see worker/tracking/config.py Retention, rate limits, UA/geo capture toggles

Webhooks

Variable Default Description
WEBHOOK_URL / WEBHOOK_URLS — Global endpoint(s); compose maps WEBHOOK_URL=${WEBHOOK_URLS}
WEBHOOK_SECRET — HMAC-SHA256 signing secret (required by secrets-check)
WEBHOOK_TIMEOUT 15 (dispatcher; notification sender uses 30) Per-attempt timeout
WEBHOOK_MAX_RETRIES 7 (dispatcher; notification sender 3) Total attempts
WEBHOOK_WORKERS / WEBHOOK_QUEUE_SIZE 4 / 10000 Dispatcher threads / in-memory queue
WEBHOOK_SERVICE_URL http://webhooks:8081 Postfix bounce/webhook scripts
WEBHOOK_CLEANUP_* enabled, every 4 h, keep 24 h success / 72 h failure Delivery-log retention (webhooks service)
SERVICE_NAME unknown Sets the dispatcher's source; compose sets log_ingestor where needed

Log Ingestor & SMTP Credentials

Variable Default Description
POSTFIX_LOG_PATH /var/log/postfix/mail.log Log file to tail
INGESTOR_STATE_PATH /var/lib/log_ingestor/state Tail-position state
INGESTOR_POLL_SECONDS / INGESTOR_QID_CACHE 2 / 20000 Poll interval / queue-ID cache
INGESTOR_INTERNAL_RELAYS tracking-filter,webhook-filter Internal hops to skip
LAST_USED_THROTTLE_SECONDS 60 Throttle for smtp_credentials.last_used_at writes
AUTO_SUSPEND_ENABLED false Auto-suspend failing SMTP credentials
AUTO_SUSPEND_MIN_MESSAGES / AUTO_SUSPEND_FAILURE_RATIO / AUTO_SUSPEND_WINDOW_HOURS 20 / 0.5 / 1 Suspension thresholds

Rate Limiter

Service basics: RATE_LIMITER_HOST/RATE_LIMITER_PORT (0.0.0.0/8082), RATE_LIMITER_URL (http://rate_limiter:8082, Postfix policy client), RATE_LIMITER_TIMEOUT (5), RATE_LIMIT_CACHE_TTL (300), warning/critical/exceeded thresholds (80/95/100).

Default quotas are 36 vars of the form {ORG|DOMAIN|MAILBOX}_{INBOUND|OUTBOUND}_{SECOND|MINUTE|HOURLY|DAILY|MONTHLY|BURST}_DEFAULT — e.g. ORG_OUTBOUND_HOURLY_DEFAULT=10000, DOMAIN_OUTBOUND_HOURLY_DEFAULT=2000, MAILBOX_OUTBOUND_HOURLY_DEFAULT=200. See worker/rate_limiter/config.py for the full matrix; per-SMTP-credential limits live on the credential row, not in env.

Storage / Archive / DR

Variable Default Description
AWS_ACCESS_KEY_ID / AWS_SECRET_ACCESS_KEY / AWS_DEFAULT_REGION / AWS_BUCKET / AWS_S3_PREFIX — / — / eu-west-2 / development-local-1 / mailyte S3 for attachments/exports (api, storage_usage)
ARCHIVE_S3_BUCKET / ARCHIVE_S3_PREFIX mailyte-mail-archive / mail-archive Archiver's bucket (separate credentials via ARCHIVE_AWS_* in compose)
S3_ENDPOINT_URL — Non-AWS S3 endpoints
ARCHIVE_SPOOL_PATH / ARCHIVE_SPOOL_DRAIN_SECONDS /app/storage/archive-spool / 300 Local spool when S3 is down
DR_AGE_RECIPIENT / ARCHIVE_AGE_RECIPIENT / ARCHIVE_AGE_IDENTITY_FILE — / — / /run/secrets/archive_age_identity age encryption for archives/backups
ARCHIVE_SERVICE_URL / ARCHIVE_TIMEOUT / ARCHIVE_OUTBOUND_ENABLED http://archiver:8083 / 3 / true Sieve/injector archive hooks
DEFAULT_RETENTION_DAYS 2555 Archive retention (7 years)
BACKUP_EXPECTED_HOSTS — Hosts the backup-freshness gauges must cover
MAIL_DATA_PATH / ATTACHMENT_PATH /var/mail/vhosts / /var/attachments storage_usage scan roots

Security Services & Misc Secrets

Variable Default Description
OAUTH_TOKEN_SECRET / OAUTH_ISSUER — (weak default rejected) / https://auth.mailyte.com OAuth service
URL_HMAC_SECRET / SAFE_LINK_BASE_URL — / https://safe.mailyte.com URL protection
TOTP_ISSUER Mailyte TOTP label
GEOIP_DB_PATH /usr/share/GeoIP/GeoLite2-Country.mmdb geo_blocking
TRANSPORT_RULES_ENABLED false Rspamd transport-rules Lua module
GRAFANA_ADMIN_USER / GRAFANA_ADMIN_PASSWORD admin / — (required) Grafana login
MAX_AUTH_FAILURES_PER_IP / MAX_AUTH_FAILURES_PER_USER / AUTH_FAILURE_WINDOW_SECS / AUTH_BLOCK_DURATION_SECS 10 / 5 / 900 / 3600 Dovecot auth policy
Variable Default Description
QDRANT_HOST / QDRANT_PORT localhost in code; compose sets qdrant / 6333 Vector DB
EMBEDDING_PROVIDER / EMBEDDING_MODEL_NAME sentence_transformers / all-MiniLM-L6-v2 Local embeddings by default
OPENAI_API_KEY — Only needed when EMBEDDING_PROVIDER=openai
RAG_PORT / RAG_HOST 8090 / 0.0.0.0
60+ further RAG_* / EMBEDDING_* / CHUNK_* / SEARCH_* vars see worker/rag/config.py Chunking, indexing, caching, tenancy

Logging & Runtime Mode

Variable Default Description
LOG_LEVEL INFO Per-service log level
FLASK_ENV / ENVIRONMENT development production marks prod mode
FLASK_DEBUG / DEBUG 0
MAILYTE_LOG_DIR logs Root for GET /platform/logs
COMPOSE_PROJECT_NAME mailyte-prod (from .env) Pins the compose project name across deploy directories — do not remove

Removed / Nonexistent Variables

These appeared in older docs but are read by nothing: API_HOST, API_PORT, DB_READ_HOST (reader module is unimported), TRACKING_WORKERS, ANALYTICS_WORKERS, QUEUE_MANAGER_WORKERS, GRAFANA_PASSWORD, PROMETHEUS_RETENTION (retention is the --storage.tsdb.retention.time=30d flag in compose), CLOUD_PROVIDER, AWS_S3_BUCKET, DEFAULT_TENANT_ID.

Example .env

Generate it rather than writing it by hand:

Bash
./scripts/generate-secrets.sh   # writes a complete .env with strong secrets

Then set the identity values:

Bash
COMPOSE_PROJECT_NAME=mailyte-prod
HOSTNAME=mail.yourdomain.com
DOMAIN=yourdomain.com
ADMIN_EMAIL=admin@yourdomain.com
ACME_EMAIL=admin@yourdomain.com
ACME_STAGING=false
MAIL_HOSTNAME=mail.yourdomain.com
DOVEADM_API_KEY=<random string — enables SMTP-credential cache flushing>
WEBHOOK_URLS=https://your-app.com/webhooks/mailyte