Bridge mode¶
Bridge mode is docker-net-dhcp's default. Unlike the parent-attached
modes (macvlan / ipvlan), bridge mode plugs
container veths into a Linux bridge: one you maintain, or since
v2.3.0 one the plugin makes from a spare NIC. It needs a small amount of
one-time host setup, and it works anywhere a bridge can be bridged onto
the LAN where the DHCP server lives.
For the full option/observability/troubleshooting matrix see the driver reference. This page is the end-to-end walkthrough.
1. Prepare a host bridge¶
There are two paths, and the host's NICs decide which:
- A spare NIC that the host does not address: since v2.3.0 the plugin makes the bridge from it (#903). The host keeps the NIC up with no address; that is the one line of host setup. See the next section.
- A single NIC that carries the host's own address: the plugin refuses to take it, because the host would stop answering on it. You build the bridge yourself and move the host's address onto it, as the rest of this section describes.
A spare NIC: the plugin makes the bridge¶
The NIC must be up and carry no address other than an IPv6 link-local
one, now and after every reboot. Tell the host's network manager so,
once. Replace eth1 with your NIC:
| Stack | Stanza |
|---|---|
| Debian / ifupdown | in /etc/network/interfaces: auto eth1, iface eth1 inet manual, pre-up sysctl -qw net.ipv6.conf.eth1.accept_ra=0, up ip link set eth1 up |
| Ubuntu / netplan | under ethernets:, eth1: {dhcp4: false, dhcp6: false, accept-ra: false} |
| systemd-networkd | /etc/systemd/network/20-eth1.network with [Match] Name=eth1 and [Network] DHCP=no, IPv6AcceptRA=no |
| NetworkManager | sudo nmcli con add type ethernet ifname eth1 con-name eth1-spare ipv4.method disabled ipv6.method disabled |
Router advertisements are turned off in each, because an address the
kernel forms from one is an address on the NIC, and the plugin refuses
an addressed NIC. On NetworkManager, the automatic "Wired connection"
it makes for an unconfigured NIC runs DHCP on it; the profile above
replaces it. A NIC that nothing configures comes back down after a
reboot, and the plugin then refuses it (parent interface is down).
Docker's FORWARD policy is DROP, and it drops the DHCP frames the
bridge passes between a container and the NIC. Add the rule, persist it
(below, the iptables row), and create
the network with force_create=true, which the plugin needs because it
reads the policy and cannot see the rule:
sudo iptables -I DOCKER-USER -i lan0 -o lan0 -j ACCEPT
docker network create -d ghcr.io/claymore666/docker-net-dhcp:v2.3.1 \
--ipam-driver null -o bridge=lan0 -o parent=eth1 -o force_create=true lan-dhcp
For DHCPv6 (ipv6_mode), add the same rule with ip6tables; the
plugin checks the IPv4 policy only.
The plugin creates lan0, enslaves eth1, and deletes lan0 again
with the network. docker0 and br-* are refused as the bridge name.
The driver reference
lists every refusal and what happens after a reboot. Go on with
3. Run containers.
A single NIC: build the bridge yourself¶
You need a pre-configured bridge interface on the host. Enslaving the NIC to a bridge changes how the host itself is addressed, so read the warning below before running anything on a machine you cannot walk up to.
The host's own address moves to the bridge
Once eth0 is enslaved it must be left address-less, and DHCP
has to run on the bridge instead. Applying this over SSH with the
NIC still configured, or with a typo in the bridge stanza, drops the
connection and does not give it back.
Have console or out-of-band access (IPMI, iDRAC, the hypervisor
console) before you start. On Ubuntu, sudo netplan try gives you
an automatic revert if you lose the session; the other stacks have
no equivalent safety net.
The bridge normally inherits the NIC's MAC, but not on every driver. If your DHCP server pins the host's address by MAC reservation, the host may come back on a different address. Set the bridge MAC explicitly if that matters.
Try it now (does not survive a reboot)¶
These manual steps work on most Linux systems and are the fastest way to confirm the plugin does what you want. They are lost on the next reboot. Make them permanent with one of the stanzas below.
# Create the bridge
sudo ip link add my-bridge type bridge
sudo ip link set my-bridge up
# Assuming 'eth0' is connected to your LAN (where the DHCP server is)
sudo ip link set eth0 up
# Attach your network card to the bridge
sudo ip link set eth0 master my-bridge
# If your firewall's forwarding policy is DROP, add an ACCEPT rule
sudo iptables -A FORWARD -i my-bridge -j ACCEPT
# Get an IP for the host (goes out to the DHCP server, since eth0 is
# attached to the bridge). This is the HOST's own DHCP client, whichever
# one your distribution ships -- the plugin is not involved and is not
# installed yet. Use the same tool you used for eth0, e.g.
sudo dhclient my-bridge
Whatever brings addresses up on this host is the right command here; the persistent stanzas below hand the same job to the host's network manager.
Make the bridge persistent¶
Pick the one stanza that matches whatever manages networking on the host. In every case the pattern is identical: the NIC carries no address and is listed as a bridge port, and the bridge is the thing that runs DHCP.
Each recipe disables STP explicitly. That is not cosmetic here. See Leave STP off below.
Debian / ifupdown¶
Needs bridge-utils (sudo apt install bridge-utils).
Note that networking.service runs ifup -a synchronously, so the
boot blocks until the bridge's DHCP request completes or dhclient
gives up, which is around a minute if the DHCP server is slow or absent.
That delay is normal for this stack and is not a fault. If a fast boot
matters more than having the address ready at boot, the networkd or
netplan recipes do not have this property.
# /etc/network/interfaces
auto lo
iface lo inet loopback
# Enslaved: no address of its own.
iface eth0 inet manual
auto my-bridge
iface my-bridge inet dhcp
bridge_ports eth0
bridge_stp off
bridge_fd 0
Ubuntu / netplan¶
# /etc/netplan/60-dhcp-bridge.yaml (chmod 600)
network:
version: 2
renderer: networkd
ethernets:
eth0:
dhcp4: false
dhcp6: false
bridges:
my-bridge:
interfaces: [eth0]
dhcp4: true
parameters:
stp: false
forward-delay: 0
sudo chmod 600 /etc/netplan/60-dhcp-bridge.yaml
sudo netplan try # auto-reverts in 120s if you lose the session
sudo netplan apply
systemd-networkd¶
Three files: the bridge device, the port, and the bridge's own addressing:
# /etc/systemd/network/10-my-bridge.netdev
[NetDev]
Name=my-bridge
Kind=bridge
[Bridge]
STP=false
ForwardDelaySec=0
# /etc/systemd/network/30-my-bridge.network
[Match]
Name=my-bridge
[Network]
DHCP=ipv4
ConfigureWithoutCarrier=yes
ConfigureWithoutCarrier=yes matters at boot: a bridge with no port up
yet has no carrier, and without it networkd can decline to start DHCP
on the bridge at all. This is what netplan emits for the equivalent
bridge, so it is the reference implementation's own answer and not a
workaround.
On a cloud image, cloud-init writes its own .network file for the NIC,
for example 10-cloud-init-eth0.network. networkd applies only the
first file, in name order, whose [Match] fits a device, so the
20-eth0.network above loses to it silently and the NIC is never
enslaved. Either name the port file so it sorts before cloud-init's, or
turn cloud-init's network configuration off
(/etc/cloud/cloud.cfg.d/99-disable-network-config.cfg containing
network: {config: disabled}) and remove its file. On Ubuntu, use the
netplan recipe above instead; netplan owns the NIC there.
NetworkManager (nmcli)¶
Not on Ubuntu: netplan owns ethernet there
Ubuntu ships NetworkManager restricted to wireless devices:
# /usr/lib/NetworkManager/conf.d/10-globally-managed-devices.conf
[keyfile]
unmanaged-devices=*,except:type:wifi,except:type:gsm,except:type:cdma
Every ethernet device is therefore unmanaged, and nmcli con up
fails with "No suitable device found for this connection" no
matter how correct the profile is. Use the netplan recipe above on
Ubuntu. This recipe is for distributions where NetworkManager
manages ethernet: Fedora, RHEL and derivatives, and Debian installs
that chose it.
The existing standalone profile for the NIC has to stop autoconnecting,
or it will race the bridge for eth0. Find its name with
nmcli con show.
sudo nmcli con add type bridge ifname my-bridge con-name my-bridge \
ipv4.method auto bridge.stp no bridge.forward-delay 0
sudo nmcli con add type ethernet ifname eth0 con-name my-bridge-port \
master my-bridge slave-type bridge
# Stop the old profile from grabbing the NIC on boot
sudo nmcli con mod "Wired connection 1" connection.autoconnect no
sudo nmcli con down "Wired connection 1"
sudo nmcli con up my-bridge
Leave STP off unless you need it¶
With STP enabled, a bridge puts every newly added port through
listening and learning states before it forwards, which is two
forwarding delays, 30 seconds at the stock 15s setting. Container
veths are added at attach time, so a bridge with STP on breaks DHCP
for every container: the client broadcasts into a port that is not
forwarding yet, gets no answer, and the attach fails or falls back.
A bridge created with plain ip link add ... type bridge has STP off
already, which is why the imperative recipe above works as written.
Managed stacks are the risk. They apply their own default, and it is not
always off. That is why every stanza above sets it explicitly instead of
relying on a default.
If containers only get addresses when you attach them slowly, or
leases_obtained sits flat while dhcp_timeouts climbs, check STP
first:
stp_state 0 is what you want. Note that a non-zero forward_delay in
the same output is harmless while STP is off, and that it is reported in
centiseconds, so the stock 15 seconds reads as 1500.
Persist the firewall rule too¶
The FORWARD -i my-bridge -j ACCEPT rule from the imperative recipe is
as temporary as the bridge was. Whichever stanza you used above, the
rule needs the distro's own persistence mechanism:
| Stack | How |
|---|---|
| iptables | sudo apt install iptables-persistent, then sudo netfilter-persistent save |
| nftables | add the rule to /etc/nftables.conf, sudo systemctl enable nftables |
| firewalld | sudo firewall-cmd --permanent --direct --add-rule ipv4 filter FORWARD 0 -i my-bridge -j ACCEPT && sudo firewall-cmd --reload |
| ufw | set DEFAULT_FORWARD_POLICY="ACCEPT" in /etc/default/ufw, then sudo ufw reload |
You only need this if the forwarding policy is DROP. Check with sudo
iptables -S FORWARD | head -1.
2. Create the network¶
# On arm64 use the -arm64 tag. A network stores this exact reference
# as its driver, so it must name the plugin you installed.
docker network create -d ghcr.io/claymore666/docker-net-dhcp:v2.3.1 \
--ipam-driver null -o bridge=my-bridge my-dhcp-net
With IPv6 as well, use the ipv6_mode driver option on every engine.
On engine 26 (26.1.4, measured 2026-09-24) the daemon refuses
docker network create --ipv6 on a network with the null IPAM driver.
On 29.8.1 it accepts the create, but a --ip6 address still does not
reach the plugin: the Solicit carried no requested address (measured the
same day).
# arm64: the -arm64 tag here too.
docker network create -d ghcr.io/claymore666/docker-net-dhcp:v2.3.1 \
--ipam-driver null -o bridge=my-bridge -o ipv6_mode=dhcp my-dhcp-net
ipv6_mode takes off (the default), dhcp for a DHCPv6 lease beside
the v4 one, slaac to form the address from the router's advertised
prefix and send no Solicit, and auto to read the advertisement and do
what it says: DHCPv6 where the advertisement asks for it, the prefix
where it does not. -o ipv6=true is the short spelling of dhcp. The
default route and the MTU come from the advertisement in all three, and
on a propagate_dns network its resolvers reach the container too,
behind any a DHCPv6 server supplies. The modes, and
ipv6_auto_strict for what auto does when the router asks for DHCPv6
and no server answers, are in the
driver reference.
One of the two IPAM shapes is required. Docker's built-in IPAM must not be the allocator: it hands out addresses from its own pool, which collides with the real LAN the bridge is attached to. Pass
--ipam-driver null, as above, or, from v2.1.0, name this plugin as the IPAM driver as well, which puts the leased address into Docker's address management and makes--ipand Composeipv4_addresswork. The exchange is shared across modes; the link it runs on differs. Macvlan takes the parent gate and adds a child of the parent interface; bridge builds a veth pair, puts the endpoint's hardware address on the half the client runs on, and makes the other half a bridge port. The integration suite asks for an address on macvlan only, so the bridge half of that request carries no arm of its own. Both shapes are set out in Address allocation.
See the driver reference
for every network-level option (lease_timeout, ignore_conflicts,
skip_routes, gateway, propagate_dns, …).
3. Run containers¶
$ docker run --rm -ti --network my-dhcp-net alpine
/ # ip address show
159: my-bridge0@if160: <BROADCAST,MULTICAST,UP,LOWER_UP,M-DOWN> mtu 1500 ...
link/ether 86:41:68:f8:85:b9 brd ff:ff:ff:ff:ff:ff
inet 10.255.0.246/24 brd 10.255.0.255 scope global my-bridge0
/ # ip route show
default via 10.255.0.123 dev my-bridge0
10.255.0.0/24 dev my-bridge0 scope link src 10.255.0.246
Or in Docker Compose, against a network created out-of-band (the
recommended approach, because the network is shared across compose
projects and survives compose down):
services:
app:
hostname: my-http
image: nginx
mac_address: 86:41:68:f8:85:b9
networks:
- dhcp
networks:
dhcp:
external: true
name: my-dhcp-net
(The older external: block with a nested name: is deprecated and
warns on current Compose. The two-key form above is the supported one.)
You can also have Compose manage the network itself (it is then deleted
on compose down):
services:
app:
image: nginx
hostname: my-server
networks:
- dhcp
networks:
dhcp:
# arm64: the -arm64 tag, matching the plugin you installed.
driver: ghcr.io/claymore666/docker-net-dhcp:v2.3.1
driver_opts:
bridge: my-bridge
ipv6_mode: 'dhcp'
ipam:
driver: 'null'
Notes:
- The container takes a little longer than usual to start, because a DHCP lease is obtained before it is created.
- The plugin renews the lease, and updates the container's default gateway when the offered gateway changes, for the life of the endpoint. The renewal runs inside the plugin process and never inside the container.
-o host_ifname=container_namenames the host-side half of the veth pair after the container, soip linkandbrctl showon the host read like the compose file;-o host_ifname=hostnameuses the container's hostname, which is not unique on a host. Off by default, where the link isdh-plus twelve hex digits. Bridge mode only: the option is refused atdocker network createinmacvlanandipvlan, which leave nothing on the host to name. The rule, what happens when the name is taken, and the counters that say so are in Host-side interface names.- Use
--mac-address/mac_addressfor MAC-keyed reservations or to reuse an old lease;--hostname/hostnameis sent as DHCP option 12 for DHCP-DNS integration. Per-endpoint and per-container knobs are documented in the driver reference.
See also¶
- Driver reference lists every option, the observability surface and the troubleshooting steps.
- macvlan / ipvlan modes attach a container to a host NIC without a bridge.
- How it works describes the veth pair and the DHCP client behind both.