You add an nftables rule to drop traffic on port 8080. You check the ruleset — it's active. You curl localhost:8080 from outside the host, and the Dockerized API responds anyway. Your firewall just got ignored.
This isn't a configuration mistake. Docker deliberately writes its own iptables rules that execute before nftables ever sees the packet. If you're running GPU inference services, internal LLM APIs, or any container that shouldn't be internet-facing, this behavior is a production security gap.
Why Docker Bypasses Your Firewall
Docker manipulates iptables-legacy directly, inserting DNAT rules in the nat table and ACCEPT rules in the filter table. These rules redirect incoming traffic to container IPs before your nftables ruleset runs.
Check what Docker created:
sudo iptables-legacy -t nat -L DOCKER -n -v
sudo iptables-legacy -t filter -L DOCKER -n -v
You'll see entries like:
DNAT tcp -- * * 0.0.0.0/0 0.0.0.0/0 tcp dpt:8080 to:172.17.0.2:8080
The packet gets rewritten and forwarded before your nftables input chain ever evaluates it. Even if you block port 8080 in nftables, Docker's NAT rule already sent the traffic to the container.
On modern Debian and Ubuntu systems, nftables is the default firewall backend. But Docker still uses iptables-legacy for compatibility. This creates two parallel firewall systems — and Docker's rules win.
The Fix: Disable Docker's iptables Manipulation
Stop Docker from writing iptables rules. Edit /etc/docker/daemon.json:
{
"iptables": false
}
Restart Docker:
sudo systemctl restart docker
Now Docker won't touch your firewall. But you've also disabled container NAT and port publishing. If you run docker run -p 8080:8080 myapp, the port mapping silently fails. The container starts, but nothing listens on the host.
You now manage all forwarding and NAT yourself in nftables.
Build Your Own Docker NAT in nftables
You need three components:
- DNAT for inbound traffic (external → container)
- SNAT for outbound traffic (container → internet)
- Forwarding rules between host and Docker bridge
Here's a complete nftables configuration for a single container exposing port 8080:
#!/usr/sbin/nft -f
flush ruleset
table inet filter {
chain input {
type filter hook input priority 0; policy drop;
ct state established,related accept
iif "lo" accept
# Allow SSH
tcp dport 22 accept
# Block direct access to 8080 from outside
# Traffic will arrive via DNAT as forwarded packets
}
chain forward {
type filter hook forward priority 0; policy drop;
ct state established,related accept
# Allow forwarding to Docker containers
iif "eth0" oif "docker0" ip daddr 172.17.0.2 tcp dport 8080 accept
# Allow container responses
iif "docker0" oif "eth0" accept
}
chain output {
type filter hook output priority 0; policy accept;
}
}
table ip nat {
chain prerouting {
type nat hook prerouting priority -100; policy accept;
# DNAT: external traffic on 8080 → container
iif "eth0" tcp dport 8080 dnat to 172.17.0.2:8080
}
chain postrouting {
type nat hook postrouting priority 100; policy accept;
# SNAT: container outbound traffic → host IP
oif "eth0" ip saddr 172.17.0.0/16 masquerade
}
}
Save this as /etc/nftables.conf and apply:
sudo nft -f /etc/nftables.conf
Replace 172.17.0.2 with your container's IP. Find it with:
docker inspect -f '{{range.NetworkSettings.Networks}}{{.IPAddress}}{{end}}' <container_name>
Selective Exposure: Allow Only Internal Networks
If you want the container reachable only from your private network (not the internet), add a source filter in the DNAT rule:
iif "eth0" ip saddr 10.0.0.0/8 tcp dport 8080 dnat to 172.17.0.2:8080
This allows connections from RFC1918 space but drops everything else before DNAT happens.
For GPU inference APIs or internal vector search endpoints, this prevents accidental internet exposure while keeping the service available to your application tier.
Handling Multiple Containers
For multiple published ports, add one DNAT rule and one forward rule per container:
# Container 1: LLM API on 8080
iif "eth0" tcp dport 8080 dnat to 172.17.0.2:8080
iif "eth0" oif "docker0" ip daddr 172.17.0.2 tcp dport 8080 accept
# Container 2: Vector DB on 9200
iif "eth0" tcp dport 9200 dnat to 172.17.0.3:9200
iif "eth0" oif "docker0" ip daddr 172.17.0.3 tcp dport 9200 accept
For a dynamic container environment, this manual approach doesn't scale. Use Docker networks with explicit binds (--publish 127.0.0.1:8080:8080) so the service listens only on localhost, then manage external access through an nginx reverse proxy protected by nftables.
Enable nftables on Boot
Make the ruleset persistent:
sudo systemctl enable nftables
sudo systemctl start nftables
On Debian/Ubuntu, nftables reads /etc/nftables.conf at boot. Verify the service is active:
sudo systemctl status nftables
What You Lose
With "iptables": false, Docker Compose port mappings (ports: - "8080:8080") stop working unless you manually configure nftables NAT. Docker networks still function for inter-container communication, but host publishing requires your explicit forwarding rules.
For production GPU clusters running inference APIs, this tradeoff is worth it. You control exactly which ports are exposed and to whom. A single nftables ruleset governs all traffic — no hidden Docker rules bypassing your firewall.
Verification
Test the block:
# From outside the host
curl http://<host-ip>:8080
# Should fail if no DNAT rule exists
Add the DNAT rule, reload nftables, and retry. The request should reach the container.
Check your ruleset matches what you expect:
sudo nft list ruleset
Verify Docker didn't sneak in iptables rules:
sudo iptables-legacy -t nat -L DOCKER
# Should be empty or show "Chain DOCKER (0 references)"
If Docker re-created rules, it means daemon.json wasn't applied. Restart the daemon and double-check the JSON syntax.
Use Cases for Manual Firewall Control
This pattern matters when:
- Running inference APIs on GPU instances where accidental exposure costs money and leaks proprietary models
- Operating multi-tenant platforms where container isolation must be firewall-enforced, not just network-namespace-enforced
- Deploying internal RAG pipelines with vector databases that should never touch the public internet
- Meeting compliance requirements that demand explicit, auditable firewall rules for all published services
Docker's automatic iptables manipulation is convenient for development. In production infrastructure, convenience is a security liability. You need deterministic control over which packets reach which containers.
This post is an excerpt from Practical AI Infrastructure Engineering — a production handbook covering Docker, GPU infrastructure, vector databases, and LLM APIs. Full book with 4 hands-on capstone projects available at https://activ8ted.gumroad.com/l/ssmfkx
Originally published at fivenineslab.com
Top comments (0)