The problem was with my reverse proxy not resolving the mailcow service. The Docker resolver can resolve based on the service name or the container name. I had intended to use the container name, but I didn’t add the “-1” that Docker adds to the end. So a few things for completeness sake:
Make sure you have the default Docker resolver in your Nginx http block. It would look like this resolver 127.0.0.11. Your default Nginx config (typically named default.conf doesn’t show the http block, it is the root of that file by default. I.e. put resolver 127.0.0.11 as a sibling of your server blocks:
Make sure you reference the correct service or container name in your Nginx config. By default, you would want to reference the nginx-mailcow service or the mailcow-nginx-mailcow-1 container. I decided I didn’t want to reference either of those, so I set the container name I proxy pass to to mailcow in the docker-compose.yml file. Remember the subdomain you access like mail.example.com IS NOT the same address you will proxy pass to.
Make sure your containers are all on the same network. I use a user-defined network instead of the default Docker network, so I had to add that to the appropriate Mailcow services. I did this in docker-compose.override.yml.
Nginx default.conf example:
resolver 127.0.0.11
server {
listen 80;
# Match requests with and with www, and with and without a subdomain
# If a request has a subdomain, it will be stored in the $subdomain variable
server_name ~^(www\.)?(?<subdomain>.+?)?\.?example\.com$;
server_tokens off;
# Redirect all HTTP traffic to HTTPS
location / {
if ($subdomain) {
return 301 https://$subdomain.example.com$request_uri;
}
return 301 https://example.com$request_uri;
}
}
server {
listen 443 ssl;
http2 on;
server_name ~^(www\.)?(?<subdomain>.+?)?\.?example\.com$;
server_tokens off;
ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;
# Disable TLS 1.0, TLS 1.1, and TLS 1.2 because they are old and insecure
ssl_protocols TLSv1.3;
location / {
set $proxy_pass "";
set $mailcow_container "mailcow";
set $mailcow_subdomain "mail";
set $mailcow_port "8080";
if ($subdomain = $mailcow_subdomain) {
set $proxy_pass "http://$mailcow_container:$mailcow_port$request_uri";
}
if ($proxy_pass = "") {
return 403;
}
# Proxy the request to the corresponding Docker container based on subdomain
proxy_pass $proxy_pass;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}
Mailcow docker-compose.override.yml example:
version: '2.1'
networks:
d_net:
external: true
services:
dovecot-mailcow:
networks:
- d_net
volumes:
- ./example.com/fullchain.pem:/etc/ssl/mail/cert.pem:ro
- ./example.com/privkey.pem:/etc/ssl/mail/key.pem:ro
nginx-mailcow:
container_name: mailcow
networks:
- d_net
volumes:
- ./example.com/fullchain.pem:/etc/ssl/mail/cert.pem:ro
- ./example.com/privkey.pem:/etc/ssl/mail/key.pem:ro
postfix-mailcow:
networks:
- d_net
volumes:
- ./example.com/fullchain.pem:/etc/ssl/mail/cert.pem:ro
- ./example.com/privkey.pem:/etc/ssl/mail/key.pem:ro