Files
DOCKERMAILSERVER-LXC/README.md
T
karim 1d3818e725 docker-mailserver LXC für Proxmox: Stack + Admin-UI + Webmail + Hardening
- dms-lxc.sh: Proxmox-Host-Installer (unprivilegierter LXC, Debian 13, Docker),
  curl-Self-Download, Multi-Domain-DKIM, SnappyMail-Provisionierung, PVE-Firewall
- Stack: docker-mailserver, Node-Admin-API (Supabase-Auth), React-Admin-UI
  (OPENBUREAU-Look), SnappyMail (Shibui-Theme), Rspamd-Web-UI, docker-socket-proxy
- Admin: Postfächer/Aliase/Catch-all/Quota, editierbare Domains+Settings,
  Server (Quota/Queue über abgesicherte Bridge), Status & DNS
- Hardening: no-new-privileges, Whitelisted exec-Bridge, Rspamd-Passwort,
  .env chmod 600, PVE-CT-Firewall, generisch/teilbar (keine festen Domains)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-02 02:26:28 +02:00

15 KiB
Raw Blame History

docker-mailserver auf Proxmox (LXC) — mit React-Admin UI & SnappyMail

Ein vollständiges, selbst gehostetes Mail-Setup für ein kleines Büro (45 Personen):

Komponente Aufgabe
docker-mailserver (DMS) Postfix + Dovecot + Rspamd (SMTP/IMAP, Spam, DKIM)
Admin-API (Node.js) verwaltet Postfächer/Aliase/Quotas über die DMS-Config-Dateien — Auth via Supabase
Admin-UI (React-Admin) Weboberfläche im Stil von OPENBUREAU
SnappyMail schlankes Webmail für die Mitarbeiter
Nginx Proxy Manager (bereits vorhanden) HTTPS/Let's-Encrypt für Admin-UI & Webmail

Das Ganze läuft in einem unprivilegierten LXC-Container (Debian 13) auf Proxmox, in dem Docker mit nesting=1 betrieben wird.

                    Internet
                       │
   Port 25/465/587/    │   Port 80/443
   143/993  ──────►    ▼   ──────► Nginx Proxy Manager (HTTPS)
   direkt zum DMS                       │
   (Mail-TLS im DMS)                    ├─► admin.example.com → Admin-UI  (:8080)
                                        └─► mail.example.com  → SnappyMail (:8888)

1. Voraussetzungen

  • Proxmox VE (8.x), du arbeitest als root auf dem Host.
  • Eine Domain (example.com) mit Zugriff auf die DNS-Einstellungen.
  • Port 25 ausgehend/eingehend beim Hoster/ISP freigegeben (viele Privatanschlüsse blockieren 25!) und eine statische öffentliche IP mit der Möglichkeit, einen PTR/rDNS zu setzen.
  • Eine Supabase-Instanz (dieselbe wie bei OPENBUREAU) für das Admin-Login.
  • Der bereits vorhandene Nginx Proxy Manager.

2. Installation

Variante A — Einzeiler (auf dem Proxmox-Host als root). Das Skript lädt den stack/ bei Bedarf selbst herunter:

bash <(curl -fsSL https://git.kgva.ch/karim/DOCKERMAILSERVER-LXC/raw/branch/main/dms-lxc.sh)
# privates Repo:  GIT_TOKEN=<token> bash <(curl -fsSL .../dms-lxc.sh)

Variante B — Ordner kopieren (inkl. stack/) und ausführen:

cd /root/dms && bash dms-lxc.sh

Das Skript fragt alles interaktiv ab (Domains, Webmail-/Admin-Domain, Brand, Supabase, NPM-IP …) — oder per ENV vorbelegen:

CTID=110 \
MAIL_FQDN=mail.example.com  MAIL_DOMAIN=example.com  MAIL_DOMAINS="example.com weitere.tld" \
BRAND="Mein Büro"  WEBMAIL_FQDN=mail.example.com  ADMIN_FQDN=admin.example.com \
NET_IP=192.168.1.50/24 NET_GW=192.168.1.1 \
NPM_IP=192.168.1.10 \
ADMIN_ALLOWED_EMAILS="admin@example.com" \
SUPABASE_URL=https://xxxx.supabase.co  SUPABASE_ANON_KEY=eyJ... \
bash dms-lxc.sh

Generisch & teilbar: Es sind keine festen Domains im Stack — alles kommt aus dem Dialog und ist später in der Admin-UI editierbar. Du kannst das Skript also weitergeben.

Es macht automatisch:

  1. unprivilegierten LXC (Debian 13, nesting=1,keyctl=1) anlegen + PVE-CT-Firewall,
  2. Docker installieren,
  3. Self-signed-Cert + erstes Postfach vor-seeden, dann den Stack bauen & starten,
  4. DKIM je Domain erzeugen, SnappyMail provisionieren (Domain→mailserver, Shibui-Theme),
  5. am Ende DNS-Records (alle Domains, inkl. DKIM) + Rspamd-Passwort + Hardening-Checkliste ausgeben.

Wichtige Variablen (oben im Skript / per ENV): CORES, RAM_MB (4096), DISK_GB (20), STORAGE, BRIDGE, ADMIN_PORT/WEBMAIL_PORT/RSPAMD_PORT, NPM_IP, HARDEN_FIREWALL, ENABLE_CLAMAV.

Verwaltung später:

pct enter 110
cd /opt/dms-stack
docker compose ps
docker compose logs -f mailserver

3. DNS einrichten

Ein Mailserver, mehrere Domains: Es gibt einen Mailhost (mail.kgva.ch), der die Postfächer aller Domains bedient (kgva.ch, gabrielevarano.ch, karimgabrielevarano.xyz, openbureau.ch …). Alle Clients/Webmail verbinden sich immer zu mail.kgva.ch — egal welche Adress-Domain.

Einmalig für den Mailhost (<IP> = öffentliche IP):

Typ Name Wert
A mail.kgva.ch. <IP>
PTR/rDNS <IP> mail.kgva.chbeim Hoster/ISP setzen!

Pro Mail-Domain (Beispiel kgva.ch — analog für jede weitere Domain):

Typ Name Wert
MX kgva.ch. 10 mail.kgva.ch.
TXT (SPF) kgva.ch. v=spf1 mx ~all
TXT (DMARC) _dmarc.kgva.ch. v=DMARC1; p=quarantine; rua=mailto:postmaster@kgva.ch
TXT (DKIM) je Domain eigener Key — siehe Skript-Ausgabe / Admin-UI → Status & DNS (langer Public-Key)

Jede Domain hat einen eigenen DKIM-Schlüssel. Die fertigen Records (alle Domains) findest du nach dem Setup in der Admin-UI unter Status & DNS zum Kopieren — oder per Skript-Ausgabe. Weitere Domain später hinzufügen: MAIL_DOMAINS in .env ergänzen, Konto anlegen, docker exec mailserver setup config dkim domain neue-domain.tld, DNS setzen.

Der PTR-Record ist entscheidend dafür, dass deine Mails nicht als Spam landen.

DKIM später erneut anzeigen:

pct enter 110
cat /opt/dms-stack/docker-data/dms/config/rspamd/dkim/*.dns.txt

Prüfen lassen kannst du alles bequem über mail-tester.com und dkimvalidator.com.


4. Port-Weiterleitung / Firewall

Diese Ports müssen von außen zum Container (<IP>) gelangen:

Port Zweck
25 SMTP (eingehende Mails von anderen Servern)
587 / 465 Mail-Versand der Mitarbeiter (Submission)
143 / 993 IMAP (Mailabruf)

Falls der Container hinter NAT liegt, am Router entsprechend weiterleiten. Die Web-Ports 80/443 gehen an den Nginx Proxy Manager, nicht direkt an den Container.


5. Supabase-Login einrichten (Admin-UI)

Die Admin-UI nutzt Supabase-Auth (wie OPENBUREAU):

  1. In deinem Supabase-Projekt unter Authentication → Users einen Benutzer mit deiner E-Mail anlegen (oder Einladung).
  2. Diese E-Mail muss in ADMIN_ALLOWED_EMAILS stehen (in /opt/dms-stack/.env).
  3. SUPABASE_URL und SUPABASE_ANON_KEY in .env müssen gesetzt sein.

Änderungen an der .env aktiv machen:

cd /opt/dms-stack
docker compose up -d            # liest .env neu (UI/-API neu starten)
docker compose restart admin-ui admin-api

Nur E-Mails aus ADMIN_ALLOWED_EMAILS erhalten Zugriff — alle anderen Supabase-User werden von der API mit 403 abgewiesen.


6. Nginx Proxy Manager (HTTPS für Web)

Im NPM zwei Proxy Hosts anlegen:

Domain Forward Hostname/IP Port SSL
admin.example.com <Container-IP> 8080 Let's Encrypt anfordern, „Force SSL"
mail.example.com <Container-IP> 8888 Let's Encrypt anfordern, „Force SSL"

Für SnappyMail unter Advanced ggf. „Websockets Support" aktivieren.


7. TLS für den Mailserver (Port 465/587/993)

Mail-Protokolle laufen nicht über NPM, der Mailserver braucht also ein eigenes Zertifikat. DMS ist auf SSL_TYPE=manual mit festem Pfad konfiguriert:

/opt/dms-stack/docker-data/certs/cert.pem   (Zertifikat / fullchain)
/opt/dms-stack/docker-data/certs/key.pem    (privater Schlüssel)

Das Setup-Skript legt dort beim ersten Start automatisch ein self-signed Zertifikat ab, damit STARTTLS sofort funktioniert (Clients warnen wegen Selbstsignierung — für interne Tests okay). Für ein echtes Let's-Encrypt-Zertifikat einfach diese zwei Dateien ersetzen:

  1. In NPM unter SSL Certificates ein Zertifikat für mail.kgva.ch via DNS-Challenge anfordern (funktioniert auch ohne offenen Port 80).
  2. Dessen fullchain.pemdocker-data/certs/cert.pem und privkey.pemdocker-data/certs/key.pem kopieren.
  3. cd /opt/dms-stack && docker compose restart mailserverkeine Config-Änderung nötig, der Pfad bleibt gleich.

Bei Erneuerung (alle ~90 Tage) die beiden Dateien neu kopieren und docker compose restart mailserver. Das lässt sich per Cron/Hook automatisieren. Alternativ acme.sh/certbot direkt im LXC (DNS-Plugin) und die Cert-Dateien an dieselben Pfade schreiben.


8. Admin-UI benutzen

https://admin.example.com öffnen → mit Supabase-E-Mail/Passwort einloggen.

  • Übersicht (Dashboard): Zähler (Postfächer/Aliase/Domains), Domains, Schnell-Links.
  • Postfächer: Konten anlegen/löschen, Passwort ändern, Quota setzen (z.B. 5G, leer = unbegrenzt). Validierung von E-Mail/Quota/Passwortlänge.
  • Aliase: Weiterleitungen, z.B. info@example.comchef@example.com (mehrere Ziele mit Komma). Catch-all: als Quelle @example.com eintragen → fängt alle unbekannten Adressen der Domain.
  • Einstellungen: Domains hinzufügen/entfernen (Hinzufügen erzeugt automatisch den DKIM-Key), Brand, Webmail-/Admin-Domain und Mailserver-FQDN bearbeiten. (Erst-Befüllung aus dem Deploy-Dialog.)
  • Server: Quota-Auslastung (belegt/frei je Postfach), Mail-Queue, aktive Sessions. Läuft über eine abgesicherte Bridge (docker-socket-proxy, nur exec, Whitelist).
  • Status & DNS: pro Domain MX/SPF/DMARC + DKIM zum Kopieren, „DKIM erzeugen/erneuern"-Button.

Konten/Aliase werden direkt in die DMS-Config-Dateien geschrieben; DMS übernimmt Änderungen automatisch.


9. SnappyMail (Webmail) einrichten

  1. https://mail.example.com öffnen.
  2. Admin-Panel beim ersten Start: https://mail.example.com/?admin — das Admin-Passwort steht in /opt/dms-stack/docker-data/snappymail/_data_/_default_/admin_password.txt.
  3. Unter Domains die Domain example.com hinzufügen:
    • IMAP: mailserver Port 993 (SSL/TLS) — innerhalb des Docker-Netzes per Containername erreichbar, extern mail.example.com Port 993.
    • SMTP: Port 587 (STARTTLS).
  4. Mitarbeiter loggen sich dann mit ihrer vollen E-Mail-Adresse + Passwort ein.

KGVA-Theme („Shibui")

SnappyMail wird im selben Look wie das KGVA-Nextcloud-Theme ausgeliefert. Das Theme liegt unter stack/snappymail-theme/Shibui/styles.css und wird per Volume nach /snappymail/themes/ gemountet — es erscheint in SnappyMail als Shibui@custom.

Aktivieren (eine der beiden Varianten):

  • Für alle (empfohlen): Admin-Panel (https://mail.example.com/?admin) → Themes → Standard-Theme auf Shibui setzen. Alternativ in data/_data_/_default_/configs/application.ini unter [webmail] theme = "Shibui@custom" eintragen und docker compose restart snappymail.
  • Pro Nutzer: Webmail → Einstellungen → Allgemein → Theme → Shibui.

Anpassen: einfach styles.css editieren und docker compose restart snappymail. Die Farb-/Font-Variablen stehen gesammelt im :root-Block oben (Petrol-Akzent #7BA89B, washi-Neutraltöne, Inter/Instrument Serif/JetBrains Mono). Hinweis: Die Fonts werden aktuell von Google Fonts geladen; auf Wunsch hoste ich sie lokal (DSGVO).


10. Mail-Client-Einstellungen (für Outlook/Thunderbird/Apple Mail)

Einstellung Wert
Posteingang (IMAP) mail.example.com, Port 993, SSL/TLS
Postausgang (SMTP) mail.example.com, Port 587 (STARTTLS) oder 465 (SSL)
Benutzername volle E-Mail-Adresse
Authentifizierung normales Passwort

11. Backup

Alle Daten liegen unter /opt/dms-stack/docker-data/:

pct enter 110
cd /opt/dms-stack
docker compose down
tar czf /root/dms-backup-$(date +%F).tar.gz docker-data .env mailserver.env
docker compose up -d

Zusätzlich empfiehlt sich ein Proxmox-Backup (vzdump) des ganzen LXC sowie für Postfächer ggf. eine Offsite-Kopie von docker-data/dms/mail-data/.


12. Troubleshooting

pct enter 110 && cd /opt/dms-stack

docker compose ps                      # Status aller Container
docker compose logs -f mailserver      # Mail-Logs
docker exec mailserver setup email list        # Postfächer auflisten (CLI)
docker exec mailserver postqueue -p            # Mail-Queue ansehen
docker exec mailserver setup debug fetchmail   # Debug

Häufige Stolperfallen:

  • Mails kommen nicht raus / landen im Spam: Port 25 vom ISP geblockt, fehlender PTR, oder DKIM/SPF/DMARC nicht gesetzt → mail-tester.com nutzen.
  • Docker startet im LXC nicht: nesting=1,keyctl=1 prüfen (pct config 110); bei Bedarf pct set 110 --features nesting=1,keyctl=1 und Container neu starten.
  • Admin-UI „Token ungültig": E-Mail steht nicht in ADMIN_ALLOWED_EMAILS, oder SUPABASE_URL/ANON_KEY falsch.
  • Admin-UI lädt, aber Login schlägt fehl: config.js prüfen → curl http://<ip>:8080/config.js sollte deine Supabase-URL zeigen; sonst docker compose up -d admin-ui.

13. Spam-Filter (Rspamd)

docker-mailserver bringt Rspamd mit (aktiviert: Bayes, SPF/DKIM/DMARC-Checks, Greylisting, RBLs) — ein sehr guter Spam-Filter ohne Extra-Infrastruktur. Die Web-UI (Statistik/Training) läuft auf Port 11334 und ist passwortgeschützt (Passwort wird beim Deploy generiert und ausgegeben; steht in docker-data/dms/config/rspamd/override.d/worker-controller.inc).

  • Im NPM als Proxy-Host rspamd.<domain><CT-IP>:11334 (HTTPS).
  • Passwort ändern: Datei editieren (password / enable_password) + docker compose restart mailserver.
  • Optional härter hashen: docker exec mailserver rspamadm pw → Hash statt Klartext eintragen.

Für größere Setups/dedizierte Quarantäne kann man optional ein Proxmox Mail Gateway davorschalten — für ein kleines Büro ist Rspamd i.d.R. ausreichend.


14. Hardening

Schon eingebaut:

  • Unprivilegierter LXC + Nesting; Container mit no-new-privileges (admin-api, admin-ui, snappymail, socket-proxy).
  • Mailserver-Bridge nur über docker-socket-proxy (ausschließlich exec) + Kommando-Whitelist in der API — kein roher Docker-Socket an die App.
  • Rspamd-UI passwortgeschützt; Fail2ban aktiv (cap_add: NET_ADMIN); DMS kein Open-Relay (PERMIT_DOCKER=none, SPOOF_PROTECTION=1).
  • Admin nur für E-Mails aus ADMIN_ALLOWED_EMAILS (Supabase). .env ist chmod 600.
  • Proxmox-CT-Firewall: Mail-Ports offen, Web/Admin/Webmail/Rspamd nur von NPM_IP. Wirkt, sobald die Datacenter/Node-Firewall aktiv ist (Datacenter → Firewall → Options → Enable: Yes).

Vor dem Echtbetrieb noch erledigen:

  • Echtes TLS für Mail: NPM-Zertifikat (DNS-Challenge) → docker-data/certs/cert.pem|key.pem, restart mailserver (ersetzt self-signed).
  • Admin-UI/Webmail/Rspamd nur über NPM (HTTPS) erreichbar machen — rohe Ports nicht ins Internet; NPM_IP setzen.
  • Supabase-Login mit 2FA absichern; starke Postfach-Passwörter.
  • Backups (vzdump + docker-data/), siehe Abschnitt 11.
  • PTR/Reverse-DNS auf mail.<domain> setzen; Port 25 beim ISP prüfen (sonst Smarthost/RELAY_HOST).
  • DMARC nach der Testphase auf p=reject verschärfen.

Projektstruktur

.
├── dms-lxc.sh                 # Proxmox-Host-Skript (LXC anlegen + Stack deployen)
├── README.md
└── stack/                     # wird in den Container nach /opt/dms-stack kopiert
    ├── docker-compose.yml
    ├── .env.example
    ├── mailserver.env
    ├── api/                   # Node.js Admin-API (Supabase-Auth)
    │   ├── server.js
    │   └── lib/{store,auth}.js
    └── admin/                 # React-Admin UI (Vite → nginx)
        └── src/{App,Layout,Status,authProvider,dataProvider}.jsx ...