Is Mailcow Too Port-Hungry? Here's the Fix.
How to run Mailcow and other services on the same server using floating IPs, Docker Compose port binding, and UFW firewall rules per IP address.
Mailcow is brilliant, but it is hungry. A single deployment devours ports 25, 80, 110, 143, 443, 465, 587, 993, 995, and more. It binds to every network interface. It leaves nothing for anything else. Want to run Nextcloud alongside it? You cannot. A WordPress site? No room. A reverse proxy? Forget it. Mailcow eats the network.
The workarounds are all bad. Change Mailcow's ports? MX records and mail clients expect the standard ones. Spin up a second VPS? Another server to provision, secure, patch, monitor, and pay for. Route everything through a reverse proxy? Nginx and Caddy cannot reverse proxy SMTP or IMAP. This is not an HTTP problem. It is a port collision at the network layer.
The Fix: One Floating IP, a Few Compose Edits
Provision a floating IP from Hetzner and assign it to your server. The floating IP appears in your server's network configuration as a secondary address. To make it persistent across reboots, add it to your netplan configuration instead of relying on a temporary ip addr add command:
network:
version: 2
ethernets:
eth0:
addresses:
- 203.0.113.10/32
- 203.0.113.20/32Apply with netplan apply and the floating IP survives reboots. Now you have two stable IP addresses: a primary IP and a floating IP. Decide which IP Mailcow will own. Edit the Mailcow docker-compose.yml and change every port binding from the shorthand to the full syntax pointing at Mailcow's chosen IP.
Most Docker Compose files use the shorthand port syntax. 80:80 binds port 80 on every available network interface. That is why Mailcow claims everything. Docker supports a more precise syntax: ip:host_port:container_port. For example, 203.0.113.10:443:443 binds port 443 to that specific IP only, leaving the same port free on every other IP assigned to the server.
Before:
ports:
- "80:80"
- "443:443"
- "25:25"After:
ports:
- "203.0.113.10:80:80"
- "203.0.113.10:443:443"
- "203.0.113.10:25:25"On the floating IP, bind Nginx Proxy Manager to ports 80 and 443. NPM then routes incoming web traffic to Nextcloud, WordPress, or any other service behind it. Because NPM binds only to the floating IP, it never collides with Mailcow's port 80 and 443 on the primary IP. The result: Mailcow owns the primary IP. Everything else owns the floating IP. Two services listening on port 443, each bound to a different IP, running on the same physical server. No conflicts.
Firewall Rules Per IP With UFW
The segmentation continues at the firewall layer. Because Linux sees each IP address as a routable endpoint, UFW rules can match traffic by destination IP using the to syntax. Mail ports (25, 465, 587, 993, 995) should only be accepted on Mailcow's IP. Web ports (80, 443) should only be accepted on the other IP.
ufw allow in on eth0 to 203.0.113.10 port 25 proto tcp
ufw allow in on eth0 to 203.0.113.10 port 465 proto tcp
ufw allow in on eth0 to 203.0.113.20 port 80 proto tcp
ufw allow in on eth0 to 203.0.113.20 port 443 proto tcpThe same rules using raw iptables:
iptables -A INPUT -i eth0 -d 203.0.113.10 -p tcp --dport 25 -j ACCEPT
iptables -A INPUT -i eth0 -d 203.0.113.10 -p tcp --dport 465 -j ACCEPT
iptables -A INPUT -i eth0 -d 203.0.113.20 -p tcp --dport 80 -j ACCEPT
iptables -A INPUT -i eth0 -d 203.0.113.20 -p tcp --dport 443 -j ACCEPTFor iptables, save with iptables-save > /etc/iptables/rules.v4 so rules survive reboots. Or if you use both UFW and iptables together, place raw rules in /etc/ufw/before.rules so UFW does not override them.
With either tool, the firewall enforces the segmentation. Mail ports only reach the Mailcow IP. Web ports only reach the Nginx Proxy Manager IP. A spammer trying port 25 on the web IP gets dropped. A web request to port 443 on the Mailcow IP gets through to the webmail UI, because it belongs there. The separation is complete.
Three layers of separation, all on one server: UFW drops traffic bound for the wrong IP. Docker routes traffic from the right IP to the right container. Each container only sees the ports assigned to it. Mailcow and your other services coexist without ever being aware of each other.
This Pattern Works for Any Port-Hungry Service
Mailcow is the clearest example, but the technique applies universally. Matrix Synapse wants ports 80, 443, and 8448. Traefik or Caddy want 80 and 443. A 3CX phone system wants 5060 and 5090. Assign each one a dedicated IP and write its Compose file with the full port syntax. No reverse proxy magic. No port remapping. No extra servers. Just Docker doing what it always could.
VPS1 deploys Mailcow, floating IPs, UFW rules, and Docker segmentation as a complete stack. Your email works. Your other services work. One server, zero port conflicts. Tell us what you need to run together and we will make the IPs add up.
More articles
WordPress on Your Own Server: Escape the Hosting Fees Without Losing Your Site
WordPress powers 43% of the web, but managed hosting keeps raising prices. Self-host WordPress on your own VPS for a fraction of the cost with full control.
Backup Your Business: Duplicati vs. Kopia for Self-Hosted Backups
Two leading open-source backup tools, two different approaches. Duplicati is visual and scheduled. Kopia is fast and snapshot-based. Here is how to protect your business data.
Docker Management Showdown: Portainer vs. Coolify vs. Dokploy
Three self-hosted Docker management platforms, three different philosophies. Portainer manages containers. Coolify deploys apps. Dokploy does both. Here is how to choose.