Media Center Series Part 2: Wireguard with Docker on Raspberry Pi 4

Originally my media center was a docker containers with OpenVPN to handle VPN traffic. Since they I have updated my setup to use Wireguard instead and I’m documenting the resources I found useful in case anyone wants to do the same. Two main reasons drove me to move from OpenVPN to Wireguard:

The first reason is that my old provider Private Internet Access was bought out by Kape. There haven’t been any changes to PIA yet but the new owner is a bit worrying considering their track record. So I have changed to Mullvad as my new provider and they have very good Wireguard support.

The second reason is that Wireguard offers some compelling improvements over OpenVPN:

  • OpenVPN is very resource demanding on the RPi. The Model 4 Raspberry Pi has a new I/O architecture which can properly use Gigabit LAN. Unfortunately the CPU overhead for OpenVPN was high enough that it limited throughput. My Pi would regularly have a core pinned near 100% utilization from OpenVpn. In comparison Wireguard’s performance is considerably lighter leading to better throughput.
  • OpenVPN runs in user space where as Wireguard will be included in the kernel eventually. For now this complicates things a bit since it has to be installed separately but once the changes land it will make setup much easier.

In terms of researching how to set this up three articles were particularly useful:

Installing Wireguard on Debian

First I wanted general information about installing the Wireguard kernel modules and user space tools.

The Debian wiki generally outlines the process but leaves some details to the reader in terms of file permissions. Linode’s covered those in better details. I only care about the client configuration details since I am not running a server. Additionally instead of writing my own configuration file I used the Mullvad Wireguard configuration tools to generate one.

I also needed to install the following to follow the setup:

sudo apt install openresolv resolvconf

Using Wireguard with Docker

There’s a blog post that describes two solutions for using Wireguard with docker. The first involves creating a docker container that runs the Wireguard client. I went with the second approach instead which covers setting up a Wireguard interface on the host and using in a Docker network.

Raspberry Pi Specific Changes for Installation

Finally I want to do this on a Rapsberry Pi 4 which requires additional steps to install raspberrypi-kernel-headers and get Wireguard to run.

Putting It All Together With Ansible

Handling the Mullvad config file

The playbook assumes there is a file called “mullvad.conf” in the same directory as playbook which is the configuration file downloaded from the Mullvad configuration generation tool and the vars section uses the ini plugin for lookups to read the server Address and DNS information from config file:


  - name: Wireguard
    connection: ssh
    become_user: root
    become: yes
    hosts: rpi
      address: "{{ lookup('ini', 'Address section=Interface file=mullvad.conf').split(',')[0] }}"
      dns: "{{ lookup('ini', 'DNS section=Interface file=mullvad.conf') }}"
      subnet_ip: ""
      - name: Print Mullavd Address
          msg: "Address is: {{ address }}"
      - name: Print Mullvad DNS
          msg: "DNS is: {{ dns }}"

Note the we have to split the address on “,” since the INI entry contains both the ipv4 and ipv6 address and we only want the first half

Installing Wireguard

The installation process is basically a combination of the two posts on installing, but replacing command with ansible tasks to clean it up:

      - name: enable unstable
          path: /etc/apt/sources.list.d/unstable-wireguard.list
          create: yes
          line: deb unstable main
      - name: enable wireguard repo
          path: /etc/apt/preferences.d/limit-unstable
          create: yes
          block: |
            Package: *
            Pin: release a=unstable
            Pin-Priority: 150
      - name: Add first key
          id: 8B48AD6246925553
      - name: Add second key
          id: 7638D0442B90D010
      - name: Add third key
          id: 04EE7237B7D453EC
      - name: Install deps
            - raspberrypi-kernel-headers
            - dirmngr
            - wireguard-dkms
            - wireguard-tools
            - resolvconf
          update_cache: yes

Configuring the Wireguard Interface

Finally we want to set up the wireguard interface that docker will use. We do this by first copying the Mullvad config over to the machine. Then as noted by the Wireguard on Docker article we remove the “Address” and “DNS” options from the config file since we have to manually configure the interface instead of using the wg-quick command. Then we are free to setup up the interface and configure it:

      - name: Copy config to server
          src: mullvad.conf
          dest: /etc/wireguard/wg1.conf
      - name: remove Address entry
          path: /etc/wireguard/wg1.conf
          section: Interface
          option: Address
          state: absent
      - name: remove DNS entry
          path: /etc/wireguard/wg1.conf
          section: Interface
          option: DNS
          state: absent
      - name: add wireguard interface
        command: "ip link add dev wg1 type wireguard"
      - name: set wireguard config
        command: "wg setconf wg1 /etc/wireguard/wg1.conf"
      - name: set wireguard address
        command: "ip address add {{ address }} dev wg1"
      - name: put interface up
        command: "ip link set up dev wg1"
      - name: set nameserver
        command: "printf 'nameserver %s\n' '{{ dns }}' | resolvconf -a tun.wg1 -m 0 -x"
      - name: handle reverse path filtering
        command: "sysctl -w net.ipv4.conf.all.rp_filter=2"

Creating the Docker Network

Finally all that is left is to create the docker network and setup the firewall to route it through wireguard:

- name: create docker network
          name: docker-vpn0
            - subnet: "{{ subnet_ip }}"
      - name: add firewall rule
        command: "ip rule add from {{ subnet_ip }} table 200"
      - name: add firewall route
        command: "ip route add default via {{ address.split('/')[0]}} table 200"