Skip to content
Français
Account: not signed in, sync available My learning
Select theme
Français

Isolating a container from the network: none or an internal network

By updated

Improve this page

Logo Docker

Two requests land on the same morning. Security wants a container that processes customer files to reach nothing: not the outside, not the host, not its neighbors. The application team wants its database to be reachable only by its application, but reachable, and by name. Both say "isolate it", and these are not the same request at all.

The answer to one is an outage for the other. A container with no network at all makes the database unusable, and a confined network still lets the neighbors through. This page teaches you to set up each of the two isolations with docker run --network none and docker network create --internal, then to check from the inside that they hold, which proves more than reading the configuration.

The containers and the network in the examples carry French names, which are plain identifiers: balise is the control service (a "beacon"), analyseur the container to isolate, cloison the internal network (a "partition"), and $HOTE the variable that holds the host's address.

What you will learn

  • Tell the two isolations apart: no network at all, or a network that does not lead out
  • Create an internal network with the subnet of your choice (the range of addresses the network hands out to its members)
  • Observe the missing route that makes the isolation, instead of believing it
  • Spot the two-network trap: a container can belong to both
  • Measure isolation by what the container actually receives

Prerequisites

An installed engine (installing Docker), the basic verbs (the Docker command line), and the notion of a dedicated network with name resolution, covered in Docker networks. The commands on this page are written for Docker Engine 29.9.2 and the alpine:3.24 image, whose busybox shell provides ip and nc. The measurements also query the control service from the host, so you need nc on the host itself (the netcat-openbsd package on Debian 13).

What are the two ways to isolate a container?

You can remove the network from the container, or place it on a network that leads nowhere. The first case is set with --network none: the container keeps only its loopback interface (lo), which only lets it talk to itself. The second case is set with a network created by docker network create --internal: its members have an interface, resolve each other by name, and have no route to the outside.

The diagram puts the three situations in front of the same control service, a service published on the machine's address, which stands in for everything outside the containers' network. Look at who reaches the control service, and who reaches its neighbor.

Three situations facing a control service published on the host: a container on the default network gets out and reaches it; a container with no network has no interface other than loopback and does not reach it; two containers on an internal network talk to each other by name but have no route to the control service.Internal network cloisonNo network (none)Default networkgoes outno interfacetalk by nameno routebalisethe witness, published on the hostordinary containeranalyseurloopback onlyregistrelecteur
Source · SVG
Esc to close, + and − to zoom, scroll to move around. 100 %
PostureNon-loopback interfacesReaches the outsideReaches a neighbor by name
default networkoneyesno
--network nonenonenonot applicable
internal networkonenoyes

The last row explains the choice. If the container must talk to others, none is too strong. If it must talk to nobody, an internal network is too weak, since its neighbors can reach it.

How do you cut a container off from the network completely?

Create it with --network none. For the measurement to mean anything, you first set up a control service: a container that answers one fixed line, published on port 18310 of the machine. Without a control service, "the container reaches nothing" would be just as true if the control service had disappeared.

Terminal window
docker run -d --name balise -p 18310:8080 alpine:3.24@sha256:be2656061b536c1883c4034790e21d0a09c5aaaf51968b89368a3c0371ea2c9a \
sh -c 'while true; do echo signal=balise | nc -l -p 8080; done'
HOTE=$(ip -o -4 route get 1.1.1.1 | awk '{for (i = 1; i <= NF; i++) if ($i == "src") print $(i + 1)}')
nc -w 3 "$HOTE" 18310

The second command derives the machine's routable address from its default route, instead of hard-coding it. The control service must answer from the host before anything else.

signal=balise

Now create the container with no network, and look at it from the inside.

Terminal window
docker run -d --name analyseur --network none alpine:3.24@sha256:be2656061b536c1883c4034790e21d0a09c5aaaf51968b89368a3c0371ea2c9a sleep infinity
docker exec analyseur ip -o link
docker exec analyseur nc -w 3 "$HOTE" 18310
echo "nc exit code = $?"
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN qlen 1000\ link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
nc exit code = 1

