Docker Macvlan in Compose: Give Containers a LAN IP
Set up a Docker macvlan network in Compose so containers get real LAN IPs, plus the host-to-container gotcha and the shim interface that fixes it.
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. Macvlan is the Docker network driver for those cases: it gives a container its own MAC address and its own IP on your LAN, so the rest of the network sees a machine rather than a forwarded port.
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 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 normally handles this, 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 .239 and the two allocators can never collide.
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
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.
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 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.
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.
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.
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.
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
Sources
Related
Docker Compose Host Networking (network_mode: host)
How network_mode host works in Docker Compose, why published ports stop working, what it breaks for DNS and Traefik, and when to pick it anyway.
Docker Compose Networking: Bridge, DNS, Ports & Aliases
Learn Docker Compose networking from default and external networks to service-name DNS, aliases, port publishing, host mode, macvlan, and common fixes.
Best Docker Containers for Your Home Server in 2026
A practical homelab operator's guide to the best docker containers for home server use: Jellyfin, Vaultwarden, Nextcloud, Tailscale and a dozen more.