Outcome: Redirect all HTTP and HTTPS traffic from an old subdomain to a new domain, using mailcow’s nginx and acme for SSL — so the redirect works reliably for all visitors regardless of browser HTTPS enforcement.
When to use: When you’ve moved a self-hosted service (e.g. Discourse) to a new domain and want the old URL to keep working as a redirect. DNS-provider URL redirects (e.g. Namecheap) are not a reliable substitute — they typically fail silently on HTTPS because they don’t hold a valid SSL certificate for the old domain.
Prerequisites:
- Mailcow is running at
/opt/mailcow-dockerizedon your server- The old subdomain (
=OLD_SUBDOMAIN=) still has its DNS A/AAAA record pointing to=OLD_MAIL_HOST=- The new domain (
=NEW_DOMAIN=) is already live and working- All commands are run as
rootfrom/opt/mailcow-dockerizedunless otherwise noted
Step 1 — Add the old subdomain to ADDITIONAL_SAN
Mailcow’s acme container manages all SSL certificates. The old subdomain needs a valid cert so that HTTPS visitors receive the redirect rather than a TLS error.
nano /opt/mailcow-dockerized/mailcow.conf
Find the ADDITIONAL_SAN line and add =OLD_SUBDOMAIN=:
ADDITIONAL_SAN=...,=OLD_SUBDOMAIN=
Then force-recreate the acme container and watch for success:
cd /opt/mailcow-dockerized
docker compose up -d --force-recreate acme-mailcow
docker compose logs -f acme-mailcow
You’re looking for:
=OLD_SUBDOMAIN= verified!
Certificate signed!
The
AUTH failed: ERR AUTHmessages in the acme logs are a harmless mailcow quirk — Redis has no password configured. Ignore them.
Step 2 — Create the nginx redirect config
nano /opt/mailcow-dockerized/data/conf/nginx/=NGINX_CONF=.conf
Paste in:
# Redirect HTTP → HTTPS → new domain
server {
listen 80;
listen [::]:80;
server_name =OLD_SUBDOMAIN=;
location ^~ /.well-known/acme-challenge/ {
allow all;
default_type "text/plain";
root /web;
}
location / {
return 301 https://=NEW_DOMAIN=$request_uri;
}
}
# Redirect HTTPS → new domain
server {
listen 443 ssl;
listen [::]:443 ssl;
server_name =OLD_SUBDOMAIN=;
ssl_certificate /etc/ssl/mail/cert.pem;
ssl_certificate_key /etc/ssl/mail/key.pem;
location / {
return 301 https://=NEW_DOMAIN=$request_uri;
}
}
The HTTPS server block is essential. Modern browsers force HTTPS, so without it, HTTPS visitors will get a TLS error and never see the redirect at all.
Step 3 — Test and reload nginx
cd /opt/mailcow-dockerized
docker compose exec nginx-mailcow nginx -t && \
docker compose exec nginx-mailcow nginx -s reload
The -t flag tests the config first — if there are errors, nginx will report exactly which line before anything is changed.
Step 4 — Verify the redirects are working
# HTTP → new domain (expect 301)
curl -I http://=OLD_SUBDOMAIN=
# HTTPS → new domain (expect 301)
curl -I https://=OLD_SUBDOMAIN=
Both should return:
HTTP/1.1 301 Moved Permanently
Location: https://=NEW_DOMAIN=/
Troubleshooting
TLS error / connection hangs on HTTPS — The cert hasn’t been issued yet for =OLD_SUBDOMAIN=. Confirm the domain is in ADDITIONAL_SAN (Step 1) and re-run the acme container. Watch the logs until you see Certificate signed!.
nginx -t fails — Check for a conflicting .conf file in /opt/mailcow-dockerized/data/conf/nginx/ that also has =OLD_SUBDOMAIN= as a server_name. Rename the old one to .conf.disabled to deactivate it:
mv /opt/mailcow-dockerized/data/conf/nginx/oldfile.conf \
/opt/mailcow-dockerized/data/conf/nginx/oldfile.conf.disabled
curl returns 000 — The SSL certificate doesn’t cover =OLD_SUBDOMAIN= yet. See the TLS error fix above.
Redirect goes to the wrong place — Double-check the return 301 line in both server blocks points to https://=NEW_DOMAIN= and not to $host (which would loop back to the old domain).
Cleanup — when the old domain is no longer needed
Once traffic to =OLD_SUBDOMAIN= has fully died off (check your analytics), you can clean up:
- Remove
=OLD_SUBDOMAIN=fromADDITIONAL_SANinmailcow.conf - Delete or disable the nginx config:
mv /opt/mailcow-dockerized/data/conf/nginx/=NGINX_CONF=.conf \
/opt/mailcow-dockerized/data/conf/nginx/=NGINX_CONF=.conf.disabled
- Reload nginx and force-recreate acme to drop the domain from the cert:
cd /opt/mailcow-dockerized
docker compose exec nginx-mailcow nginx -s reload
docker compose up -d --force-recreate acme-mailcow
Related topics: