Three things stood between this and a real deployment. A missing JWT_SECRET fell back to a constant that is published in this repository, so any deployment that forgot the variable would have accepted session tokens anyone could mint, including an admin one. The auth service now refuses to start without a secret of at least 32 characters. The test suites set it through vitest config, because ESM hoists imports and an assignment inside a test file runs too late. Session cookies were issued without the Secure attribute: it is driven by NODE_ENV, which compose never set. Behind a TLS terminator that is a downgrade waiting to happen. Nothing created the first mailbox. A fresh install has no domain and no account, and every route into the console needs one - so there was no first login. There is now a create-admin command, which also provisions the standard folders and the four system aliases. It is a deliberate command rather than a bootstrap on boot: creating an administrator as a side effect of starting a container is how a deleted account quietly comes back. Also: the production images were shipping compiled test files, since the build compiled everything under src. A separate build config excludes them while lint still type-checks them. Documentation. README.md covers what this is, how it is put together, and what is not finished - stated plainly, because a mail server that half works loses mail. docs/DEPLOYMENT.md is the Coolify path in the order it has to happen, starting with the three prerequisites no code can satisfy: outbound port 25, the PTR record, and the DNSSEC delegation. docs/OPERATIONS.md is the runbook - health states, certificates, backups, reading a delivery failure, DKIM rotation, and an explicit list of what is not automated. .env.production.example replaces guessing which of the dev defaults are safe: none of them are. docs/ was gitignored as a place for internal notes; only docs/internal/ is now, so these guides ship with the code. Every command the documentation tells an operator to run was executed against a running stack: create-admin, preflight, the health endpoint, the published mail ports, and the Secure cookie. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
87 lines
3.7 KiB
Bash
87 lines
3.7 KiB
Bash
# LambdaMail - production environment
|
|
#
|
|
# Copy this into Coolify's environment editor, or to .env on the host, and
|
|
# replace every value marked CHANGE. The stack refuses to start without the
|
|
# required ones rather than falling back to a default, because a default
|
|
# secret in a published repository is not a secret.
|
|
#
|
|
# Generate each secret with: openssl rand -base64 36
|
|
|
|
# --- Container images -------------------------------------------------------
|
|
# Published by .github/workflows/release.yml for linux/amd64 and linux/arm64.
|
|
# Pin IMAGE_TAG to a released version or a sha- tag to roll back.
|
|
IMAGE_NAMESPACE=ghcr.io/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.
|
|
MAIL_DOMAIN=example.com # CHANGE
|
|
PRIMARY_MAIL_HOST=mail.example.com # CHANGE
|
|
WEBMAIL_HOST=mail.example.com # CHANGE - where the web UI is served
|
|
ADMIN_HOST= # optional separate host for /admin
|
|
PUBLIC_IPV4=203.0.113.10 # CHANGE - the VPS public IPv4
|
|
# Leave empty unless IPv6 has a matching PTR and is covered by SPF. Half
|
|
# configured IPv6 is a common cause of rejection (PLAN.md premises).
|
|
PUBLIC_IPV6=
|
|
|
|
# --- Secrets ----------------------------------------------------------------
|
|
POSTGRES_DB=lambdamail
|
|
POSTGRES_USER=lambdamail
|
|
POSTGRES_PASSWORD=CHANGE-generate-with-openssl-rand
|
|
REDIS_PASSWORD=CHANGE-generate-with-openssl-rand
|
|
# Signs webmail and admin sessions. Minimum 32 characters; the auth service
|
|
# refuses to start below that.
|
|
JWT_SECRET=CHANGE-generate-with-openssl-rand-at-least-32-chars
|
|
# 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=CHANGE-generate-with-openssl-rand-at-least-32-chars
|
|
|
|
# --- DNS automation ---------------------------------------------------------
|
|
# Token scope: Zone.DNS:Edit plus Zone.Zone:Read on the zone. Leave empty to
|
|
# manage the 13 records by hand; the service then logs that sync is off.
|
|
CLOUDFLARE_API_TOKEN=
|
|
DNS_SYNC_INTERVAL_SECONDS=21600
|
|
|
|
# --- TLS --------------------------------------------------------------------
|
|
# traefik: read the certificate Coolify's proxy already manages (default).
|
|
# acme: obtain it here via DNS-01. Required for DANE, and only then.
|
|
TLS_MODE=traefik
|
|
COOLIFY_PROXY_DIR=/data/coolify/proxy
|
|
COOLIFY_PROXY_NETWORK=coolify
|
|
TRAEFIK_ACME_FILENAME=acme.json
|
|
CERT_POLL_INTERVAL_SECONDS=60
|
|
PROTOCOLS_SPOOL_DIR=/var/lambdamail/spool
|
|
# Used only when TLS_MODE=acme.
|
|
ACME_EMAIL=
|
|
ACME_DIRECTORY_URL=
|
|
|
|
# --- Transport security -----------------------------------------------------
|
|
# Start at testing. 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 under TLS_MODE=traefik. 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_SEAL_ENABLED=false
|
|
|
|
# --- Smarthost --------------------------------------------------------------
|
|
# Most VPS providers block outbound port 25. If the preflight reports it
|
|
# blocked, set a relay here; it is added to the published SPF automatically.
|
|
RELAY_HOST=
|
|
RELAY_PORT=587
|
|
RELAY_USER=
|
|
RELAY_PASS=
|
|
|
|
# --- Storage ----------------------------------------------------------------
|
|
STORAGE_DRIVER=local
|
|
S3_ENDPOINT=
|
|
S3_BUCKET=
|
|
S3_ACCESS_KEY=
|
|
S3_SECRET_KEY=
|