In lesson 03-05 you used Docker networks as a black box: you created a network, connected containers and the names resolved. It worked, but you did not know why. This lesson opens that box: Linux bridges, veth pairs, iptables rules, a DNS server hidden at 127.0.0.11, and drivers that give a container its own IP on your physical LAN.
Everything that follows is demonstrated against the running Aurora Libros platform. The host commands assume Linux; if you use Docker Desktop, they run inside the virtual machine (lesson 05-07).
Contents
- Anatomy of a bridge network: Linux bridges
vethpairs and how to match the two ends- A packet's journey, step by step
iptables: the chains Docker creates- Outbound NAT and publishing DNAT
- Bypassing the host firewall (security warning)
DOCKER-USER: taking back control- The embedded DNS at
127.0.0.11 - Addressing: subnets, ranges, fixed IPs and MTU
- The
hostandnonedrivers in depth macvlan: an IP of your own on the physical LANipvlanin L2 and L3 mode- Comparison table of the six drivers
- IPv6 in Docker
- Real diagnostics:
tcpdumpandnsenter
- Anatomy of a bridge network: Linux bridges
A Docker bridge network is a software bridge in the Linux kernel: a virtual Ethernet switch. The default network uses the docker0 bridge; every user-defined network creates one called br-<first 12 characters of the network ID>.
docker network ls --filter driver=bridge --format "{{.ID}}\t{{.Name}}"
ip -brief link show type bridge9f3a1c7d5e2b aurora-libros_frontend
c81be4f0a7d9 aurora-libros_backend
docker0 UP 02:42:8e:1c:44:a1 <BROADCAST,MULTICAST,UP,LOWER_UP>
br-9f3a1c7d5e2b UP 02:42:3b:9c:10:7e <BROADCAST,MULTICAST,UP,LOWER_UP>
br-c81be4f0a7d9 UP 02:42:af:22:d5:03 <BROADCAST,MULTICAST,UP,LOWER_UP>The mapping is direct and easy to verify: the twelve characters after br- are the beginning of the network ID. The bridge has its own IP address on the host, and that address is the gateway for the containers on that network:
ip -brief addr show br-9f3a1c7d5e2b
docker network inspect aurora-libros_frontend --format '{{(index .IPAM.Config 0).Subnet}} gw={{(index .IPAM.Config 0).Gateway}}'And bridge link show master br-9f3a1c7d5e2b lists the interfaces plugged into that bridge: one for every container attached to the network.
veth pairs and how to match the two ends
veth pairs and how to match the two endsA veth (virtual Ethernet) is a pair of interfaces joined by a virtual cable: whatever goes in one end comes out the other. Docker creates one per container-network connection: one end stays on the host and is plugged into the bridge; the other is moved inside the container and renamed eth0. The pairing is not obvious, because the names are random; the key is the interface index, since each end points at the other's index through iflink.
docker compose exec aurora-api cat /sys/class/net/eth0/iflink # -> 7
ip link show | grep '^7:' # which interface is number 7?There is your pair: eth0 in aurora-api ↔ veth3a1f9c2 on the host. The @if6 notation in the name itself confirms the symmetry: the host end points at index 6, which is the container's eth0 inside its own network namespace.
Once you have identified the pair, you can measure a specific container's traffic from the host without entering it:
Look at peer_ifindex: it is the most direct way to confirm the pairing. And watch out for the reversed direction: what the container sends shows up as rx on the host's veth, because the packet enters the host through that end.
- A packet's journey, step by step
flowchart LR
subgraph NSAPI["aurora-api network namespace"]
P["node process<br/>172.21.0.5"] --> E0["eth0 (if6)"]
end
E0 -->|"veth cable"| V["veth3a1f9c2 (if7)"]
subgraph HOST["Host"]
V --> BR["br-9f3a1c7d5e2b<br/>172.21.0.1"]
BR --> IPT["iptables: nat + filter"]
IPT --> ETH["host eth0"]
end
BR -->|"same bridge:<br/>direct switching"| V2["veth5d80a3f"]
V2 --> DB["aurora-db 172.22.0.3"]
ETH --> NET(("Internet"))
Two very different paths coexist here:
| Destination | Route | Cost |
|---|---|---|
| Another container on the same network | eth0 → veth → bridge → veth → eth0 |
Kernel switching, no NAT |
| Another network or the Internet | eth0 → veth → bridge → routed → iptables (MASQUERADE) → host eth0 |
NAT and connection tracking |
| From outside towards a published port | host eth0 → iptables (DNAT) → bridge → veth → eth0 |
Destination NAT |
The fact that aurora-api ↔ aurora-db traffic is pure switching explains why its performance is practically that of the host network: there is no address translation on that path.
iptables: the chains Docker creates
iptables: the chains Docker createsOn startup, dockerd installs a set of chains of its own. Knowing them by name saves you hours of debugging:
| Chain | Table | Purpose |
|---|---|---|
DOCKER |
nat and filter |
DNAT rules for published ports and their forwarding permission |
DOCKER-USER |
filter |
Empty and reserved for you; evaluated before DOCKER |
DOCKER-ISOLATION-STAGE-1 |
filter |
Detects traffic leaving one bridge for another |
DOCKER-ISOLATION-STAGE-2 |
filter |
Drops it: this is what isolates networks from each other |
DOCKER-INGRESS |
nat and filter |
Only with Swarm (lesson 06-03) |
num target prot opt source destination
1 DROP all -- 0.0.0.0/0 0.0.0.0/0
2 RETURN all -- 0.0.0.0/0 0.0.0.0/0There it is, in two lines: the isolation you verified back in lesson 04-04 when aurora-web could not reach aurora-db. A packet that leaves one bridge and tries to enter another lands on that DROP.
- Outbound NAT and publishing DNAT
Chain PREROUTING (policy ACCEPT)
DOCKER all -- 0.0.0.0/0 0.0.0.0/0 ADDRTYPE match dst-type LOCAL
Chain POSTROUTING (policy ACCEPT)
MASQUERADE all -- 172.21.0.0/16 0.0.0.0/0
MASQUERADE all -- 172.22.0.0/16 0.0.0.0/0
MASQUERADE tcp -- 172.21.0.2 172.21.0.2 tcp dpt:80
Chain DOCKER (2 references)
DNAT tcp -- 0.0.0.0/0 0.0.0.0/0 tcp dpt:8080 to:172.21.0.2:80Three rules, three mechanisms:
- Subnet
MASQUERADE: anything leaving172.21.0.0/16for the outside world is rewritten with the host's IP. That is why a container can download from the Internet without having a public IP. - Publishing
DNAT: the-p 8080:80onaurora-webbecomes this rule. Any packet arriving at port 8080 on any local address is redirected to172.21.0.2:80. MASQUERADEwith identical source and destination: so-called hairpin NAT, which lets a container reach itself through the host's published port.
Check the correspondence with the actual publication:
If you publish while restricting the interface —"127.0.0.1:8080:80"— the rule changes and adds -d 127.0.0.1. That one-word difference is what separates "reachable from my laptop" from "reachable from the entire office network".
- Bypassing the host firewall (security warning)
This is the most important point in the lesson, and the one that causes the nastiest surprises in production.
When you publish a port, Docker inserts its DNAT into PREROUTING, which is evaluated before the rules in the INPUT chain where ufw or firewalld put theirs. The redirected traffic goes through FORWARD, not through INPUT. The result: your firewall never sees that traffic and never blocks it.
sudo ufw status | head -4
docker run -d --name exposed -p 5000:80 nginx:alpine
curl -s -o /dev/null -w '%{http_code}\n' http://<host-IP>:5000/The firewall says only port 22 is open. Port 5000 answers anyway from any machine on the network. Entire PostgreSQL and Redis servers have ended up on the Internet in exactly this way.
The three defenses, from simplest to most complete:
| Defense | How | Scope |
|---|---|---|
| Publish on localhost only | "127.0.0.1:5432:5432" |
Prevents remote access at the root. Always the first option |
| Do not publish at all | Use internal networks and docker compose exec |
What aurora-db has been doing since lesson 04-04 |
Rules in DOCKER-USER |
Filter forwarded traffic | Fine-grained control by source |
Security warning. Port exposure and firewall rules on a server must be reviewed with the security or compliance officer in your organization. What is shown here explains the mechanism; the actual policy —what gets exposed, to whom, and with which compensating controls— is not decided single-handedly by whoever writes the
compose.yaml.
DOCKER-USER: taking back control
DOCKER-USER: taking back controlDocker never touches DOCKER-USER and evaluates it before its own rules. It is the place designed for your policies:
# Allow the corporate network only; drop everything else
sudo iptables -I DOCKER-USER -i eth0 -j DROP
sudo iptables -I DOCKER-USER -i eth0 -s 10.0.0.0/8 -j RETURN
sudo iptables -L DOCKER-USER -n --line-numbersnum target prot opt source destination
1 RETURN all -- 10.0.0.0/8 0.0.0.0/0
2 DROP all -- 0.0.0.0/0 0.0.0.0/0Two essential warnings. First: order matters, -I inserts at the top, so the permissive rule must be inserted last in order to end up first. Second: these rules do not survive a reboot; you have to persist them with iptables-persistent, nftables or your distribution's tool. And if you are thinking of disabling Docker's iptables handling altogether ("iptables": false in /etc/docker/daemon.json), accept that you lose outbound Internet access for containers and port publishing until you write every rule yourself: it is rarely worth it.
- The embedded DNS at
127.0.0.11
127.0.0.11On a user-defined network, service names resolve because Docker runs a per-container DNS server listening on 127.0.0.11:53.
docker compose exec aurora-api cat /etc/resolv.conf
docker compose exec aurora-api getent hosts aurora-dbThat loopback address does not exist on any visible interface. The trick: inside the container's namespace there is a DNAT rule (-A DOCKER_OUTPUT -d 127.0.0.11/32 -p tcp --dport 53 -j DNAT --to-destination 127.0.0.11:38765) that redirects the traffic to a high port where a daemon process is listening.
The resolution algorithm, in order:
- Is it a service name or alias on a network we share? → the daemon answers with the container's IP.
- Is it a container on the same network, by name or
container_name? → same thing. - If not, it forwards to the host's DNS servers (or to those in the service's
dns:setting).
Points worth being clear about:
- Resolution is scoped to shared networks.
aurora-webdoes not resolveaurora-dbbecause they share no network: that is not a DNS failure, it is the design. - With
--scale, one name returns several addresses in rotating order: rudimentary client-side load balancing. Check it withdocker compose up -d --scale aurora-api=3followed bygetent hosts aurora-api. options ndots:0prevents search suffixes from being appended and saves failed lookups.- Many libraries cache the resolution. If a container is recreated and changes IP, an application that resolved once may end up talking to a dead IP. That is a real reason to restart clients after recreating a service.
- Addressing: subnets, ranges, fixed IPs and MTU
By default Docker picks subnets from 172.17.0.0/16 onwards, which clashes with many corporate VPNs. You can pin them down:
docker network create --driver bridge \
--subnet 10.80.0.0/24 --gateway 10.80.0.1 --ip-range 10.80.0.128/25 \
--aux-address "reserved-nas=10.80.0.20" \
--opt com.docker.network.driver.mtu=1450 \
--opt com.docker.network.bridge.name=br-aurora \
aurora-fixed| Option | What it does |
|---|---|
--subnet |
The network's full range |
--gateway |
The bridge's IP on the host |
--ip-range |
The subset Docker assigns automatically; the rest is left free for fixed IPs |
--aux-address |
Addresses Docker must not assign (real machines inside the range) |
mtu |
Maximum frame size; lowering it avoids fragmentation behind VPNs or tunnels |
bridge.name |
A readable bridge name instead of br-<id> |
Assigning a fixed IP requires the network to have its own subnet: docker run -d --network aurora-fixed --ip 10.80.0.10 nginx:alpine. In Compose you declare it with ipv4_address inside networks. Even so, the good practice is still to rely on DNS names rather than IPs: fixed IPs are reserved for cases where something external (a firewall rule, a license, a legacy appliance) demands a stable address.
The default MTU is set in /etc/docker/daemon.json with "mtu": 1450. The typical symptom of a badly tuned MTU is cruel: connections are established and small requests work, but large responses hang.
- The
host and none drivers in depth
host and none drivers in depthhost removes the network namespace: the container uses the host's stack directly.
docker run -d --name api-host --network host auroralibros/aurora-api:1.2.0
docker exec api-host ip -brief addr | head -2 && ss -tlnp | grep 3000The process listens directly on the host's port 3000. What you gain and what you lose:
| You gain | You lose |
|---|---|
| No NAT: lower latency and more throughput on workloads with many small packets | Network isolation: none at all |
| Access to real interfaces (useful for monitoring, VPN, DHCP) | Service-name resolution: it does not exist |
| No practical limit on published ports | Port collisions between containers |
| Multicast and protocols that NAT breaks | -p is ignored with a warning |
Legitimate cases: monitoring agents (node-exporter, lesson 05-06), very high performance load balancers and diagnostic tools. For aurora-api it would be a mistake: it would lose isolation without gaining anything noticeable.
none leaves the container with nothing but lo: docker run --rm --network none alpine:3 ping -c1 -W1 8.8.8.8 answers Network is unreachable. It is the right choice for pure computation processes or for handling files of dubious origin: no network, no exfiltration.
macvlan: an IP of your own on the physical LAN
macvlan: an IP of your own on the physical LANmacvlan gives the container its own MAC address on the physical network. To the rest of the office, the container is just another machine: it gets an IP from the real range and needs neither NAT nor port publishing.
# 1) Find out the real physical interface and subnet
ip -brief addr show eth0
# 2) Create the macvlan network on top of that interface
docker network create -d macvlan \
--subnet 192.168.1.0/24 \
--gateway 192.168.1.1 \
--ip-range 192.168.1.240/28 \
-o parent=eth0 \
office-lan
# 3) A container with a LAN IP
docker run -d --name web-lan --network office-lan --ip 192.168.1.241 nginx:alpine
docker exec web-lan ip -brief addr show eth0From another machine in the office, http://192.168.1.241/ answers without a single port having been published.
The three traps, in order of frequency:
- The host cannot talk to its own macvlan containers. This is not a bug: the kernel prevents traffic between the parent interface and its macvlan child interfaces. The fix is to create an extra macvlan interface on the host and route towards it:
sudo ip link add macvlan-host link eth0 type macvlan mode bridge
sudo ip addr add 192.168.1.250/32 dev macvlan-host && sudo ip link set macvlan-host up
sudo ip route add 192.168.1.240/28 dev macvlan-host- Promiscuous mode. The switch or the hypervisor must accept several MACs per port. On most cloud services (and on many Wi-Fi networks) it is forbidden, and macvlan simply does not work.
- Collision with DHCP. Docker does not speak DHCP: reserve an
--ip-rangeoutside the DHCP server's scope or you will end up with two machines sharing an IP.
ipvlan in L2 and L3 mode
ipvlan in L2 and L3 modeipvlan solves the promiscuous mode problem: all containers share the parent interface's MAC and are told apart by IP.
# L2: same broadcast domain as the host, no MAC of its own
docker network create -d ipvlan --subnet 192.168.1.0/24 --gateway 192.168.1.1 \
-o parent=eth0 -o ipvlan_mode=l2 lan-l2
# L3: the host routes; no broadcast, independent subnet
docker network create -d ipvlan --subnet 10.90.1.0/24 \
-o parent=eth0 -o ipvlan_mode=l3 lan-l3| Aspect | macvlan |
ipvlan L2 |
ipvlan L3 |
|---|---|---|---|
| MAC | One per container | Shared with the parent | Shared |
| Promiscuous mode | Required | No | No |
| Broadcast and DHCP | Yes | Yes | No |
| Routing | Via the LAN gateway | Via the LAN gateway | Done by the host |
| External routes | Automatic | Automatic | Must be added on the router |
L3 mode is the cleanest option in data centers with controlled routing, but it requires somebody to teach the router how to reach 10.90.1.0/24. If you skip that, the containers get out and nothing comes back.
- Comparison table of the six drivers
| Driver | Isolation | Performance | Internal DNS | Port publishing | Typical use case |
|---|---|---|---|---|---|
bridge (user-defined) |
High | Good (NAT on the way out) | Yes | -p with DNAT |
95% of cases, Aurora Libros included |
default bridge |
Medium | Same | No (only --link, deprecated) |
-p |
Compatibility; avoid it |
host |
None | The best | No | Not applicable | Monitoring agents, high performance |
none |
Total | Not applicable | No | No | Processes with no network |
macvlan |
Medium (flat LAN) | Very good, no NAT | Yes, on the Docker network | Unnecessary | Integrating with LAN machines or legacy systems |
ipvlan |
Medium | Very good | Yes | Unnecessary | Like macvlan where promiscuous mode is forbidden |
The overlay driver, for connecting containers across several hosts, is covered in lesson 06-03 along with Swarm's routing mesh.
- IPv6 in Docker
Since version 27, IPv6 is enabled per network without touching the daemon:
docker network create --ipv6 --subnet 2001:db8:aur::/64 aurora-v6
docker run --rm --network aurora-v6 alpine:3 ip -6 -brief addr show eth0
# eth0 UP 2001:db8:aur::2/64 fe80::42:acff:fe14:2/64In Compose, enable_ipv6: true on the network is enough. Three warnings: use ULA addresses (fd00::/8) if you do not have a delegated public prefix; enable "ip6tables": true in /etc/docker/daemon.json so that NAT and publishing work; and check that your firewall rules exist for IPv6 too, because an IPv4-only policy leaves an open door that nobody is watching.
- Real diagnostics:
tcpdump and nsenter
tcpdump and nsenterThe nicolaka/netshoot image with --network container: (lesson 03-04) puts you inside the container's network namespace with a full toolbox:
docker run --rm -it --network container:aurora-libros-aurora-api-1 nicolaka/netshoot \
sh -c 'ip -brief addr; ip route; ss -tlnp'Two interfaces, because aurora-api sits on both frontend and backend; the default route leaves via frontend, since backend is internal and has no gateway.
Capturing the real traffic between the API and the database:
docker run --rm --net container:aurora-libros-aurora-api-1 nicolaka/netshoot \
timeout 10 tcpdump -n -i any 'host 172.22.0.3 and port 5432' -c 6IP 172.22.0.4.51442 > 172.22.0.3.5432: Flags [S], seq 2451
IP 172.22.0.3.5432 > 172.22.0.4.51442: Flags [S.], seq 8891, ack 2452
IP 172.22.0.4.51442 > 172.22.0.3.5432: Flags [P.], length 96
IP 172.22.0.3.5432 > 172.22.0.4.51442: Flags [P.], length 1284The addresses belong to the backend network and there is no translation whatsoever: direct communication through the bridge, exactly as section 3 predicted. From the host, without entering any container, the equivalent capture is sudo tcpdump -n -i veth3a1f9c2 -c 5 'tcp port 5432'.
nsenter is the lowest-level route: it enters a process's network namespace using the host's tools.
pid=$(docker inspect --format '{{.State.Pid}}' aurora-libros-aurora-api-1)
sudo nsenter -t "$pid" -n ip -brief addr
sudo nsenter -t "$pid" -n ss -tnp state establishedThis is the technique that saves the day when a container is distroless and does not even have sh (lesson 05-03): you need nothing inside, because you use the host's binaries against its namespace. Exactly why this works is explained in lesson 05-07.
Common Mistakes and Tips
Believing that the host firewall protects published ports. It does not: Docker's DNAT is evaluated first. Publish on 127.0.0.1 or filter in DOCKER-USER.
Adding rules to INPUT to block a container. The traffic goes through FORWARD. The right chain is DOCKER-USER.
Writing rules inside the DOCKER chain. Docker rewrites it on startup or whenever a port is published, and your rules vanish.
Relying on container IPs. They change when containers are recreated. Use service names and reserve fixed IPs for genuine external requirements.
Using --network host "to make it faster". You lose isolation and name resolution, and on an HTTP API the difference is usually negligible. Measure first.
Setting up macvlan without checking promiscuous mode, or forgetting to lower the MTU behind a VPN: in the first case everything looks properly configured and nothing answers; in the second, connections open but hang when transferring large amounts of data.
Tip: faced with a networking problem, always follow the same order: does the name resolve? (getent hosts), is there a route? (ip route), is anybody listening? (ss -tlnp inside the destination), does the packet arrive? (tcpdump). Each step rules out an entire layer.
Exercises
Exercise 1. With the platform running, identify the veth pair for aurora-db, prove that its host end is plugged into the backend network's bridge, and measure the traffic that has passed through it. Explain why rx on the host corresponds to what the container sends.
Exercise 2. Demonstrate the firewall bypass: publish a container on a port, check that ufw does not declare it open and that it answers from another machine anyway; then block it properly with DOCKER-USER and verify that it still works from localhost.
Exercise 3. Use tcpdump to capture a real query between aurora-api and aurora-db triggered by curl /books, and use docker exec against Redis to prove why the second request generates no traffic towards the database.
Solutions
Solution 1.
idx=$(docker compose exec -T aurora-db cat /sys/class/net/eth0/iflink | tr -d '\r')
veth=$(ip -o link | awk -v i="$idx:" '$1==i {sub(/@.*/,"",$2); print $2}')
net=$(docker network inspect aurora-libros_backend --format '{{.Id}}' | cut -c1-12)
echo "veth=$veth expected bridge=br-$net"
ip -o link show "$veth" | grep -o 'master [^ ]*'
ethtool -S "$veth" | grep -E 'peer_ifindex|rx_packets|tx_packets'veth=veth5d80a3f expected bridge=br-c81be4f0a7d9
master br-c81be4f0a7d9
peer_ifindex: 10
rx_packets: 3921
tx_packets: 4877The master matches exactly the bridge derived from the backend network's ID: that veth is aurora-db's cable to that network and to no other. And peer_ifindex: 10 points at the index of the eth0 inside the container.
About the reversed direction: the statistics are read from the host's point of view. When aurora-db answers a query, the packet leaves through its eth0, travels along the virtual cable and enters the host through veth5d80a3f; for the host that is reception, hence rx. Confusing this leads to inverted diagnoses: if you see rx spiking on a database's veth, it does not mean a lot is being sent to it, it means it is returning a lot, and there is probably a missing LIMIT in some query.
Solution 2.
docker run -d --name exposed -p 5000:80 nginx:alpine
sudo ufw status | grep -c 5000 # 0: ufw knows nothing about that port
curl -s -o /dev/null -w 'host: %{http_code}\n' http://127.0.0.1:5000/
# from another machine: curl -s -o /dev/null -w 'remote: %{http_code}\n' http://192.168.1.42:5000/
sudo iptables -I DOCKER-USER -i eth0 -p tcp --dport 5000 -j DROP
curl -s -o /dev/null -w 'host after the rule: %{http_code}\n' http://127.0.0.1:5000/
# from another machine: curl -m 3 http://192.168.1.42:5000/
sudo iptables -D DOCKER-USER -i eth0 -p tcp --dport 5000 -j DROP && docker rm -f exposed0
host: 200
remote: 200
host after the rule: 200
curl: (28) Connection timed out after 3001 millisecondsThe rule filters by incoming interface (-i eth0), so traffic arriving through the physical interface is dropped while traffic originating on the host itself —which therefore does not come in through eth0— still gets through. That nuance is what makes the rule usable: it blocks the outside world without breaking local debugging.
The underlying lesson: ufw reported a system with a single open port and it was false. Publishing a port on a server with a public IP is equivalent to opening it to the Internet, unless you bind the publication to 127.0.0.1 or filter explicitly. That is exactly why aurora-db has published nothing since lesson 04-04.
Solution 3.
docker compose exec aurora-cache redis-cli FLUSHALL
docker run --rm --net container:aurora-libros-aurora-api-1 nicolaka/netshoot \
timeout 12 tcpdump -n -i any 'host 172.22.0.3 and port 5432' &
sleep 1
curl -s http://localhost:8080/api/books | head -c 58; echo # first: cache is empty
curl -s http://localhost:8080/api/books | head -c 58; echo # second: warm cache
docker compose exec aurora-cache redis-cli KEYS 'books:*'
wait{"source":"db","total":9,"books":[{"id":1,"title":"El
{"source":"cache","total":9,"books":[{"id":1,"title":
1) "books:all"
IP 172.22.0.4.51488 > 172.22.0.3.5432: Flags [P.], length 112
IP 172.22.0.3.5432 > 172.22.0.4.51488: Flags [P.], length 1284
2 packets capturedThe first request returns source: db and the capture shows the conversation with PostgreSQL on port 5432, with untranslated addresses from the backend network. The second returns source: cache and adds not a single packet to the capture: the cache-aside pattern finds the books:all key in Redis and never even opens a query.
Two useful conclusions. The first, architectural: tcpdump has just turned something that until now was a JSON field you had to trust into an observable fact. The second, diagnostic: if one day /books always answered source: db, this very capture would tell you whether the problem is in writing the key to Redis or in reading it, without touching a line of code.
Conclusion
Docker networking has stopped being magic. You know that a bridge network is a Linux bridge —docker0 or br-<id>— with one veth end per container plugged into it, and you know how to match the two ends with iflink and ethtool -S to measure a specific container's traffic from the host. You know a packet's two possible paths: pure switching between containers on the same network, and MASQUERADE when it heads outside. And you have read the real rules: the DOCKER chain with its DNAT for every -p, and DOCKER-ISOLATION-STAGE-2 with the two-line DROP that explains all the network isolation you have been using since module 3.
You also take away a warning worth more than many configurations: publishing a port bypasses the host firewall, because the DNAT is evaluated before INPUT; the defense is to publish on 127.0.0.1, not to publish at all, or to write rules in DOCKER-USER, the only chain Docker respects. You know how the embedded DNS at 127.0.0.11 really resolves a name and why resolution is scoped to shared networks; you control addressing (--subnet, --ip-range, --aux-address, mtu) so as to coexist with VPNs and real machines; you know the six drivers in depth, including macvlan with its promiscuous-mode trap and ipvlan in L2 and L3; and you diagnose with netshoot, ss, tcpdump on the real traffic between aurora-api and aurora-db, and nsenter when there is not even an sh inside the container.
The next layer to open is the data layer. In lesson 05-02 you will look at storage in depth: what a storage driver is and how overlay2 works internally with its lowerdir, upperdir and merged; how much copy-on-write really costs —measured— and why that forces a database to use volumes; volume drivers and their options, including NFS and volumes shared between stacks; and a backup strategy for Aurora Libros with tested restores, because a backup that has never been restored is not a backup.
Docker: From Beginner to Advanced
Module 1: Introduction to Docker
- What Is Docker?
- Installing Docker
- Docker Architecture
- Basic Docker Commands
- Understanding Docker Images
- Creating Your First Docker Container
- The Course Project: The Aurora Libros Platform
Module 2: Working with Docker Images
- Docker Hub and Repositories
- Building Docker Images
- Dockerfile Basics
- Advanced Dockerfile Instructions
- Managing Docker Images
- Tagging and Publishing Images
Module 3: Docker Containers
- Running Containers
- Container Lifecycle
- Managing Containers
- Inspecting and Debugging Containers
- Docker Networking
- Data Persistence with Volumes
- Resource Limits and Restart Policies
Module 4: Docker Compose
- Introduction to Docker Compose
- Defining Services in Docker Compose
- Docker Compose Commands
- Multi-Container Applications
- Environment Variables in Docker Compose
- Profiles, Overrides and Multiple Environments
- Local Development with Docker Compose
Module 5: Advanced Docker Concepts
- Docker Networking Deep Dive
- Docker Storage Options
- Docker Security Best Practices
- Optimizing Docker Images
- Advanced Builds with BuildKit and Buildx
- Logging and Monitoring in Docker
- The Runtime Inside: Namespaces, Cgroups and Layers
Module 6: Docker in Production
- Preparing an Image for Production
- CI/CD with Docker
- Orchestrating Containers with Docker Swarm
- Introduction to Kubernetes
- Deploying Docker Containers in Kubernetes
- Scaling and Load Balancing
- Deployment Strategies and Rollback
