Configure Docker Macvlan and Ipvlan for Dedicated LAN IPs

devops-docker-compose

The scenario

A client had a legacy SIP recording appliance that needed to sit on the LAN with its own IP address and its own MAC. Not behind a NAT’d port mapping, not sharing the host’s identity — a real Layer 2 citizen on the 192.168.1.0/24 segment, indistinguishable from a physical box. We’d containerized it for easier deployment, and on paper that was fine. In practice, the appliance’s licensing daemon fingerprinted the MAC address, and the SIP stack refused inbound media streams once it saw the source IP get rewritten by Docker’s default bridge network. A docker macvlan ipvlan setup was the only way to make this work without ditching containers entirely.

Docker’s default bridge network is great for 90% of workloads, but it does exactly what you’d expect from NAT: every container shares the host’s IP externally, and inbound traffic only reaches a container if you explicitly publish a port. That’s a dealbreaker for network appliances, IDS sensors, or anything doing raw packet inspection — they need to see real source IPs, and sometimes they need to broadcast ARP requests as themselves. The goal here is to give each container a dedicated, routable IP directly on the physical LAN segment using the macvlan or ipvlan network drivers. The one-line difference: macvlan assigns each container its own unique MAC address on the parent NIC, while ipvlan shares the host’s single MAC across all containers and only varies at the IP layer. That distinction drives almost every decision you’ll make below.

Prerequisites

Before touching Docker, confirm your kernel and engine actually support these drivers. Macvlan needs kernel 3.9+, ipvlan needs 4.2+ — check with uname -r. We tested this whole setup against Docker Engine 24.0.7; ipvlan L3 mode has been stable since 20.10, so don’t assume it works cleanly on anything older. Confirm your engine version with:

docker info | grep -i "Server Version"

Next, identify your physical parent interface with ip link show, and note the subnet and gateway for the LAN segment you’re attaching to. Reserve a small IP range for containers that sits outside your DHCP scope — I’ve seen “random” packet loss that turned out to be a DHCP lease clashing with a manually assigned container IP, and it took an embarrassingly long time to spot because ping worked intermittently.

One thing that trips people up immediately: if you’re running this on AWS, Azure, or GCP, stop and reconsider macvlan. Most cloud hypervisors reject unrecognized MAC addresses at the vNIC level by default — your macvlan traffic will leave the container, hit the hypervisor, and silently vanish. No error, no log, just dropped packets. Ipvlan avoids this entirely because it reuses the parent interface’s MAC, so it’s the safer default for anything running on a cloud instance. Save macvlan for bare metal or on-prem hypervisors where you control the vSwitch config (see the Docker macvlan driver docs for the full compatibility notes).

Step 1 – Inspect the host network and pick a driver

docker macvlan ipvlan setup illustration

Start by confirming the actual state of your parent interface — name, MTU, driver — before you create anything:

ip -d link show eth0

The decision between macvlan and ipvlan usually comes down to two constraints. If your switch port enforces a MAC address limit (common on managed switches with port security enabled), macvlan will get you locked out the moment a second container tries to register a new MAC — ipvlan is the only viable option there. If you need a genuinely distinct MAC per container, for licensing tools or ARP-based network probes, macvlan is non-negotiable.

Write this caveat on a sticky note now, because it will bite you during verification if you forget it: in macvlan bridge mode, the Docker host itself cannot talk to containers on that network. The host and its macvlan children live on separate broadcast domains as far as the kernel’s bridge logic is concerned. Nothing is broken — it’s by design — but if you try to ping a container from the host and get nothing back, this is why.

Step 2 – Create the macvlan network and attach a container

With the parent interface confirmed, create the macvlan network and attach a container with a static IP outside the DHCP pool:

docker network create -d macvlan \
  --subnet=192.168.1.0/24 \
  --gateway=192.168.1.1 \
  -o parent=eth0 \
  macnet

docker run -d --name web1 \
  --network macnet \
  --ip 192.168.1.50 \
  nginx:1.25

If you’re used to bridge networking, you’ll instinctively reach for -p 8080:80 here. Don’t — it fails hard:

docker run -d --name web1 --network macnet -p 8080:80 --ip 192.168.1.50 nginx:1.25
# Error response from daemon: publish requires containers to run in the default network

