Mailcow with Nginx reverse proxy (updated)
2025-04-19
Using other web services with Mailcow
This is roughly what I did to have an Nginx web server on the same machine as dockerized Mailcow
Note: mail.xxxxx.com is your mail server (MX), yourdomain.com is whatever domain you wish to use.
- Change the default http and https ports (for example 8480 and 8443)
and set SKIP_LETS_ENCRYPT=y in /opt/mailcow-dockerized/mailcow.conf - Restart Mailcow by executing
cd /opt/mailcow-dockerized;docker compose restart - Make sure the DNS entry mail.xxxxx.com points to the server
- mkdir -p /var/www/html/letsencrypt/.well-known/acme-challenge
- Create /etc/nginx/letsencrypt_path. This can be re-used in your other domains.
# url for letsencrypt location ^~ /.well-known/acme-challenge/ { allow all; default_type "text/plain"; # Path can be used for cert-validation on all domains root /var/www/html/letsencrypt; break; } - Stop the nginx server systemctl stop nginx
- Run certbot certonly -d mail.xxxxx.com (select 2 standalone)
- Create /etc/nginx/sites-available/mail.xxxxx.com
- ln -s /etc/nginx/sites-available/mail.xxxxx.com
/etc/nginx/sites-enabled/mail.xxxxx.com - Test with nginx -t, if all is well run sudo systemctl start nginx
- Pointing your browser to https://mail.xxxxx.com should show you the Mailcow login page.
server {
listen 443 ssl;
listen [::]:443 ssl;
http2 on;
server_name mail.xxxxx.com;
charset UTF-8;
access_log /var/log/nginx/access.mail.xxxxx.com;
error_log /var/log/nginx/error.mail.xxxxx.com;
include snippets/error_pages.conf;
ssl_certificate /etc/letsencrypt/live/mail.xxxxx.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/mail.xxxxx.com/privkey.pem;
include /etc/letsencrypt/options-ssl-nginx.conf;
ssl_dhparam /etc/letsencrypt/ssl-dhparams-2048.pem;
include /etc/nginx/letsencrypt_path;
location / {
proxy_pass http://127.0.0.1:8480;
proxy_buffering off;
include /etc/nginx/proxy_params;
}
}
server {
listen 80;
listen [::]:80;
server_name mail.xxxxx.com;
return 301 https://mail.xxxxx.com;
}
Problem
The problem is that Mailcow can no longer use port 80 to update it's ssl certificates that are used by postfix and dovecot. Instead certbot puts them in /etc/letsencrypt/live. To fix this the following script runs from crontab daily.
#!/usr/bin/env bash # we are running behind an nginx proxy, where certbot is run by systemd, # so the mail.xxxxx.com certificates are updated, but not copied to the mailcow folder # in case the certificate isn't renewed automatically, run this: # sudo certbot -n certonly --webroot -w /var/www/html/letsencrypt -d mail.xxxxx.com t1="/etc/letsencrypt/live/mail.xxxxx.com/fullchain.pem" t2="/opt/mailcow-dockerized/data/assets/ssl/mail.xxxxx.com/cert.pem" # test if certificate has been updated differ=$(cmp -b $t1 $t2 | grep -c "differ") [[ "$differ" = "0" ]] && exit 0 # these files are required in /data/assets/ssh/mail.xxxxx.com cp /etc/letsencrypt/live/mail.xxxxx.com/fullchain.pem /opt/mailcow-dockerized/data/assets/ssl/mail.xxxxx.com/cert.pem cp /etc/letsencrypt/live/mail.xxxxx.com/privkey.pem /opt/mailcow-dockerized/data/assets/ssl/mail.xxxxx.com/key.pem # these files are required in /data/assets/ssl/ (turns out sending failed otherwise) cp /etc/letsencrypt/live/mail.xxxxx.com/fullchain.pem /opt/mailcow-dockerized/data/assets/ssl/cert.pem cp /etc/letsencrypt/live/mail.xxxxx.com/privkey.pem /opt/mailcow-dockerized/data/assets/ssl/key.pem # update postfix & docker docker exec $(/usr/bin/docker ps -qaf name=postfix-mailcow) postfix reload docker exec $(/usr/bin/docker ps -qaf name=dovecot-mailcow) dovecot reload
Inspiration: Felix Moesbauer
Don't forget to check and correct if necessary your DANE and MTA-STS records if you use them
DANE
On your mail server:
apt install hash-slinger -y
Create a TLSA record:
tlsa --create --selector 1 -p 25 --certificate /etc/letsencrypt/live/mail.xxxxx.com/fullchain.pem mail.xxxxx.com
The last line or the result will be something like:
_25._tcp.mail.xxxxx.com. IN TLSA 3 1 1 443ac7c5c70fbfbc...
Enter this TLSA line in your mail server's DNS record.
Do the same for the following ports:
_110._tcp.mail.xxxxx.com _143._tcp.mail.xxxxx.com _465._tcp.mail.xxxxx.com _587._tcp.mail.xxxxx.com _993._tcp.mail.xxxxx.com _995._tcp.mail.xxxxx.com
You can check your DANE records at huque.com

MTA-STS
This is a fallback to DANE, you can run both.
Note: https access to mta-sts.yourdomain.com is obligatory
- Create /var/www/html/mta-sts/.well-known/mta-sts.txt with the following content:
version: STSv1 mode: enforce max_age: 172800 mx: mail.xxxxx.com
- Create the DNS record _mta-sts.mail.xxxxx.com as TXT with "v=STSv1; id=INSERT AN ID, EX. 202511101644"
- Create the DNS record _mta-sts.yourdomain.com as CNAME pointing to _mta-sts.mail.xxxxx.com (your mail server)
- Create the DNS record mta-sts.yourdomain.com pointing to the ip of the web server
- You may need to wait for the DNS records to propagate
- Stop Nginx with systemctl stop nginx
- Create a letsencrypt certificate with certbot certonly -d mta-sts.yourdomain.com (select 2 standalone)
- Create mta-sts.yourdomain.com in Nginx:
- /etc/sites-available/mta-sts.yourdomain.com
server { listen 443 ssl; listen [::]:443 ssl; http2 on; charset UTF-8; access_log /var/log/nginx/access.mts-sta.yourdomain.com; error_log /var/log/nginx/error.mts-sta.yourdomain.com; include snippets/error_pages.conf; ssl_certificate /etc/letsencrypt/live/mta-sts.yourdomain.com/fullchain.pem; ssl_certificate_key /etc/letsencrypt/live/mta-sts.yourdomain.com/privkey.pem; include /etc/letsencrypt/options-ssl-nginx.conf; ssl_dhparam /etc/letsencrypt/ssl-dhparams-2048.pem; include /etc/nginx/letsencrypt_path; root /var/www/html/mta-sts; index index.html index.htm; server_name mta-sts.yourdomain.com; location / { try_files $uri $uri/ =404; } } server { listen 80; listen [::]:80; server_name mta-sts.yourdomain.com; return 301 https://mta-sts.yourdomain.com; } - ln -s /etc/nginx/sites-available/mta-sts.yourdomain.com
/etc/nginx/sites-enabled/mta-sts.yourdomain.com - Test with nginx -t and if all goes well start nginx systemctl start nginx
You can check your domain's MTA-STS at mxtoolbox.com
All this worked for me :-) I hope it does for you!