/24 what does it mean?
0 or a 1. To find the total number of unique combinations, you calculate 2⁸, which equals 256. This means an 8-bit number can represent 256 distinct values.- The lowest possible value is all zeros:
00000000(which equals 0). - The highest possible value is all ones:
11111111(which equals 255).
0 to 255, you get exactly 256 total numbers.- The lowest possible 8-bit number is
00000000(which is 0). - The highest possible 8-bit number is 11111111 (which is \(128 + 64 + 32 + 16 + 8 + 4 + 2 + 1 = 255)
addresses in a network = 2^(32 − N)
/24 → 2^(32−24) = 2^8 = 256 ✓ matches your count
/28 → 2^(32−28) = 2^4 = 16
Why it splits into 16
Carving a /24 into /28s means taking 4 more bits away from the host part:
169.254.6.0/24 11111111.11111111.11111111 . 00000000
└── 8 host bits ──┘
2^8 = 256 addresses
169.254.6.0/28 11111111.11111111.11111111 . 1111 . 0000
└─4─┘ └─4──┘
new subnet host bits
bits: 2^4 = 2^4 = 16
= 16 addresses
subnets each"bip": "169.254.4.1/28",
"default-address-pools": [ { "base": "169.254.6.0/24", "size": 28 } ]
Two separate knobs, and they're easy to conflate:
| Key | Meaning |
|---|---|
base | the total space Docker may carve up |
size | the prefix length of each network it creates — not the number of networks |
So size: 28 means every auto-created network is a /28. The base then divides evenly:
169.254.6.0/24 ── 256 addresses, sliced into /28 ──
├── 169.254.6.0/28 ← network 1
├── 169.254.6.16/28 ← network 2
├── 169.254.6.32/28 ← network 3
│ ⋯
└── 169.254.6.240/28 ← network 1616 is the block size. Not all 16 are assignable:
169.254.9.0/28
├── .0 network address ← not assignable
├── .1 gateway ← Docker takes it
├── .2 … .14 13 container slots
└── .15 broadcast ← not assignable
block = 16 · usable hosts = 14 · after the gateway = 13ip route get 172.17.0.5
BEFORE AFTER (a stack comes up on 172.17.0.0/16)
───────────────────────────── ────────────────────────────────────────
default via 172.19.11.1 172.17.0.0/16 dev docker0 ← NEW, /16 > /0
169.254.0.0/16 enp0s31f6 172.19.11.0/24 enp0s31f6
172.19.11.0/24 enp0s31f6 169.254.0.0/16 enp0s31f6
default via 172.19.11.1
packet ─→ gateway ─→ LAN ─→ 172.17.0.5 packet ─→ docker0 ─→ ARP ─→ nobody
✓ SYN arrives ✗ dies. 172.17.0.5
never saw a packet. Docker created a network on 172.19.11.0/28 ← contains 172.19.11.1
main table local table = "addresses I answer for"
─────────────────────────── ──────────────────────────────────────
172.19.11.0/28 br-18191aa6 ← NEW local 172.19.11.161 dev enp0s31f6
172.19.11.0/24 enp0s31f6 local 172.19.11.1 dev lo ← NEW
default via 172.19.11.1
the host now thinks the GATEWAY is itself
packet for 8.8.8.8 ─→ default via 172.19.11.1 ─→ look up .1 ─→ "that's me"
└─→ delivered to lo ─→ ✗ ── inside the host (nothing on the wire yet) ── ── on the wire ──
─────────────────────────────────────────────── ─────────────────
1. app: send() to 8.8.8.8
2. routing table: which interface?
8.8.8.8 is not in 172.19.11.0/24
→ default route → next hop 172.19.11.1, dev enp0s31f6
3. routing table AGAIN: how do I reach 172.19.11.1?
it IS in my /24 → on-link → ARP for it
4. ARP: who has 172.19.11.1? → 00:09:0f:09:6f:1a
5. build the ethernet frame:
dst MAC = 00:09:0f:09:6f:1a ← the gateway
dst IP = 8.8.8.8 ← unchanged
────→ gateway ──→ … ──→ 8.8.8.8 you: 172.19.11.160 host: 172.19.11.161 (sshd :22)
────────────────── ──────────────────────────────
1. SYN ───────────────────────────→ arrives on enp0s31f6
← no routing decision needed.
A packet that arrives, arrives.
2. kernel accepts. sshd replies.
3. route lookup for 172.19.11.160
← ★ THIS is where it dies
4. SYN-ACK ←───────────────────────── (or doesn't) ── A ── your address falls inside a range Docker claimed
─────────────────────────────────────────────────────────
reply to 172.19.11.160 → matches 172.19.11.0/24 → enp0s31f6 ✓
reply to 172.19.11.5 → matches 172.19.11.0/28 → br-xxxx ✗
(Docker took .0/28; .5 is inside it)
── B ── you're off-subnet, and the gateway got shadowed
─────────────────────────────────────────────────────────
reply to 10.20.30.40 → off-link → default via 172.19.11.1
→ lookup .1 → "local, dev lo" → loopback ✗
── C ── rp_filter = 1 (strict)
─────────────────────────────────────────────────────────
SYN arrives on enp0s31f6. Kernel checks: would I send *back* to
this source via enp0s31f6? A Docker route now says "no, via docker0"
→ packet dropped BEFORE sshd ever sees it. ✗
(this host is at rp_filter=2, so it's spared)docker0 — yes, with one trap that catches everyone
The trap is a naming collision: bridge is both a driver and the name of one specific network.
driver "bridge" ← the technology. docker0 AND every br-xxxx use it.
network named "bridge" ← docker0 specifically. The default one.
network_mode: bridge ← attach to docker0.
So this lands you on docker0:
services:
app:
network_mode: bridge # explicit
And this does not — it silently creates a user-defined bridge called <project>_default:
services:
app:
networks: [default] # ← its own br-xxxx, NOT docker0
Compose's auto-created default network is a user-defined bridge. Every compose stack on this host proves it:
docker network ls
bridge ← docker0. linkdown. empty.
umami_default ← user-defined, br-ba9f53a61dc4
That's why docker0 is linkdown here — nothing has ever asked for it.
| you write | you get | gateway |
|---|---|---|
network_mode: bridge | docker0 | bip |
nothing / networks: [default] | <project>_default (own br-xxxx) | subnet + 1 |
an explicit networks: block | one br-xxxx per network | subnet + 1 |
network_mode: host | none | none |
User-defined is strictly better, which is why docker0 is effectively legacy:
- DNS by name — on a user-defined network,
dbresolves to the container. On docker0 you need--link(deprecated) or hardcoded IPs. That's whyumami-umami-1can just saydbin its connection string. - Isolation between networks — Docker inserts
DOCKER-ISOLATIONrules socertgen-netcan't reachumami_default. The default bridge is more permissive. - Hot attach/detach without restarting containers.
2. network_mode: host — yes, genuinely dangerous
It removes the boundary entirely. The container doesn't get its own network namespace; it shares the host's.
── bridged ── ── host mode ──
┌─ container netns ─┐ ┌────────────────────────┐
│ eth0 169.254.9.3 │ │ container process │
└────────┬──────────┘ │ lives in the HOST's │
│ veth │ network namespace │
┌────────▼──────────┐ │ │
│ br-ba9f53a61dc4 │ ← boundary │ binds :443, :22, :80 │
└────────┬──────────┘ │ reaches 127.0.0.1 │
│ iptables NAT + isolation │ sees the whole LAN │
┌────────▼──────────┐ └───────────┬────────────┘
│ host 172.19.11.161│ │
└───────────────────┘ no boundary. it IS the host.mode allocates an IP? from where can it take .161?
───────── ─────────────────────── ───────────────────── ─────────────────
host no — only if the host
already has it
bridge yes 169.254.x.0/28 (Docker) no
macvlan yes your LAN subnet yes — and that's
a duplicate-IP bug HOST MODE × 3
─────────────
┌──────────────────────────────────────────────────────┐
│ the host's single network namespace │
│ │
│ enp0s31f6 172.19.11.160/24 │
│ │
│ :80 ← container A │
│ :8080 ← container B │
│ :9000 ← container C │
│ │
│ no eth0 for any of them. no address. no veth. │
│ A, B and C reach each other on 127.0.0.1 │
└──────────────────────────────────────────────────────┘
from the LAN, all three answer to the SAME address:
172.19.11.160:80 172.19.11.160:8080 172.19.11.160:9000
└──────────────── one IP, three ports ────────────────┘
The port becomes the only thing that tells them apart
BRIDGED × 3 HOST MODE × 3
─────────── ─────────────
┌─A─┐ ┌─B─┐ ┌─C─┐ ┌──────────────────────┐
│.2 │ │.3 │ │.4 │ ← own addresses │ :80 :8080 :9000 │
└─┬─┘ └─┬─┘ └─┬─┘ │ ← only ports differ │
└─────┼─────┘ └──────────────────────┘
br-xxxx (169.254.9.1)
│
host 172.19.11.161