The output shows two independent observations, and you need to make both. The first reads the configuration: only lo is left. The second tests the behavior: the container does not receive the control service's line, and nc returns code 1. They agree here, but one can mislead where the other does not: the section on the two networks below gives a case. You also have to read the answer, not just note that the connection is established: a published port sometimes accepts the connection before anything is listening behind it.

How do you confine a group of containers without cutting them off from each other?

Create a network declared internal, with an explicit subnet: a network created without a plan gets a subnet that the engine picks on its own, which you avoid in production.

Terminal window
docker network create --internal --subnet 172.30.14.0/24 cloison
docker network inspect cloison --format 'internal={{.Internal}} subnet={{(index .IPAM.Config 0).Subnet}}'
internal=true subnet=172.30.14.0/24

Then start two containers directly on it, without going through the default network.

Terminal window
docker run -d --name registre --network cloison alpine:3.24@sha256:be2656061b536c1883c4034790e21d0a09c5aaaf51968b89368a3c0371ea2c9a \
sh -c 'while true; do echo signal=registre | nc -l -p 7000; done'
docker run -d --name lecteur --network cloison alpine:3.24@sha256:be2656061b536c1883c4034790e21d0a09c5aaaf51968b89368a3c0371ea2c9a sleep infinity
docker exec lecteur nc -w 3 registre 7000
docker exec lecteur nc -w 3 "$HOTE" 18310
echo "nc exit code to the control service = $?"
signal=registre
nc exit code to the control service = 1

The first exchange succeeds because a user-created network, internal or not, resolves the names of its members; the lesson on Docker networks details this built-in DNS. The second fails, and the reason can be read in the container's routing table.

Why does an internal network never let traffic out?

An internal network is not just a filter placed in front of the containers: it rests first on the absence of a path. No default route is configured in a member. The default route is the line of the routing table that says where to send any packet with no more specific route; without it, the kernel has nowhere to send a packet bound for the outside. The docker network create documentation adds that the engine also installs firewall rules that reject traffic between this network and the others. Compare with the default network.

Terminal window
docker exec lecteur ip route
docker run --rm alpine:3.24@sha256:be2656061b536c1883c4034790e21d0a09c5aaaf51968b89368a3c0371ea2c9a ip route
172.30.14.0/24 dev eth0 scope link src 172.30.14.3
default via 172.17.0.1 dev eth0
172.17.0.0/16 dev eth0 scope link src 172.17.0.3

The first command shows only the route of the 172.30.14.0/24 subnet; the second shows the next two lines, including a default line toward the default network's gateway. The absence of a default line is enough to explain the failure observed. This is the first of the two protections, the one about routes, and the easiest to observe: you read it inside the container. The other is the engine's firewall, which you do not read from the container.

Two limits follow. This mechanism says nothing about what happens between members of the same internal network, which can all reach each other. And the network is not cut off from the host: according to the documentation, the host can directly reach the IP address of each member, and the network's gateway address stays reachable from the members. Confinement stops the members from getting out toward other networks; it protects them neither from each other nor from the host.

Why does my confined container still get out?

Because a container can belong to two networks at once. The internal network adds a membership; it does not remove the default network's. A container attached to both keeps the second one's route and gets out as before.

The action is therefore twofold: be on the internal network, and have left the other one. The measurement reuses the objects of the page. A new member of cloison, passerelle (a "gateway"), is attached in addition to the default network (bridge): the control service balise and the network cloison already exist, and $HOTE still holds the machine's address.

Terminal window
docker run -d --name passerelle --network cloison alpine:3.24@sha256:be2656061b536c1883c4034790e21d0a09c5aaaf51968b89368a3c0371ea2c9a sleep infinity
docker network connect bridge passerelle
docker container inspect passerelle --format '{{range $reseau, $v := .NetworkSettings.Networks}}{{println $reseau}}{{end}}'
docker exec passerelle nc -w 3 "$HOTE" 18310
echo "nc exit code to the control service = $?"
bridge
cloison
signal=balise
nc exit code to the control service = 0