This isn’t a bug, it’s a consequence of the architecture. Port publishing relies on Docker managing iptables NAT rules for you, and macvlan/ipvlan networks bypass that path entirely by design — the container is directly on the wire. There’s nothing for Docker to NAT.

Step 3 – Create the ipvlan network and compare

Now build the ipvlan equivalent so you can see the operational difference side by side:

docker network create -d ipvlan \
  --subnet=192.168.1.0/24 \
  --gateway=192.168.1.1 \
  -o ipvlan_mode=l2 \
  -o parent=eth0 \
  ipvnet

docker run -d --name web2 \
  --network ipvnet \
  --ip 192.168.1.51 \
  nginx:1.25

Check the MAC address inside the container against the host’s own eth0 — they’ll match, which is the whole point of ipvlan:

docker exec web2 ip addr show eth0
# link/ether d4:3d:7e:11:22:33 ... — same MAC as the physical host interface

Ipvlan L2 mode behaves like standard LAN traffic — ARP, broadcast, the works. There’s also L3 mode (-o ipvlan_mode=l3), which strips ARP and broadcast entirely and routes purely at Layer 3. That’s a good fit for multi-tenant isolation where you don’t want containers seeing each other’s broadcast traffic, but it comes at a cost: DHCP client broadcasts and mDNS simply won’t function inside those containers, so plan on static IPs and no service discovery over multicast.

Step 4 – Fix host-to-container reachability

This is the gotcha almost everyone hits on their first macvlan deployment. The host can create the macvlan network, run containers on it, and still be completely unable to ping them. The Linux kernel’s macvlan bridge mode explicitly isolates the host’s own network namespace from the macvlan children it spawns — it’s not a firewall rule you can just delete.

The workaround is a shadow macvlan sub-interface on the host itself, sitting on the same parent NIC, with its own IP in the same subnet:

ip link add mac0 link eth0 type macvlan mode bridge
ip addr add 192.168.1.250/32 dev mac0
ip link set mac0 up
# host can now reach 192.168.1.50 via mac0

Ipvlan doesn’t need this dance. Since the parent interface already shares its MAC with containers, and there’s no separate bridge-mode isolation layer, the host can reach ipvlan containers directly. That alone makes ipvlan the lower-maintenance option for most modern setups — one less moving part, one less thing to explain to whoever inherits this box after you.

Verify and test

Don’t trust docker network inspect alone — verify from a completely separate machine on the same physical LAN segment. From that second host, run:

arping -I eth0 192.168.1.50   # expect a unique MAC — confirms macvlan
arping -I eth0 192.168.1.51   # expect the host's own eth0 MAC — confirms ipvlan

Here’s a real output from that test session:

$ arping -I eth0 192.168.1.50
Unicast reply from 192.168.1.50 [02:42:C0:A8:01:32]  0.612ms

$ arping -I eth0 192.168.1.51
Unicast reply from 192.168.1.51 [D4:3D:7E:11:22:33]  0.549ms   # matches host eth0 MAC

If either of those hangs with no reply, run tcpdump -i eth0 arp or icmp on the Docker host itself. If you see the ARP request go out but never see a reply logged, that’s almost always the cloud-vNIC MAC filtering problem described earlier — switch to ipvlan and retest. Finally, check docker network inspect macnet and confirm the IPAM block’s gateway and subnet match the physical segment exactly — a mismatch here is the single most common cause of “container has no internet” tickets. Restart the container and confirm it comes back up with the same IP; if it doesn’t, your --ip assignment isn’t as static as you think.

Macvlan and ipvlan trade away Docker’s NAT convenience for genuine Layer 2 or Layer 3 presence on your physical network, and that trade comes with real operational cost: host-isolation quirks you have to work around, a single-NIC dependency that becomes a single point of failure without bonding, and the loss of Docker’s default iptables shielding — meaning firewalling now has to happen explicitly at the host or switch ACL level, because these containers are directly reachable from the LAN. If you’re on a cloud VM or just want fewer moving parts, reach for ipvlan first. Save macvlan for the cases where you genuinely need a distinct hardware-level MAC per container, and stick with plain bridge networking for everything else — most workloads don’t need this at all, and I’ve watched teams overcomplicate networking for services that would’ve been perfectly happy behind a published port. For more networking and infrastructure patterns we’ve run in production, check the DevOps_DayS archive.

Related

Leave a Reply

Your email address will not be published. Required fields are marked *

Support us · 💳 Monobank