After spending time on manual GRE tunnels, I wanted something more programmable — a virtual switch I could configure with flow rules, VLANs, and multiple tunnel types instead of hand-wiring routes and iptables. That’s what led me to Open vSwitch (OVS). In this guide I’ll show how I replaced Docker’s default Linux bridge with an OVS bridge, and how I used OVS’s own GRE/VXLAN tunneling to connect containers across hosts.
Why Open vSwitch Instead of the Default Linux Bridge
Docker’s default networking uses the kernel’s native Linux bridge (docker0), which is simple but limited: it’s Layer 2 only, has no concept of flow-based forwarding rules, and doesn’t natively support tunnel encapsulation between hosts without external tooling. OVS is a production-grade, programmable virtual switch that supports:
- OpenFlow-based flow tables, so you can define exactly how traffic is matched and forwarded.
- Native tunnel port types (GRE, VXLAN, Geneve, STT) without manually building
ip tunnelinterfaces. - VLAN tagging and trunking.
- Integration with SDN controllers if you need centralized control across many hosts.
sFlow/NetFlowexport for traffic visibility.
This is the same switching technology behind OpenStack Neutron and a lot of production container networking before native overlay drivers matured, and it’s still very relevant when you need fine-grained flow control that a simple bridge can’t give you.
Prerequisites
- A Linux host with root access (Ubuntu 22.04 in my examples).
- Docker installed.
openvswitch-switchpackage.
Install OVS:
sudo apt-get update
sudo apt-get install -y openvswitch-switch
sudo systemctl enable --now openvswitch-switch
Confirm it’s running:
sudo ovs-vsctl show
Expected output on a fresh install:
07c1e6b2-1234-4abc-9def-abcdef012345
ovs_version: "3.1.0"
Step 1: Create an OVS Bridge
sudo ovs-vsctl add-br ovs-br0
sudo ip link set ovs-br0 up
sudo ip addr add 172.20.1.1/24 dev ovs-br0
Verify:
sudo ovs-vsctl show
Bridge ovs-br0
Port ovs-br0
Interface ovs-br0
type: internal
Step 2: Point Docker at the OVS Bridge
Docker’s daemon needs to know to use this bridge instead of docker0. I do this via daemon.json:
{
"bridge": "none"
}
Setting bridge: none tells Docker not to create or use its own bridge at all — I’ll manage container network attachment manually through OVS instead. Restart Docker:
sudo systemctl restart docker
Step 3: Attach a Container to the OVS Bridge
With Docker’s bridging disabled, I run a container with no networking, then manually create a veth pair and plug one end into the OVS bridge:
docker run -d --name web --net=none nginx:alpine
Get the container’s PID so I can enter its network namespace:
PID=$(docker inspect -f '{{.State.Pid}}' web)
Create the veth pair:
sudo ip link add veth-web type veth peer name veth-web-c
Attach the host-side end to the OVS bridge:
sudo ovs-vsctl add-port ovs-br0 veth-web
sudo ip link set veth-web up
Move the container-side end into the container’s network namespace and configure it:
sudo ip link set veth-web-c netns $PID
sudo nsenter -t $PID -n ip link set veth-web-c name eth0
sudo nsenter -t $PID -n ip addr add 172.20.1.10/24 dev eth0
sudo nsenter -t $PID -n ip link set eth0 up
sudo nsenter -t $PID -n ip route add default via 172.20.1.1
Test connectivity from the host:
curl 172.20.1.10:80
Expected: the default Nginx welcome page HTML.
I wrapped this whole sequence into a small script (ovs-docker-attach.sh) since typing it manually for every container gets old fast — Openvswitch actually ships a helper script called ovs-docker for exactly this purpose:
sudo ovs-docker add-port ovs-br0 eth0 web --ipaddress=172.20.1.11/24 --gateway=172.20.1.1
Step 4: Connect Two Hosts with an OVS GRE Tunnel
This is where OVS becomes noticeably nicer than hand-built GRE — the tunnel is just another port on the bridge, with no separate ip tunnel interface to manage.
On host A (192.168.1.10), bridging to host B (192.168.1.20):
sudo ovs-vsctl add-port ovs-br0 gre0 -- set interface gre0 type=gre options:remote_ip=192.168.1.20
On host B:
sudo ovs-vsctl add-port ovs-br0 gre0 -- set interface gre0 type=gre options:remote_ip=192.168.1.10
Now any container attached to ovs-br0 on either host is on the same broadcast domain, tunneled transparently over GRE. Verify the port was added correctly:
sudo ovs-vsctl show
Bridge ovs-br0
Port gre0
Interface gre0
type: gre
options: {remote_ip="192.168.1.20"}
Port veth-web
Interface veth-web
Test cross-host container connectivity the same way as before — assign container IPs from the same subnet on both hosts and ping between them.
Using VXLAN Instead of GRE
If I need to cross networks that don’t reliably pass GRE (protocol 47), VXLAN over UDP is often easier to get through firewalls and load balancers:
sudo ovs-vsctl add-port ovs-br0 vxlan0 -- set interface vxlan0 type=vxlan options:remote_ip=192.168.1.20 options:key=100
key=100 sets the VXLAN Network Identifier (VNI), letting me run multiple isolated overlay segments across the same physical link by using different VNIs per tunnel.
Step 5: Using OpenFlow Rules for Traffic Control
One of the biggest advantages of OVS is being able to define explicit flow rules instead of relying purely on MAC learning. For example, to drop all traffic from one container’s port except to a specific destination:
sudo ovs-ofctl add-flow ovs-br0 "priority=100,in_port=veth-web,ip,nw_dst=172.20.1.20,actions=normal"
sudo ovs-ofctl add-flow ovs-br0 "priority=90,in_port=veth-web,ip,actions=drop"
List current flows:
sudo ovs-ofctl dump-flows ovs-br0
This is effectively hand-rolled micro-segmentation, similar in spirit to what a Kubernetes NetworkPolicy gives you, just implemented at the flow-table level instead of through a CNI abstraction.
Docker Compose with an OVS-backed Network
Compose doesn’t speak OVS natively, so in practice I run the OVS setup as a pre-provisioning step (via a shell script or Ansible playbook) and then just use --net=none for services in Compose, relying on ovs-docker in a post_start hook or an external orchestration script:
version: "3.9"
services:
web:
image: nginx:alpine
network_mode: none
I then run ovs-docker add-port against the web container’s assigned name right after docker compose up -d. It’s not as elegant as a native Compose network driver, which is one reason most teams eventually move to Docker’s overlay driver or a CNI plugin instead of raw OVS for day-to-day workloads — OVS shines when you need the flow-level control, not when you just want basic multi-host reachability.
Monitoring and Troubleshooting
sudo ovs-vsctl show # bridge/port/interface topology
sudo ovs-ofctl dump-flows ovs-br0 # active flow rules
sudo ovs-appctl fdb/show ovs-br0 # MAC learning table
sudo ovs-vsctl list interface gre0 # tunnel interface statistics, including drop counters
Common problems:
- Tunnel port shows up but no traffic passes: check
ovs-vsctl list interface gre0forstatisticsshowingrx_errors, and confirm the underlying firewall allows the tunnel protocol (47 for GRE, UDP 4789 for VXLAN). - Container has no connectivity after attaching to OVS: double check the veth pair was actually moved into the container’s namespace (
nsenter -t $PID -n ip addrshould showeth0with the assigned IP). - MAC address conflicts across hosts: OVS doesn’t dedupe MACs for you; if you’re scripting container creation, generate MACs deterministically or let the veth default MACs handle it, but watch for collisions in large fleets.
Best Practices
- Use
ovs-dockerfor day-to-day port management instead of the fully manual veth/nsenter dance — it wraps all of the steps shown above into one command and reduces mistakes. - Prefer VXLAN over GRE when tunnels might cross restrictive firewalls, load balancers, or NAT devices, since UDP is generally easier to pass than a raw IP protocol.
- If you find yourself writing more than a handful of OpenFlow rules by hand, that’s usually a signal to look at an SDN controller (OVN, OpenDaylight) or move up to a CNI-based Kubernetes networking plugin that already builds this logic in.
- Back up your OVS database periodically (
ovs-vsctlstate lives in/etc/openvswitch/conf.db) — losing it means rebuilding bridge and flow configuration from scratch.
Integrating OVS with Kubernetes-Style Workflows
Even though this guide focuses on plain Docker, it’s worth knowing that the same OVS concepts scale directly into orchestrated environments. OVN (Open Virtual Network), built on top of OVS, is the networking backend for several production Kubernetes CNI plugins. The bridges, tunnel ports, and flow rules you’ve just configured by hand with ovs-vsctl and ovs-ofctl are conceptually identical to what OVN generates automatically from Kubernetes NetworkPolicy objects and Service definitions — the main difference is that OVN adds a centralized control plane that computes and pushes flow rules for you across an entire cluster, instead of you writing ovs-ofctl add-flow commands host by host. Understanding the manual OVS workflow first makes debugging an OVN-Kubernetes cluster substantially less mysterious when something in production isn’t routing the way you expect.
When OVS Is (and Isn’t) the Right Choice
I reach for raw OVS with Docker in a few specific situations: when I need VLAN trunking into an existing physical network that already uses 802.1Q segmentation, when I need flow-level traffic shaping or mirroring that a simple bridge can’t provide, or when I’m prototyping something that will eventually run on an OVN/OpenStack-based platform and I want the local dev environment to behave the same way. For everyday multi-tier application networking on a single host, a plain user-defined Docker bridge is simpler and requires far less manual wiring — I’d only introduce OVS’s added complexity when I actually need one of its specific capabilities.
Summary
Open vSwitch gives Docker networking a level of programmability the default Linux bridge simply doesn’t have: native tunnel port types, OpenFlow-based traffic control, and VLAN/VXLAN segmentation, all manageable through a consistent CLI (ovs-vsctl, ovs-ofctl). It takes more manual wiring than a purpose-built overlay tool, but that manual wiring is exactly what makes it flexible enough to underpin real SDN stacks like OpenStack Neutron and OVN-Kubernetes.
References
- Open vSwitch official documentation: https://docs.openvswitch.org/
- Docker network drivers overview: https://docs.docker.com/network/drivers/
- OVN (Open Virtual Network) project docs: https://docs.ovn.org/
- CNCF Cloud Native Landscape (networking category): https://landscape.cncf.io/