- caddy-Service in docker-compose (profiles: caddy) – reverse-proxyt webmail/admin mit automatischem Let's-Encrypt; auf dem LXC bleibt er aus (dort macht NPM das HTTPS) - stack/caddy/Caddyfile (Domains via WEBMAIL_FQDN/ADMIN_FQDN aus .env) - vps-install.sh: ENABLE_CADDY (default 1) -> --profile caddy beim Deploy, smtp_address_preference=ipv4 als postfix-main.cf-Override (gegen IPv6- 'Network unreachable'-Queue-Delays), Output mit https-URLs + A-Records Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
docker-mailserver auf Proxmox (LXC) — mit React-Admin UI & SnappyMail
Ein vollständiges, selbst gehostetes Mail-Setup für ein kleines Büro (4–5 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
rootauf 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:
- unprivilegierten LXC (Debian 13,
nesting=1,keyctl=1) anlegen + PVE-CT-Firewall, - Docker installieren,
- Self-signed-Cert + erstes Postfach vor-seeden, dann den Stack bauen & starten,
- DKIM je Domain erzeugen, SnappyMail provisionieren (Domain→mailserver, Shibui-Theme),
- 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.ch — beim 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_DOMAINSin.envergä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) ← WICHTIG vor dem ersten Login
Die Admin-UI nutzt Supabase-Auth (wie OPENBUREAU). Das Tool legt den Admin-User NICHT selbst an — du erstellst ihn einmalig im Supabase-Dashboard. Schritt für Schritt:
- Supabase-Projekt (falls noch keins): supabase.com → New Project (Free-Tier reicht).
- Keys holen: Project Settings → API →
Project URL→ das istSUPABASE_URLanonpublicKey → das istSUPABASE_ANON_KEY
- Admin-User anlegen: Authentication → Users → Add user →
- E-Mail + Passwort eintragen, „Auto Confirm User" aktivieren (sonst braucht's eine Bestätigungsmail).
- Diese E-Mail muss in
ADMIN_ALLOWED_EMAILSstehen (Deploy-Dialog bzw./opt/dms-stack/.env). - Werte gesetzt?
cd /opt/dms-stack && docker compose up -d && docker compose restart admin-ui admin-api. - Login:
https://admin.<domain>→ mit dieser E-Mail + Passwort einloggen.
Nur E-Mails aus
ADMIN_ALLOWED_EMAILSerhalten Zugriff — alle anderen Supabase-User werden mit 403 abgewiesen.Alternative (Self-Service): In Supabase unter Authentication → Providers → Email „Enable Signup" aktivieren; dann kann sich die erlaubte E-Mail über die Login-Seite selbst registrieren. (Standard: aus.)
Weitere Admins später: einfach in Supabase als User anlegen und die E-Mail in
ADMIN_ALLOWED_EMAILSergänzen.
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:
- In NPM unter SSL Certificates ein Zertifikat für
mail.kgva.chvia DNS-Challenge anfordern (funktioniert auch ohne offenen Port 80). - Dessen
fullchain.pem→docker-data/certs/cert.pemundprivkey.pem→docker-data/certs/key.pemkopieren. cd /opt/dms-stack && docker compose restart mailserver— keine 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. Alternativacme.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.com→chef@example.com(mehrere Ziele mit Komma). Catch-all: als Quelle@example.comeintragen → 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
https://mail.example.comöffnen.- 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. - Unter Domains die Domain
example.comhinzufügen:- IMAP:
mailserverPort993(SSL/TLS) — innerhalb des Docker-Netzes per Containername erreichbar, externmail.example.comPort993. - SMTP: Port
587(STARTTLS).
- IMAP:
- 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 aufShibuisetzen. Alternativ indata/_data_/_default_/configs/application.iniunter[webmail]theme = "Shibui@custom"eintragen unddocker compose restart snappymail. - Pro Nutzer: Webmail → Einstellungen → Allgemein → Theme → Shibui.
Anpassen: einfach
styles.csseditieren unddocker 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.comnutzen. - Docker startet im LXC nicht:
nesting=1,keyctl=1prüfen (pct config 110); bei Bedarfpct set 110 --features nesting=1,keyctl=1und Container neu starten. - Admin-UI „Token ungültig": E-Mail steht nicht in
ADMIN_ALLOWED_EMAILS, oderSUPABASE_URL/ANON_KEYfalsch. - Admin-UI lädt, aber Login schlägt fehl:
config.jsprüfen →curl http://<ip>:8080/config.jssollte deine Supabase-URL zeigen; sonstdocker 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)..envistchmod 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_IPsetzen. - 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=rejectverschä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 ...