14.1.3.3 Host Firewall Config
A focused guide to Host Firewall Config, connecting core concepts with practical Docker and container operations.
Host firewall configuration for a Docker production host concerns how the operating system's packet filtering rules interact with the rules that the Docker daemon installs automatically, and how an operator layers their own restrictions on top without breaking container networking or accidentally exposing services to the public internet.
Why Docker complicates standard firewall setups
On Linux, Docker manages container networking primarily through iptables (or nftables on newer distributions configured to use the nf_tables backend). When the daemon starts, it creates its own chains, most notably DOCKER, DOCKER-ISOLATION-STAGE-1, DOCKER-ISOLATION-STAGE-2, and DOCKER-USER, and inserts rules into the FORWARD chain ahead of whatever rules a host-level firewall manager (such as ufw or firewalld) has configured. This means that a port published with -p 8080:80 becomes reachable from outside the host even if a tool like ufw reports that port as denied, because Docker's own rules are evaluated first and accept the forwarded traffic before the host firewall's deny rule is ever reached.
docker run -d -p 8080:80 nginx
iptables -L DOCKER -n
Inspecting the DOCKER chain directly shows the ACCEPT rules Docker inserted for the published port, independent of whatever the host-level firewall tool believes the effective policy to be.
The DOCKER-USER chain
Docker explicitly reserves the DOCKER-USER chain for operator-defined rules and guarantees it will not overwrite rules placed there, even across daemon restarts. This is the correct insertion point for host firewall policy that must apply to container traffic, rather than attempting to modify the DOCKER or FORWARD chains directly, which Docker regenerates.
iptables -I DOCKER-USER -i eth0 -s 203.0.113.0/24 -j DROP
iptables -I DOCKER-USER -j RETURN
A typical pattern is to default-deny inbound traffic to published container ports from outside a trusted network, then add explicit ACCEPT rules above the deny rule for the address ranges that should be allowed:
iptables -I DOCKER-USER -i eth0 ! -s 10.0.0.0/8 -j DROP
Restricting which interface receives published ports
A common production requirement is binding published ports only to a private or internal network interface, rather than to all interfaces:
docker run -d -p 127.0.0.1:5432:5432 postgres
docker run -d -p 10.0.0.5:443:443 my-api
Binding to a specific address at publish time is generally more reliable than trying to firewall off 0.0.0.0 afterward, since it removes the exposure at the source rather than attempting to filter it downstream.
ufw and Docker
ufw rules apply to the INPUT chain, while Docker's published ports are handled through the FORWARD chain via the DOCKER chain, so ufw deny rules typically have no effect on container traffic by default:
ufw deny 8080
docker run -d -p 8080:80 nginx
The above sequence does not block external access to port 8080, because the deny rule never intercepts forwarded packets destined for the container's published port.
To make ufw aware of Docker traffic, the DOCKER-USER chain must be referenced directly in ufw's after.rules file, or Docker's iptables management must be disabled and a fully custom ruleset maintained instead:
ufw route deny in on eth0 to any port 8080
The ufw route subcommand operates on the FORWARD chain and is the supported way to restrict forwarded container traffic when using ufw alongside Docker.
firewalld and Docker
firewalld uses zones and, depending on the Docker integration mode, may either cooperate with Docker's iptables rules or conflict with them if both attempt to manage the same chains. Recent Docker versions support a firewalld backend directly through the daemon configuration:
{
"iptables": true,
"ip-forward": true
}
firewall-cmd --permanent --zone=trusted --add-interface=docker0
firewall-cmd --reload
Placing the Docker bridge interface in a trusted zone avoids firewalld rejecting bridge-to-bridge or bridge-to-host traffic that Docker's networking model depends on.
Disabling Docker's iptables management
For environments that require a single, centrally managed firewall ruleset, Docker's automatic iptables manipulation can be disabled entirely:
{
"iptables": false
}
systemctl restart docker
With this setting, container-to-container and container-to-host networking will not work correctly until the operator manually replicates the NAT and forwarding rules Docker would otherwise create, including masquerading for outbound traffic and DNAT rules for published ports. This option is appropriate only when the host firewall is fully scripted and includes equivalents for Docker's default behavior.
Outbound and inter-container traffic
Host firewall policy should also account for traffic leaving containers and traffic between containers on user-defined networks. By default, containers on the same bridge network can reach each other freely, while DOCKER-ISOLATION-STAGE-1 and DOCKER-ISOLATION-STAGE-2 prevent traffic from crossing between separate Docker networks unless an explicit route exists. Production segmentation should rely on Docker's network isolation for east-west traffic between services, and on DOCKER-USER rules for restricting which external sources may reach published ports.
docker network create --driver bridge isolated-net
docker run -d --network isolated-net --name internal-api my-api
A container attached only to isolated-net and never published to the host has no route from outside the host firewall at all, which is often a stronger guarantee than any firewall rule layered on top of a publicly bound port.
Persisting rules across reboots
Manually inserted iptables rules do not survive a reboot unless explicitly persisted. On Debian-based systems this is commonly handled with iptables-persistent:
apt-get install iptables-persistent
netfilter-persistent save
On systemd-based systems, a unit that runs after docker.service starts (so the DOCKER-USER chain already exists) is the safest way to reapply custom rules automatically:
[Unit]
After=docker.service
Requires=docker.service
[Service]
ExecStart=/usr/local/sbin/apply-docker-firewall.sh
Common mistakes
- Assuming a host firewall tool's reported deny rule blocks a published container port, when in practice Docker's
DOCKERchain rules are evaluated first in theFORWARDchain. - Modifying the
DOCKERchain directly instead ofDOCKER-USER, producing rules that disappear the next time the daemon restarts or a container is created. - Publishing a port to
0.0.0.0and relying entirely on a firewall to restrict it, rather than binding the publish address to a private interface in the first place. - Disabling Docker's iptables management without fully replicating the NAT and forwarding behavior it provided, breaking outbound connectivity from containers.
- Forgetting to persist custom rules, so a host reboot silently reverts to an unrestricted state until the rules are reapplied.
A reliable host firewall configuration for Docker production hosts treats DOCKER-USER as the single integration point for custom policy, prefers binding publish addresses over after-the-fact filtering, and verifies behavior with iptables -L and iptables -t nat -L rather than assuming a host firewall tool's own status output reflects the effective ruleset.