The list of networks does contain cloison, and yet passerelle receives the control service's line: the configuration announces a confined container, the behavior shows the opposite. Now detach it from the default network, then repeat both observations.

Terminal window
docker network disconnect bridge passerelle
docker container inspect passerelle --format '{{range $reseau, $v := .NetworkSettings.Networks}}{{println $reseau}}{{end}}'
docker exec passerelle ip route
docker exec passerelle nc -w 3 "$HOTE" 18310
echo "nc exit code to the control service = $?"
docker exec passerelle nc -w 3 registre 7000
cloison
172.30.14.0/24 dev eth0 scope link src 172.30.14.4
nc exit code to the control service = 1
signal=registre

Only cloison remains in the list (the empty line comes from the println placed before the final line break), the default route has disappeared, the probe toward the control service fails with code 1 without printing any message (busybox's nc is silent in this case), and passerelle still reaches registre by name, since the isolation does not affect internal exchanges. The simplest and most readable approach is still to recreate the container under the same name with --network cloison from the start, which avoids the window during which it belongs to both.

How do you diagnose isolation that does not hold?

Four symptoms come up. The first is a "network-less" container that still reaches the control service. It was often recreated without the option, or it uses another container's network (--network container:<name>): read docker container inspect analyseur --format '{{.HostConfig.NetworkMode}}' and compare with none.

The second is two members of the internal network that do not talk to each other. The cause is a misspelled name or a container left on another network: a name only resolves between members of the same user-defined network.

The third is a member of the internal network that still gets out. It is almost always attached to a second network as well: list its networks with the docker container inspect command from the previous section, then detach it from the other one with docker network disconnect.

The fourth is a silent control service, which makes every measurement worthless: check it from the host before concluding anything.

Putting it into practice

The lab provides containers that all get out, including a control service you must not touch, and asks you to obtain both isolations on the same machine. One container must end up with no network interface other than loopback. A group of two containers must be confined on an internal network whose subnet is imposed, reach each other by name, and never reach the control service. The validation reads the state of the system, never your commands: it looks at the interfaces inside the container, tests what it receives from the control service, checks that the network is internal with the right subnet, and verifies that no member reaches the control service.

How do you clean up after the measurements?

The control service publishes port 18310 of the machine as long as it runs: delete it with the other containers of the page, then the network, which can only disappear once it has no members left.

Terminal window
docker rm -f balise analyseur registre lecteur passerelle
docker network rm cloison

The names of the containers are printed, then that of the network. If you did the lab, its containers and its network carry the names that the lab imposes: delete them the same way, reading their names in docker ps -a and docker network ls.

Key takeaways

  1. There are two isolations: no network (--network none) for a container with no one to talk to, an internal network for a group whose members talk to each other without getting out.

  2. A container with no network keeps only lo: no useful interface, no neighbor, no way out.

  3. An internal network is created with --internal and is best given an explicit --subnet rather than a subnet chosen by the engine.

  4. The isolation rests first on a missing route, which the engine backs up with firewall rules: ip route in a member shows no default line, and the host can still reach the members.

  5. The members of an internal network resolve each other by name, like on any user-defined network.

  6. A container can belong to two networks: being on the internal network is not enough, it must also have left the other one.

  7. You measure the answer, not the connection, against a control service you have checked is answering, otherwise the isolation "succeeds" because there is nothing left to reach.

Knowledge check

Check that you have grasped the essentials of this guide. The questions cover only what has just been explained here.

Knowledge check

Check what you have learned with this interactive quiz

6 questions
6 min
70% to pass

How it works

  • The timer starts when you click Start
  • Multiple choice, true/false and short answer questions
  • You can move between questions
  • Detailed results are shown at the end

Starts the quiz and the timer

Is this project useful to you?

The guides, courses, quizzes and labs stay free. If you use them regularly, you can contribute to their upkeep and development.

Become a supporting member (new tab) One-time support (new tab) Learn more

Follow the project on LinkedIn