How to Use OVS with Docker: Open vSwitch Networking and Bridge Configuration Guide

How to Use OVS with Docker

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 tunnel interfaces.
  • VLAN tagging and trunking.
  • Integration with SDN controllers if you need centralized control across many hosts.
  • sFlow/NetFlow export 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-switch package.

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 gre0 for statistics showing rx_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 addr should show eth0 with 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-docker for 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-vsctl state 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/

Total
0
Shares

Leave a Reply

Previous Post
How to Set Up a Custom Bridge for Docker

How to Set Up a Custom Bridge for Docker: Network Configuration and Container Connectivity

Next Post
How to Build a GRE Tunnel Between Docker Hosts

How to Build a GRE Tunnel Between Docker Hosts: Cross-Host Container Connectivity Guide

Related Posts