Lucas RayanandClaude Opus 5 f7e93275d5 #22: record why a deployment must pin an immutable image tag
The first deploy of the new interface reported success and served the
previous build: docker compose does not re-pull a tag it already has
locally, so a deployment sitting on "latest" keeps running whatever it
pulled the first time. Nothing in the output says so - the CSS was simply
the old one.

Pinning IMAGE_TAG to the sha- tag is what forces the pull, and it is what
makes a rollback possible at all. The deployment guide now says so, with
the reason.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-02 16:28:26 -03:00
2026-07-23 02:18:24 -03:00

LambdaMail

A self-hosted mail server with an integrated webmail and admin console, built to run on Coolify from a single docker-compose.yaml.

SMTP, IMAP, POP3 and ManageSieve, with SPF, DKIM, DMARC, ARC, MTA-STS and TLS-RPT, DNS automation through Cloudflare, and spam and virus filtering. No paid tiers and no artificial limits on domains or mailboxes.

Status: pre-release. The mail path, the security posture and the web interfaces work end to end, and there is an automated test suite behind them. Several console panels are still read-only, and some features are recorded but not yet acted on — see What is not finished before putting real mail through it.


What it does

Area
Protocols SMTP MX (25), Submission (587 STARTTLS, 465 implicit TLS), IMAP (143/993) with UID, flags, IDLE and CONDSTORE, POP3 (110/995), ManageSieve (4190)
Authentication SPF, DKIM (Ed25519 + RSA dual signature), DMARC, ARC sealing, DANE/TLSA and MTA-STS on delivery, TLS-RPT ingestion
Filtering Rspamd scoring with greylisting, RBLs and rate limiting; ClamAV; Sieve
Delivery Durable outbound queue in Postgres with the retry ladder and RFC 3464 bounces; smarthost fallback for hosts with port 25 blocked
DNS Cloudflare reconciliation of the 13 records, idempotent, with drift detection
Web Webmail at /user/* and an admin console at /admin/*, isolated by session audience, with TOTP two-factor, recovery codes, app passwords, and English, Portuguese and Spanish

Architecture

Two runtimes, split by what each is good at:

  • protocols (Go) — every protocol listener, the delivery worker, TLS certificate handling, DNS reconciliation, and the webmail's mail API. It owns folder, UID, flag and blob semantics.
  • auth (TypeScript) — logins, sessions, two-factor, account management and the admin console API.
  • web (Next.js) — both web surfaces, and the proxy that gives the browser a single origin.
  • storage (TypeScript) — health only today.

Postgres is the source of truth for everything, including the outbound queue. Redis is a cache and dispatch index; losing it loses no mail.

                    ┌──────────┐
  browser ─────────▶│   web    │──┬─▶ auth      (sessions, accounts, admin)
                    └──────────┘  └─▶ protocols (mail read/send, DKIM, TLS)
                                        │
  internet ──25/587/465/143/993──────────┘
                                        │
                              Postgres ─┴─ Redis ─ Rspamd ─ ClamAV

Deploying

For a real VPS, follow docs/DEPLOYMENT.md. It covers the three things no code can do for you — reverse DNS, the DNSSEC delegation and getting port 25 unblocked — and the order the rest has to happen in.

The short version:

cp .env.production.example .env      # then replace every CHANGE
make preflight                       # checks port 25, PTR, RBLs, token scope
docker compose up -d
make create-admin EMAIL=you@example.com PASSWORD='...'

make create-admin is not optional: a fresh install has no mailbox, and every route into the console needs one.

Running locally

cp .env.example .env
make gen-dev-cert                    # self-signed certificate for the listeners
make up                              # builds from source via the build overlay
make seed                            # a postmaster@example.test fixture

The webmail is on http://localhost:3000. The seeded password is dev-password-only — it is a fixture, not a credential.

Testing

make test        # Go suite, auth and webmail
make lint        # go vet, tsc, and the language lint

The Go and auth suites include integration tests that need Postgres. They skip without TEST_DATABASE_URL and run against a real database in CI, which is where the security regressions are enforced — wrong password, wrong TOTP code, session revocation, surface isolation, cross-mailbox reads.

Configuration

Every variable is documented inline in .env.production.example. The ones without a safe default:

Variable
JWT_SECRET Signs sessions. At least 32 characters; the service refuses to start below that rather than fall back to a constant.
LAMBDAMAIL_MASTER_KEY Encrypts DKIM keys, TOTP secrets and provider tokens. Back it up separately from the database — losing it makes every stored secret unrecoverable.
POSTGRES_PASSWORD, REDIS_PASSWORD No defaults; compose fails without them.
MAIL_DOMAIN, PRIMARY_MAIL_HOST, PUBLIC_IPV4 Identity. PRIMARY_MAIL_HOST must match the PTR of PUBLIC_IPV4.

Operating

make preflight Port 25 egress, PTR and FCrDNS, RBL listings, Cloudflare token scope, certificate presence
make create-admin EMAIL=… PASSWORD='…' First administrator on a fresh install
make reset-password EMAIL=… PASSWORD='…' Recovers a locked-out account and signs out its sessions
docker compose logs -f protocols Delivery, TLS and DNS reconciliation
GET /health on protocols 200 healthy, 200 with X-LambdaMail-Health: degraded when a certificate needs attention, 503 when a dependency is down

docs/OPERATIONS.md covers backups, certificate rotation, what the alerts mean and how to read a delivery failure.

What is not finished

Stated plainly, because a mail server that half works loses mail:

  • Recorded but not acted on. Vacation responder, signature and the Sieve rules built in the UI are stored, but nothing consumes them yet — enabling a vacation reply does not send one.
  • Read-only console panels. The guided domain onboarding checklist, the DNS desired-vs-actual diff and the CSV import report on state rather than driving it.
  • Not implemented. JMAP, BIMI, CalDAV/CardDAV, multi-node. Backups are documented but not automated.
  • DANE is off unless TLS_MODE=acme, and deliberately so: under a Traefik-managed certificate the key changes at every renewal and the published TLSA record stops matching, which permanently rejects mail at every validating MTA.

Contributing

  • All code, comments and identifiers are in English. make lint enforces it.
  • Tests come with the change that needs them; the suites above must stay green.
  • Commits reference their issue: #N: what changed and why.

License

MIT — see LICENSE.

S
Description
Your own mail server, with a webmail that is a pleasure to use.
Readme MIT
3.5 MiB
0 Stars 1 Watchers 0 Forks
2026-10-30 15:26:52 +00:00
Languages
Go 60.5%
TypeScript 37.2%
JavaScript 0.9%
CSS 0.4%
Dockerfile 0.3%
Other 0.7%