No description
  • TypeScript 93.8%
  • Shell 6.2%
Find a file
hicham 09e1661016
All checks were successful
/ check (push) Successful in 21s
mailu: mount PHP2Shell mitigations into webmail
CVE-2026-109618/109619 are fixed in PHP 8.3.36, but the 2024.06 webmail
image is built on Alpine 3.20, which is EOL and stays at php83 8.3.15,
and Mailu said the stable branch will not be patched. Until a release
on a newer Alpine exists, apply the advisory's interim measures:
session.upload_progress off (the validated FPM path) and no memory_limit
(the OOM path). post_max_size stays at 31M. Unauthenticated requests
never reach PHP (front sends them to the SSO login), so this covers
logged-in users.
2026-10-22 11:31:59 +02:00
.forgejo/workflows proc: stream a subprocess to and from files 2026-10-07 15:55:37 +02:00
coolify mailu: mount PHP2Shell mitigations into webmail 2026-10-22 11:31:59 +02:00
docs ovh: a rejected token is reported, not read as a missing zone 2026-10-09 17:22:51 +02:00
kinds/wordpress release: ask for the newest release instead of a rolling "latest" tag 2026-09-11 11:46:27 +02:00
src ovh: a rejected token is reported, not read as a missing zone 2026-10-09 17:22:51 +02:00
tests ovh: a rejected token is reported, not read as a missing zone 2026-10-09 17:22:51 +02:00
.gitignore backup, kuma-setup and deploy; the python tools are gone 2026-09-11 10:41:30 +02:00
deno.json import: find the prefix past the plugin tables that sort first 2026-10-07 21:16:31 +02:00
deno.lock import: find the prefix past the plugin tables that sort first 2026-10-07 21:16:31 +02:00
install-coolify.sh tidy the day's changes 2026-09-11 13:36:51 +02:00
install.sh drop the bin/ shims; deno task dev runs from source 2026-09-11 13:50:28 +02:00
README.md docs: ssh on the DreamHost side, and --takeover 2026-10-08 15:50:35 +02:00
stack.env.example drop the bin/ shims; deno task dev runs from source 2026-09-11 13:50:28 +02:00

wp-mail-stack

Coolify-based stack for a single VPS hosting several sites and full email. One command per site:

hostess add example.com

A site is two independent choices: what runs on the domain (its kind) and what happens to its mail (its mail mode). Everything else — DNS, TLS, the mailbox, the dashboard tile, the nightly backup — follows from those.

Component Role URL
Coolify Control plane: deploys, per-site DBs, TLS, dashboard coolify.<primary>
Coolify proxy Traefik v3 on 80/443 (+ optional ovh DNS-01 resolver) —
Mailu 2024.06 SMTP/IMAP, admin UI, webmail — standalone compose mail.<primary>
Homepage Landing page with a tile per site home.<primary>
Uptime Kuma Monitoring + mail alerts uptime.<primary>
hostess The CLI below — one binary, installed by the installer —

Site kinds

hostess kinds
Kind What it is Default mail
wordpress WordPress + its own MariaDB, installed and wired to SMTP full
static A git repo built by Coolify, redeployed on push none
hostess add example.com                                    # wordpress, takes the domain's mail over
hostess add example.com --mail=send                        # ... mail keeps being delivered elsewhere
hostess add example.com --mail=none                        # ... with no mail at all
hostess add site.example.com --kind=static --repo=<git url>

Mail modes

Mode Mailbox + DKIM DNS written
full yes MX, SPF, DMARC p=quarantine, DKIM — the domain's mail moves here
send yes DKIM, and SPF merged into the existing record; no MX, existing DMARC untouched
none no nothing

send is for a domain whose mailboxes live somewhere else already: the site still sends as noreply@<domain> through Mailu, inbound mail keeps going wherever it goes today.

From zero to first site

1. Order a VPS. Any Debian-ish box, ≥4 GB RAM, public IPv4. Critical at the provider: outbound port 25 must be unblocked (most providers block it by default — request it, or mail will never leave the box). Create a regular user with passwordless sudo, install Docker Engine + the compose plugin, add the user to the docker group.

