# LambdaMail - environment # # One file for local development and for production. # # scripts/init-env.sh copy this to .env and generate the secrets # scripts/init-env.sh --production the same, plus a list of what you still # have to set before this is a real server # # Values below are the local-development defaults. Lines marked PRODUCTION say # what a real deployment needs. In Coolify, paste the finished .env into the # environment editor instead of keeping a file on the host. # # The secrets are left empty on purpose. The script fills them with random # values, and compose refuses to start while one is empty, because a default # secret in a published repository is not a secret. # --- Container images ------------------------------------------------------- # Published to the Gitea registry by .gitea/workflows/release.yml for # linux/amd64 and linux/arm64. The deploy host pulls instead of compiling: # `docker compose pull && docker compose up -d`. `make up` builds from source # through the build overlay and ignores these. # PRODUCTION: pin IMAGE_TAG to a release (1.0.0) or to a sha- tag. Never # leave `latest` on a server: a redeploy then silently changes what runs. IMAGE_NAMESPACE=git.lucasrguerra.dev.br/lucasrguerra/lambdamail IMAGE_TAG=latest # --- Identity --------------------------------------------------------------- # MAIL_DOMAIN is the domain you send and receive as. # PRIMARY_MAIL_HOST is the host the MX record points at; its PTR must match it. # PRODUCTION: MAIL_DOMAIN=example.com PRIMARY_MAIL_HOST=mail.example.com # WEBMAIL_HOST=mail.example.com PUBLIC_IPV4= MAIL_DOMAIN=localhost PRIMARY_MAIL_HOST=mail.localhost WEBMAIL_HOST=localhost:3000 # Optional separate host for /admin. ADMIN_HOST= PUBLIC_IPV4=127.0.0.1 # Leave empty unless IPv6 has a matching PTR and is covered by SPF. A half # configured IPv6 is a common cause of rejection (PLAN.md premises). PUBLIC_IPV6= # --- Secrets ---------------------------------------------------------------- # Filled in by scripts/init-env.sh. To do it by hand: # # POSTGRES_PASSWORD, REDIS_PASSWORD openssl rand -hex 24 # JWT_SECRET, LAMBDAMAIL_MASTER_KEY openssl rand -hex 32 # # The first two are substituted into connection URIs (postgres://user:PASS@db/... # and redis://:PASS@redis:6379), so they must hold no character that means # something in a URL: use hex, NOT `openssl rand -base64`, whose alphabet has "/" # and "+". A "/" ends the authority component, the host is parsed as part of the # password and every connection fails authentication - which shows up as the # migrations container exiting 1 while Postgres itself reports healthy. POSTGRES_DB=lambdamail POSTGRES_USER=lambdamail POSTGRES_PASSWORD= REDIS_PASSWORD= # Signs webmail and admin sessions. At least 32 characters; the auth service # refuses to start below that. JWT_SECRET= # Encrypts DKIM private keys, TOTP secrets and provider tokens at rest. # LOSING THIS MAKES EVERY STORED SECRET UNRECOVERABLE: back it up separately # from the database (PLAN.md risk R9). LAMBDAMAIL_MASTER_KEY= # --- DNS automation (Cloudflare) -------------------------------------------- # The token needs Zone.DNS:Edit plus Zone.Zone:Read on the zone. Leave it empty # to manage the records by hand; the service then logs that DNS sync is off. CLOUDFLARE_API_TOKEN= DNS_SYNC_INTERVAL_SECONDS=21600 # --- TLS -------------------------------------------------------------------- # traefik: read the certificate the proxy already manages (default; DANE is not # possible in this mode). # acme: obtain the certificate here via DNS-01. Required for DANE, because # only this mode can reuse a pre-generated key. TLS_MODE=traefik # Used only when TLS_MODE=acme. Leave ACME_DIRECTORY_URL empty for Let's Encrypt # production; point it at the staging directory while testing, since production # rate limits can lock a domain out for a week. ACME_EMAIL= ACME_DIRECTORY_URL= # Where the proxy keeps its certificates, and the Docker network it listens on. # PRODUCTION (Coolify): COOLIFY_PROXY_DIR=/data/coolify/proxy COOLIFY_PROXY_DIR=./local/acme COOLIFY_PROXY_NETWORK=coolify TRAEFIK_ACME_FILENAME=acme.json CERT_POLL_INTERVAL_SECONDS=60 # Static certificate files, used when no ACME store is available (local dev; # `make gen-dev-cert` writes them). PROTOCOLS_CERT_PATH=/etc/lambdamail/certs/cert.pem PROTOCOLS_KEY_PATH=/etc/lambdamail/certs/key.pem PROTOCOLS_SPOOL_DIR=/var/lambdamail/spool # --- Transport security ----------------------------------------------------- # Start at "testing": senders honour the policy but do not hard-fail on it. # Promote to "enforce" only once TLS-RPT reports come back clean; enforce with a # broken certificate silently blackholes inbound mail. MTA_STS_MODE=testing OUTBOUND_MTA_STS_ENABLED=true # DANE stays off unless TLS_MODE=acme. Under proxy-managed certificates the key # changes at every renewal and the published TLSA record stops matching, which # permanently rejects mail at every validating MTA (PLAN.md section 5.1). OUTBOUND_DANE_ENABLED=false DNSSEC_RESOLVER=1.1.1.1:53 MAX_MESSAGE_BYTES=52428800 # ARC seals accepted messages so a downstream receiver can still tell that the # mail authenticated at the first hop. Only worth enabling on a host that # forwards mail onward. ARC_SEAL_ENABLED=false # --- Smarthost (hosts where outbound port 25 is blocked) -------------------- # Most VPS providers block port 25. If `make preflight` reports it blocked, set a # relay here: all delivery goes through it and it is added to the published SPF, # without which every relayed message fails SPF. RELAY_HOST= RELAY_PORT=587 RELAY_USER= RELAY_PASS= # The SPF term that authorises the relay, exactly as the provider documents it. # It cannot be derived from RELAY_HOST, and getting it wrong is worse than # leaving it out. Brevo's relay is smtp-relay.brevo.com, but brevo.com publishes # the SPF of their corporate mail: the sending ranges are at spf.brevo.com, so # the value there is "include:spf.brevo.com". RELAY_SPF_INCLUDE= # --- Storage ---------------------------------------------------------------- STORAGE_DRIVER=local S3_ENDPOINT= S3_BUCKET= S3_ACCESS_KEY= S3_SECRET_KEY=