Docker & ContainersVII · Networkingmacvlan and ipvlan
macvlan and ipvlan — when you need L2 adjacency
What you'll learn
- Create a macvlan network bounded so it cannot collide with the LAN DHCP pool
- Explain why the host cannot reach its own macvlan containers, and work around it
- Choose between macvlan, ipvlan l2 and ipvlan l3
- Recognise the failure signature of a switch that blocks MAC spoofing
Prerequisites
Verified against Docker Engine 29.x · Docker Engine 28.x · Docker Compose 2.x · containerd 2.x · runc 1.2.x · BuildKit 0.20+ · Linux kernel 5.15+ · Ubuntu 24.04 LTS · Debian 12 (Bookworm) · 2026-08-12
macvlan and ipvlan make a container appear as a separate host on the same L2 segment as the host’s physical NIC. macvlan gives each container its own MAC address; ipvlan has the containers share the parent interface’s MAC and distinguishes them by IP.
Both remove NAT entirely. The container is a peer on the LAN, reachable without any port mapping, and it sees real client addresses. That is the attraction. The rest of this lesson is the price.
macvlan
PARENT=eth0
docker network create -d macvlan \
--subnet 192.0.2.0/24 \
--gateway 192.0.2.1 \
--ip-range 192.0.2.192/27 \
--aux-address 'router=192.0.2.1' \
-o parent="$PARENT" \
macvlan-net
docker network inspect macvlan-net --format '{{json .IPAM.Config}}'A container on this network:
- Has a MAC address distinct from the host’s, generated by Docker.
- Holds an IP on the same subnet as the parent interface.
- Is reachable from the LAN with no port mapping and no
-p. - Reaches the internet through the LAN gateway, not through the host’s NAT.
Getting the host back onto the segment
Two workarounds, both documented upstream, with different shapes.
PARENT=eth0
SHIM_ADDR=192.0.2.223/32
CONTAINER_RANGE=192.0.2.192/27
# A macvlan interface belonging to the host, on the same parent
sudo ip link add macvlan-shim link "$PARENT" type macvlan mode bridge
sudo ip addr add "$SHIM_ADDR" dev macvlan-shim
sudo ip link set macvlan-shim up
# Route the container range out of the shim rather than out of the parent
sudo ip route add "$CONTAINER_RANGE" dev macvlan-shim
ping -c1 192.0.2.194Pick the shim address from inside the block you reserved and add it to
--aux-address so Docker never allocates it. Make it persistent through your
distribution’s network configuration — as a bare ip link it disappears on
reboot, and the failure returns looking brand new.
The second workaround is simpler and often better: attach the container to a user-defined bridge as well as the macvlan network. It then has two interfaces, one on the LAN and one reachable from the host, and the host-side tooling talks to the bridge address.
CONTAINER=lan-svc
docker network create --subnet 172.30.0.0/24 mgmt-net
docker run -d --name "$CONTAINER" --network macvlan-net nginx:1.27-alpine
docker network connect mgmt-net "$CONTAINER"
docker inspect --format \
'{{range $n, $c := .NetworkSettings.Networks}}{{$n}}={{$c.IPAddress}} {{end}}' \
"$CONTAINER"Promiscuous mode and the switch
macvlan puts several MAC addresses behind one physical port. The upstream requirement is unambiguous: “Your networking equipment needs to be able to handle promiscuous mode, where one physical interface can be assigned multiple MAC addresses.”
On bare metal with a normal switch this is the default behaviour. Three places it is not:
- Cloud VMs. AWS, GCP and Azure filter frames whose source MAC is not the one assigned to the vNIC. macvlan does not work there.
- Switch ports with port security configured to a single MAC.
- Wireless. 802.11 does not carry a second MAC behind an associated station. macvlan on a Wi-Fi parent does not work, and this is a protocol limitation rather than a driver bug.
$ CONTAINER=lan-svc
# From the container: outbound works
docker exec "$CONTAINER" ping -c2 192.0.2.1
# From another LAN host: inbound does not
# (run on 192.0.2.30) ping -c2 192.0.2.194PING 192.0.2.1 (192.0.2.1): 56 data bytes
64 bytes from 192.0.2.1: seq=0 ttl=64 time=0.43 ms
64 bytes from 192.0.2.1: seq=1 ttl=64 time=0.39 ms
--- 192.0.2.194 ping statistics ---
2 packets transmitted, 0 packets received, 100% packet lossIllustrative output
Outbound succeeding while inbound fails is the fingerprint. The container’s ARP replies carry a MAC the switch will not forward, so nothing on the LAN can complete an ARP resolution for it. Check the switch’s MAC address table for the host’s port: if you see one MAC where you expect several, the switch is the problem, not Docker.
802.1Q trunking
Name a VLAN sub-interface as the parent and Docker creates it for you:
docker network create -d macvlan \
--subnet 192.0.2.0/24 --gateway 192.0.2.1 \
--ip-range 192.0.2.192/27 \
-o parent=eth0.50 \
vlan50-net
# Docker created the sub-interface
ip -brief link show eth0.50The host port must be a trunk carrying that VLAN. Docker removes the
sub-interface when the network is removed — but only the ones it created, so
a hand-made eth0.50 survives and will be reused rather than recreated.
ipvlan
docker network create -d ipvlan \
--subnet 192.0.2.0/24 \
--gateway 192.0.2.1 \
--ip-range 192.0.2.192/27 \
-o parent=eth0 \
-o ipvlan_mode=l2 \
ipvlan-netipvlan_mode accepts l2, l3 and l3s; ipvlan_flag accepts bridge,
private and vepa. l2 with the bridge flag is the default and the one
to start from.
l3 mode is a different thing entirely. The kernel routes rather than
bridges: broadcast and multicast are dropped, ARP does not traverse, and
containers use a default route out of their interface with no next hop. It
follows that:
- DHCP does not work — the discover is a broadcast.
- mDNS, NetBIOS discovery and anything else broadcast-based does not work.
- The upstream network must have routes to your container subnets, because nothing will ARP for them. That is a router change, not a Docker one.
Choose l3 when you want containers on their own routed subnets and you
control the routers. Choose l2 when you want them to look like they are
plugged into the same switch.
Comparison
| Feature | macvlan | ipvlan (l2) | ipvlan (l3) | bridge |
|---|---|---|---|---|
| Container has own MAC | yes | no | no | no |
| Reachable from LAN without publishing | yes | yes | routed only | no |
| Needs multiple MACs per port | yes | no | no | no |
| Works on AWS / GCP / Azure | no | usually | usually | yes |
| Broadcast / DHCP / mDNS reaches container | yes | yes | no | yes |
| Upstream router config required | no | no | yes | no |
| NAT for outbound | no | no | no | yes |
| Host can reach its own containers | no | no | no | yes |
The last row is the one people read after they have already deployed.
Sanity check
docker network inspectshows anIPRangenarrower than the subnet, and the same range is excluded on the DHCP server.- A LAN host that is not the Docker host can reach the container.
- You have consciously decided how the Docker host reaches the container, and tested it.
- The
parentis the bond or VLAN device that survives a NIC failover, not a member interface.
Knowledge check
Knowledge check · 5 questions
Q1. A macvlan container can reach the gateway and other LAN hosts, and they can reach it. Only the Docker host it runs on cannot ping it. What is wrong?
Q2. A macvlan network is created with `--subnet 192.0.2.0/24` and no `--ip-range`, on a LAN with a DHCP server serving the same subnet. What is the likely outcome?
Q3. Which problem does switching from macvlan to ipvlan l2 actually solve?
Q4. Which are true of ipvlan `l3` mode? Select all that apply.
Q5. On a bonded host, a macvlan network should be parented on the bond device rather than on a member NIC such as eth0.
Passing score: 75%. Answers are checked in this browser.