2. Order a domain. This becomes the primary domain — it names the server itself (mail., coolify., home., uptime. live under it) and hosts the admin/alert mailboxes. Buy it at OVH or DreamHost if you want hostess to manage DNS automatically (see Operations); any registrar works with manual records.

3. Point DNS at the VPS. The installer creates the records (mail, coolify, home, uptime — A plus AAAA where the VPS has IPv6, except the mail host which stays v4-only) itself when the zone is at a configured provider, and tells you exactly what to paste otherwise. The one thing only you can do: at the VPS provider, set the IP's PTR (reverse DNS) to mail.<primary> — non-negotiable for deliverability (the installer checks and waits).

4. Bootstrap. Guided, curl|bash-able — checks what it can (docker, DNS, PTR, port 25), instructs and waits where it can't:

curl -fsSL https://git.hicham.party/hicham/wp-mail-stack/raw/branch/main/install.sh | bash

(equivalent manual route: clone to ~/srv, fill stack.env, install the hostess binary, run ./install-coolify.sh)

Idempotent; on the first run it prompts for three passwords (Coolify root, mail admin, Kuma admin), remembers the ones it needs again, and does the rest: Coolify + API token + instance domain + SMTP, firewall (dashboard port 8000 blocked externally), proxy customization, Mailu with admin/noreply mailboxes, Homepage, Uptime Kuma with monitors + mail alerts, backup cron.

5. Primary-domain mail records.

hostess dns mail <primary> --apply

Then send a message to https://www.mail-tester.com — aim ≥ 9/10. Expect spam-folder placement at Gmail/Outlook for the first weeks regardless: a fresh domain + fresh IP need warm-up (send real mail, have recipients reply / mark not-spam).

6. First site.

hostess add example.com

DNS, the Mailu domain + noreply@ mailbox + DKIM key, the Coolify resource, the wait for HTTPS, the WordPress install and the WP Mail SMTP plugin. Prints every credential it generated.

Day to day

hostess list                          # per site: kind, coolify status, DNS, HTTPS, mail, DKIM
hostess kinds                         # what a site can be
hostess add <domain> [--kind=] [--mail=] [--import= [--takeover]] [--dry-run]
hostess rm <domain> [--purge-mail]    # mail is kept unless you say otherwise
hostess takeover <domain>             # move a domain here from another DNS host
hostess import <domain> <source>      # pull a running WordPress site in; --apply to write, --takeover to move DNS too
hostess dns <a|site|mail|rm> <domain> # prints a plan; add --apply to publish it
hostess backup                        # what the nightly cron runs

Two habits worth keeping:

  • --dry-run before anything that matters. It walks the whole flow — every record, API call and command — and changes nothing.
  • hostess dns never writes without --apply. Reading what a zone would become is free.

Deploys, logs, restarts, env vars and DB backups live in the Coolify UI. Mailboxes and aliases live in the Mailu admin at mail.<primary>/admin. Site tiles and cert renewals are automatic. Alerts (site down, cert expiry, deploy failure) land in ALERT_EMAIL.

Layout

<repo root>/  (e.g. ~/srv)
├── stack.env            single config point (domain, emails, DNS creds)
├── install.sh           guided curl|bash entrypoint (do-nothing style)
├── install-coolify.sh   stack bootstrap, idempotent (called by install.sh)
├── src/                 the hostess CLI (typescript, compiled by CI)
├── kinds/               per-kind assets — wordpress/compose.yml
├── coolify/             stack infrastructure: proxy, mailu, homepage
├── bin/                 compatibility shims onto the hostess binary
├── mailu/  homepage/  backups/     runtime state (gitignored)
└── docs/                OPERATIONS.md, DESIGN.md

Further reading

  • docs/OPERATIONS.md — credentials and how to reset each one, DNS providers and their quirks, backups and restore, the gotchas that cost real debugging, and how to add a new site kind.
  • docs/DESIGN.md — why the stack is shaped the way it is: the do-nothing philosophy, why Mailu is not a Coolify resource, why SMTP is 465-only, why the CLI is a compiled binary.

Developing

deno task ci      # fmt --check, lint, check, test
deno task build   # dist/hostess
deno task dev list      # run from source, same permissions as the build

STACK_ROOT=/path/to/checkout points the CLI at a stack other than ~/srv.