Docker macvlan is a network driver that gives each container its own MAC address and its own IP on your physical LAN, so the router and every other machine see a separate device rather than a port forwarded through the host. In Docker Compose you declare it as a top-level network with driver: macvlan, a parent interface, and your LAN’s subnet and gateway.
Most containers are happy behind a bridge and a published port. A few are not. A DHCP server has to answer broadcasts, a DNS filter is easier to point a router at when it owns an address, and some appliances only behave when they look like a separate device on the network. Those are the cases macvlan exists for.
It also has one behaviour that catches everyone exactly once, which is covered below before the syntax, because it decides whether macvlan is the right tool at all.
The Trade-off, Up Front
A macvlan container is a first-class device on your LAN. Every other machine on the subnet can reach it on any port, no publishing needed. Your router hands it a place in the address plan. Discovery protocols work because it is genuinely on the same layer 2 segment.
But the Docker host itself cannot reach it, and it cannot reach the host. This is not a bug or a firewall rule. Macvlan sub-interfaces are deliberately barred from talking to their own parent interface, so a container on eth0’s macvlan network and the host that owns eth0 are invisible to each other. Traffic between them leaves and never comes back.
That matters more than it sounds. It means the host cannot health-check the container, a monitoring agent on the host cannot scrape it, and a reverse proxy running as a normal bridge container on that host cannot proxy to it. There is a fix, and it is in the shim interface section below, but if your plan was “put Pi-hole on macvlan and proxy to it from Traefik on the same box”, plan for the extra step now.
Prerequisites That Are Not Negotiable
A Linux host running rootful Docker. Per Docker’s macvlan documentation, the driver does not work on Docker Desktop for Mac or Windows, Docker Engine on Windows, or in rootless mode, and needs kernel 3.9 or later (4.0 or later recommended).
A wired interface. Wi-Fi will not work. The 802.11 standard does not allow a station to present multiple MAC addresses on one association, so frames from every macvlan child address are dropped by the access point. Nothing on the Docker side changes that; the parent has to be an ethernet interface.
A switch that tolerates multiple MACs on one port. Ordinary unmanaged switches do. Managed switches with port security, and effectively every cloud or VPS provider, do not. If MAC filtering is in play, ipvlan is the alternative, covered at the end.
Promiscuous mode on the parent interface. Docker’s documentation lists this as a requirement, because one physical interface ends up carrying several MAC addresses. On bare metal it rarely needs attention, but some hypervisors block it at the virtual switch. Under Proxmox, ESXi, or similar, the vSwitch or bridge has to permit promiscuous mode and MAC address changes, or the container’s traffic never reaches the wire.
Addresses your DHCP server will not hand out. Docker’s macvlan driver does not speak DHCP for the container. You assign addresses statically, from a range your router is not also handing out, or you get a duplicate-address conflict at the worst possible moment.
The Compose Network Definition
Macvlan is declared as a top-level network with a parent interface and an explicit IPAM block. Find your interface name and subnet first:
ip -br addr # interface names and current addresses
ip route | grep default # your gateway
Then declare the network. The important key is ip_range, which narrows the pool Docker allocates from to a slice you have reserved:
networks:
lan:
driver: macvlan
driver_opts:
parent: eth0
ipam:
config:
- subnet: 192.168.1.0/24
gateway: 192.168.1.1
ip_range: 192.168.1.240/28
The subnet and gateway describe your real LAN, unchanged. The ip_range of 192.168.1.240/28 carves out .240 to .255 for containers. Set your router’s DHCP pool to end at .238; that leaves .239 for the host shim below and .240 to .255 for containers, so no allocator can collide.
Do not drop the gateway line. Since Docker Engine 29.0, macvlan and ipvlan L2 networks get no default gateway unless the IPAM configuration sets one explicitly, so containers would reach the LAN and nothing beyond it.
Attaching services is ordinary, with one addition: pin the address, because a container that changes IP defeats the purpose of putting it on the LAN.
services:
pihole:
image: pihole/pihole:latest
hostname: pihole
networks:
lan:
ipv4_address: 192.168.1.241
environment:
TZ: "Etc/UTC"
FTLCONF_webserver_api_password: "change-me"
volumes:
- ./etc-pihole:/etc/pihole
restart: unless-stopped
networks:
lan:
driver: macvlan
driver_opts:
parent: eth0
ipam:
config:
- subnet: 192.168.1.0/24
gateway: 192.168.1.1
ip_range: 192.168.1.240/28
Note what is absent: no ports: section. It would do nothing. The container owns every port on 192.168.1.241, so port 53, port 80, and port 67 are all simply available. That is the whole appeal, and the reason a macvlan container needs the same firewalling attention as any other machine on the network.
Bring it up and check from a different machine, not from the Docker host:
docker compose up -d
ping 192.168.1.241 # run this from a laptop, not the host
Creating the Network with docker network create
When several Compose projects share one reserved block, create the macvlan network once from the CLI so a single allocator hands out the range:
docker network create -d macvlan \
--subnet=192.168.1.0/24 \
--gateway=192.168.1.1 \
--ip-range=192.168.1.240/28 \
--aux-address="printer=192.168.1.245" \
-o parent=eth0 \
lan
--aux-address keeps the allocator off an address already in use inside the range, here a printer on .245; the Compose equivalent is aux_addresses in the IPAM block. The driver also accepts -o macvlan_mode=, which defaults to bridge, the mode every example here assumes. Then mark the network external in each Compose file:
services:
pihole:
networks:
lan:
ipv4_address: 192.168.1.241
networks:
lan:
external: true
Per the Compose networks reference, an external network’s lifecycle sits outside Compose, so docker compose down in one project leaves the others connected. It is the same external: true pattern used to share a network between docker run and Compose containers.
The Shim Interface
To let the host reach the container, give the host its own macvlan child interface on the same parent. The host then talks to the container as one macvlan sibling to another, which is permitted, instead of parent to child, which is not. Docker’s documentation also offers a second workaround: attach the container to a bridge network as well as the macvlan one.
sudo ip link add macvlan-shim link eth0 type macvlan mode bridge
sudo ip addr add 192.168.1.239/32 dev macvlan-shim
sudo ip link set macvlan-shim up
sudo ip route add 192.168.1.240/28 dev macvlan-shim
The address on the shim must sit outside the container range and outside the DHCP pool. The route tells the host to send traffic for the container block out of the shim rather than out of eth0.
These commands do not survive a reboot. Make them permanent with a systemd unit ordered after the network is up, or with the equivalent in your distribution’s network configuration:
[Unit]
Description=macvlan shim for Docker containers
After=network-online.target
Wants=network-online.target
[Service]
Type=oneshot
RemainAfterExit=yes
ExecStart=/sbin/ip link add macvlan-shim link eth0 type macvlan mode bridge
ExecStart=/sbin/ip addr add 192.168.1.239/32 dev macvlan-shim
ExecStart=/sbin/ip link set macvlan-shim up
ExecStart=/sbin/ip route add 192.168.1.240/28 dev macvlan-shim
ExecStop=/sbin/ip link del macvlan-shim
[Install]
WantedBy=multi-user.target
A container behind a shim is reachable from the host, so host-based monitoring and a same-host reverse proxy such as Traefik both work again. It is worth deciding whether you need that before building it, because the shim is the piece most likely to be forgotten during a rebuild and then to produce a mysteriously unreachable service.
Setting a Fixed MAC Address in Compose
Set it per network, with mac_address next to ipv4_address:
services:
pihole:
networks:
lan:
ipv4_address: 192.168.1.241
mac_address: "02:00:00:00:01:f1"
Since Docker Engine 28.0 (February 2025), containers on bridge and macvlan networks get randomly generated MAC addresses, so every recreated container looks like a new device to the router or a MAC-keyed firewall rule. A pinned mac_address survives docker compose up -d after an image pull. Use a locally administered unicast address, where the first octet’s second hex digit is 2, 6, A or E (MAC address structure), which is why 02: is the usual prefix.
Avoid the service-level mac_address key. The Compose services reference warns that Docker Engine 25.0 and later may reject it and points to networks.mac_address instead, and Engine 29.0 made the container-wide MAC setting obsolete from API v1.52.
Can a Macvlan Container Get Its IP from DHCP?
No, not with Docker’s built-in driver. Macvlan containers take their addresses from Docker’s own IPAM, meaning the subnet and ip_range you declare, and the container never sends a DHCP request to your router, so a router reservation does nothing. The working pattern is the one above: shrink the router’s pool, give Docker the freed slice, and pin each service’s address.
Three different things get searched as “macvlan DHCP”:
- Running a DHCP server on macvlan works, and is one of the best reasons to use the driver: a Pi-hole on its own LAN address hears client broadcasts directly.
- Getting leases from the router needs a third-party plugin such as docker-net-dhcp, which attaches containers to a Linux bridge on the host rather than to macvlan, and whose README calls it probably not a great idea for production.
- IPv6 from the router works natively: the macvlan documentation covers router-assigned IPv6 addresses through SLAAC. IPv4 still comes from Docker.
VLAN Tagging on the Same Interface
Macvlan can attach to an 802.1Q sub-interface, which is how you place containers directly onto a tagged VLAN without extra cabling. Name the parent <interface>.<vlan-id> and Docker creates the sub-interface if it does not already exist:
networks:
iot:
driver: macvlan
driver_opts:
parent: eth0.20
ipam:
config:
- subnet: 192.168.20.0/24
gateway: 192.168.20.1
ip_range: 192.168.20.240/28
The switch port feeding the host has to be a trunk carrying VLAN 20, and the VLAN has to exist on your router with that gateway address. Done properly this is the cleanest way to isolate untrusted containers, because the separation is enforced by the network rather than by Docker.
One service can join several VLAN networks, such as lan and iot for a home automation controller. The network with the highest gw_priority (default 0) then supplies its default gateway, per the Compose services reference. The same dotted-parent syntax works for ipvlan.
When to Use It, and When Not To
Macvlan earns its complexity in a narrow set of cases:
- A DNS or DHCP server you point the router at. Pi-hole serving DHCP needs to see broadcasts, and an address of its own makes the router configuration obvious. The bridge-mode alternative, including the
FTLCONF_dns_listeningModesetting that goes with it, is in the Pi-hole Docker Compose guide. - Anything doing service discovery. mDNS, SSDP, and Bonjour depend on multicast that NAT does not carry.
- A service that must not share the host’s ports. Two containers can each own port 443 when each has its own IP.
- Migrating an appliance into a container where the rest of the network already has firewall rules or reservations keyed to an address.
Skip it when a published port would do. Bridge networking with ports: is simpler to reason about, works on Wi-Fi, works on a VPS, keeps the host reachable, and lets a reverse proxy discover services automatically. The great majority of a homelab stack, including everything in the home server container roundup, belongs on a bridge network behind a proxy.
The middle option is worth naming too. If the goal is simply “this container needs to see LAN broadcast”, network_mode: host achieves that with far less setup, at the cost of the container sharing all of the host’s ports. The full comparison is in the Docker Compose host networking guide.
IPvlan: The Fallback When MACs Are Filtered
IPvlan solves the same problem with one MAC address. Every container shares the parent interface’s MAC and is distinguished only by IP, which keeps switch port security, most hypervisor defaults, and some Wi-Fi setups happy.
networks:
lan:
driver: ipvlan
driver_opts:
parent: eth0
ipvlan_mode: l2
ipam:
config:
- subnet: 192.168.1.0/24
gateway: 192.168.1.1
ip_range: 192.168.1.240/28
The trade-offs: DHCP is unusable, because a DHCP server cannot tell the containers apart by MAC, and the host-to-container restriction still applies in l2 mode. Choose macvlan when the network permits multiple MACs, and ipvlan when it does not.
In driver_opts, ipvlan_mode defaults to l2 (alternatives l3 and l3s) and ipvlan_flag to bridge (alternatives private and vepa). Docker’s IPvlan page asks for kernel 4.2 or later.
With ipvlan_mode: l3 the host routes for the containers instead: broadcast and multicast are dropped, gateway is ignored, and the subnet must differ from the LAN’s, so your router needs a static route to it via the Docker host.
Macvlan vs IPvlan Compared
| macvlan | ipvlan l2 | ipvlan l3 | |
|---|---|---|---|
| MAC address per container | Its own | Shares the parent’s | Shares the parent’s |
| Container subnet | Your LAN’s | Your LAN’s | A separate routed subnet |
| Broadcast and multicast | Yes | Yes (shared L2 segment) | Dropped |
gateway in IPAM | Needed for a default route (Engine 29+) | Needed for a default route (Engine 29+) | Ignored |
| Works behind MAC-limited switch ports | No | Yes | Yes |
| Router change needed | None | None | Static route to the container subnet |
| Minimum kernel | 3.9 (4.0+ recommended) | 4.2 | 4.2 |
Troubleshooting
Container starts but nothing can reach it. Check the parent interface name against ip -br addr, then confirm the switch port is not filtering MACs. On a virtual host, confirm promiscuous mode is permitted on the vSwitch.
The host cannot ping the container, everything else can. Expected. Build the shim interface described above.
“network with name X already exists” after editing the Compose file. Docker does not rewrite an existing network’s IPAM. Remove it and let Compose recreate it: docker compose down then docker network rm <name>.
Two devices with the same address. The ip_range overlaps the router’s DHCP pool. Shrink the pool or move the range; they must not intersect.
Containers reach the LAN but not the internet. On Engine 29.0 or later, the network has no gateway. Add one and recreate the network.
The router flags a new device after every update. Engine 28.0 and later randomise MACs; pin one with mac_address.
It works, then stops after a reboot. The shim interface is not persistent unless you made it so. This is the single most common macvlan regression.
Nothing works on Wi-Fi. Not fixable. Use ethernet, or use bridge networking with published ports.
For the bridge-mode fundamentals underneath all of this, including how service-name DNS and custom networks behave, start with the Docker Compose networking guide.
Related across the network
- Ansible Inventory: Groups, Variables, and Patterns — ansibleguide.com
- Cloudflare Tunnel Not Connected: Fixing Error 1033 — cloudflarezerotrust.org
FAQ
does docker macvlan work on docker desktop or wifi
No to both. Docker’s macvlan driver is supported only on Linux hosts, so Docker Desktop on Mac or Windows and Docker Engine on Windows cannot use it, and neither can rootless Docker. Wi-Fi fails because access points drop frames from the extra MAC addresses. Run it on a wired Linux host, or use bridge networking instead.
can a docker container use macvlan and bridge networks at the same time
Yes. List both networks under the service in Compose. The bridge network lets the host reach the container and lets a same-host reverse proxy find it by service name, while the macvlan network gives it a LAN address. Set gw_priority on the network that should carry outbound traffic, because the highest value becomes the default gateway.
should i use macvlan or ipvlan in docker compose
Use macvlan when your switch, hypervisor or provider accepts several MAC addresses on one port, because each container then behaves like a separate device. Use ipvlan l2 when MACs are limited or filtered, since every container shares the parent’s MAC. Choose ipvlan l3 only if you can add a static route on your router for the container subnet.