Docker and IPtables

TL;DR; By default, docker daemon appends iptables rules for forwarding. For this, it uses a filter chain named DOCKER.

Chain FORWARD (policy DROP)
target     prot opt source               destination
DOCKER     all  --  

Chain DOCKER (1 references)
target     prot opt source               destination

Moreover, when you tell docker to expose a port of a container, it exposes it to the entire world, breaking your possibly existing iptables rules.

So.. if you are running docker on a host that already have an iptables based firewall, you should probably set --iptables=false.

What are you talking about?

Let's take an example. You want to start nginx and bind containerPort 80 to hostPort 9090:

docker run --name some-nginx -d -p 9090:80 nginx

What it does behind the scene is adding an iptables rule to the DOCKER filter chain:

Chain FORWARD (policy DROP)
target     prot opt source               destination
DOCKER     all  --  

Chain DOCKER (1 references)
target     prot opt source               destination
ACCEPT     tcp  --             tcp dpt:9090 <-- this was added when running the container

Now port 9090 is available from the entire world. Why? Because we're listening 9090 on any IP addresses (*) and because of the forwarding rules that are dynamically added in the DOCKER filter chain. Note that docker’s forward rules permit all external source IPs by default.

You probably don't want that.

Exposing ports locally

You might want to publish ports just locally and not to *, for internal use. Let's read the documentation of docker run:

-p=[]      : Publish a container's port or a range of ports to the host
               format: ip:hostPort:containerPort | ip::containerPort | hostPort:containerPort | containerPort
               Both hostPort and containerPort can be specified as a range of ports.
               When specifying ranges for both, the number of container ports in the range must match the number of host ports in the range. (e.g., `-p 1234-1236:1234-1236/tcp`)
               (use 'docker port' to see the actual mapping)

As you can see, you can bind the hostPort to an IP.

docker run --name some-nginx -d -p nginx
netstat -an | grep 9090
tcp6       0      0 :::9090                 :::*                    LISTEN
netstat -an | grep 9090
tcp        0      0*               LISTEN


Docker, stop messing with my iptables rules!

Let's say you are using docker on a server available on the Internet. You already have an iptables based firewall configured. Personally, I'm using uif which is a very powerful perl script available in debian. Have a look at a config example.

To tell docker to never make changes to your system iptables rules, you have to set --iptables=false when the daemon starts.

For sysvinit and upstart based systems, you can edit /etc/default/docker. For systemd, you can do that:

mkdir /etc/systemd/system/docker.service.d
cat << EOF > /etc/systemd/system/docker.service.d/noiptables.conf
ExecStart=/usr/bin/docker daemon -H fd:// --iptables=false
systemctl daemon-reload

Now reload your firewall and restart docker daemon. You can see that the chain named DOCKER and the references to it in chain FORWARD (policy DROP) disappeared.

Configure iptables to work with docker

If you're still using the Ethernet bridge created by docker and named docker0, you can set the following rules for forwarding:

# just an example. It implies that your host Ethernet NIC is eth0
-A FORWARD -i docker0 -o eth0 -j ACCEPT
-A FORWARD -i eth0 -o docker0 -j ACCEPT

Now if you want to expose TCP port 10000 of a running container to the world, this container must expose port to any IP (*) on host side:

docker run --name some-nginx -d -p 10000:80 nginx

netstat -an | grep 10000
tcp6       0      0 :::10000                :::*                    LISTEN

Then you can add this firewall rule to allow the world to access your container through the forwarding rules:

-A INPUT -p tcp -m tcp --dport 10000 -s -j ACCEPT