How to run OpenVPN and Pi-hole using Docker in a VPS

This is a tutorial on how to run OpenVPN and Pi-hole in Docker with zero experience. Since Docker is a beast with its documentations and there are countless tutorials out there, I will be skipping a lot of things. It is your responsibility to learn afterwards.


Table of Contents


Requirements

  • VPS – For this example, I will be using DigitalOcean. You can use any server you want as long as it can run Docker. I am currently hosted with BuyVM.
  • Domain – If don’t have one, you can register one for free at Freenom.

Setting Up – DigitalOcean

Go to the Create Droplets page and click on Marketplace tab. We’re gonna use a custom image to speed things up.

By using the Docker image, our VPS comes pre-installed with Docker and docker-compose. You can leave the other settings as default or modify location, that’s up to you.

Copy the IP address so we can paste it into the DNS of our new domain.

Setting Up – Freenom

Register your free domain, for this article, I registered a temporary domain pi-hole.ml for 1 month.

After you register your domain, go to your dashboard and click on the Manage Freedom DNS so we can enter the IP address of our VPS.

  • A – Leave the Name blank and paste your VPS IP as the target, this will be our main URL for Pi-hole’s dashboard
  • TRAEFIK CNAME – Subdomain for Traefik dashboard
  • VPN CNAME – Required for OpenVPN config

Setting Up – Traefik

docker run -t --rm \
-v traefik:/demyx \
demyx/utilities "touch /demyx/acme.json; chmod 600 /demyx/acme.json"

Before we bring up any Docker services, we need to create the acme.json for Traefik. The command creates a named volume “traefik” with the absolute path /demyx. This directory will house our SSL certs from LetsEncrypt and Traefik access/error logs.

Setting Up – OpenVPN

docker run --rm \
--log-driver=none \
-v ovpn-data-demyx:/etc/openvpn \
kylemanna/openvpn ovpn_genconfig -u udp://vpn.pi-hole.ml

docker run --rm -it \
--log-driver=none \
-v ovpn-data-demyx:/etc/openvpn \
kylemanna/openvpn ovpn_initpki

First command will generate the OpenVPN files in the named volume “ovpn-data-demyx” along with our subdomain URL for the UDP.

Second command will generate our keys. It will prompt you for a passphrase. You can enter your domain name as the Common Name in the second prompt.

docker run -it --rm \
-v ovpn-data-demyx:/etc/openvpn \
kylemanna/openvpn vi /etc/openvpn/openvpn.conf

# Config should look like this
push "block-outside-dns"
push "dhcp-option DNS 10.0.0.255"
push "comp-lzo no"

Execute the Docker command to edit openvpn.conf and point it to our Pi-hole’s IPv4 address: 10.0.0.255. Your config should look like the lines where it says “push.”

  • Once the terminal editor is opened, press the letter i to edit the text
  • Delete 1 of the DNS options and insert our custom address
  • To save: press ESC key, shift + colon, type wq, then press Enter key

Setting Up – Authentications

If you want to generate a strong password and basic auth, feel free to use my image.

docker run -it --rm demyx/utilities "pwgen -cns 50 1"
docker run -it --rm demyx/utilities "htpasswd -nb username password"

First command generates a 50 character password and the second one generates your basic auth, used for Traefik and Pi-hole dashboard security logins. Modify the parameters to your needs.


The YAML

YAML (“YAML Ain’t Markup Language”) is a human-readable data-serialization language. It is commonly used for configuration files, but could be used in many applications where data is being stored (e.g. debugging output) or transmitted (e.g. document headers).

Wikipedia
version: "3.7"
services:
  traefik:
    image: traefik:v1.7.16
    container_name: demyx_traefik
    restart: unless-stopped
    command: 
      - --api
      - --api.statistics.recenterrors=100
      - --docker
      - --docker.watch=true
      - --docker.exposedbydefault=false
      - "--entrypoints=Name:http Address::80"
      - "--entrypoints=Name:https Address::443 TLS"
      - --defaultentrypoints=http,https
      - --acme
      - [email protected] # CHANGE THIS
      - --acme.storage=/demyx/acme.json
      - --acme.entrypoint=https
      - --acme.onhostrule=true
      - --acme.httpchallenge.entrypoint=http
      - --logLevel=INFO
      - --accessLog.filePath=/demyx/access.log
      - --traefikLog.filePath=/demyx/traefik.log
    networks:
      - demyx
    ports:
      - 80:80
      - 443:443
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock:ro
      - traefik:/demyx
    labels:
      - "traefik.enable=true"
      - "traefik.port=8080"
      - "traefik.frontend.rule=Host:traefik.pi-hole.ml" # CHANGE THIS IF YOU WANT
      - "traefik.frontend.redirect.entryPoint=https"
      - "traefik.frontend.auth.basic.users=demyx:$$apr1$$NuLwN7lb$$t/P7yith6bh/n6eo/RZHg/" # CHANGE THIS
      - "traefik.frontend.headers.forceSTSHeader=true"
      - "traefik.frontend.headers.STSSeconds=315360000"
      - "traefik.frontend.headers.STSIncludeSubdomains=true"
      - "traefik.frontend.headers.STSPreload=true"  
  macbook:
    image: kylemanna/openvpn
    container_name: demyx_openvpn
    restart: unless-stopped
    volumes:
      - ovpn-data-demyx:/etc/openvpn
    cap_add:
      - NET_ADMIN
    ports:
      - 11941:1194/udp
    networks:
      - pihole
  iphone:
    image: kylemanna/openvpn
    container_name: demyx_openvpn_iphone
    restart: unless-stopped
    volumes:
      - ovpn-data-demyx:/etc/openvpn
    cap_add:
      - NET_ADMIN
    ports:
      - 11942:1194/udp
    networks:
      - pihole
  pihole:
    container_name: demyx_pihole
    image: pihole/pihole
    restart: unless-stopped
    volumes:
      - pihole:/etc/pihole
      - pihole-dnsmasq:/etc/dnsmasq.d
    environment:
      VIRTUAL_HOST: pi-hole.ml
      VIRTUAL_PORT: 80
      TZ: America/Los_Angeles # CHANGE THIS IF YOU WANT
      WEBPASSWORD: demyx # CHANGE THIS
      DNS1: "1.1.1.1"
      DNS2: "1.0.0.1"
    dns:
      - 127.0.0.1
      - 1.1.1.1
    labels:
      - "traefik.enable=true"
      - "traefik.port=80"
      - "traefik.frontend.rule=Host:pi-hole.ml" # CHANGE THIS IF YOU WANT
      - "traefik.frontend.redirect.entryPoint=https"
      - "traefik.frontend.auth.basic.users=demyx:$$apr1$$NuLwN7lb$$t/P7yith6bh/n6eo/RZHg/" # CHANGE THIS
      - "traefik.frontend.headers.forceSTSHeader=true"
      - "traefik.frontend.headers.STSSeconds=315360000"
      - "traefik.frontend.headers.STSIncludeSubdomains=true"
      - "traefik.frontend.headers.STSPreload=true"
    networks:
      demyx:
      pihole:
        ipv4_address: 10.0.0.255
volumes:
  ovpn-data-demyx:
    name: ovpn-data-demyx
  pihole:
    name: pihole
  pihole-dnsmasq:
    name: pihole-dnsmasq
  traefik:
    name: traefik
networks:
  demyx:
    name: demyx
  pihole:
    name: pihole
    ipam:
      driver: default
      config:
        - subnet: 10.0.0.0/16

Modify the necessary info first (email/domain/passwords), copy all of this, save it as docker-compose.yml, and then run:

docker-compose up -d
docker run -it --rm \
--name demyx_ctop \
-v /var/run/docker.sock:/var/run/docker.sock:ro \
quay.io/vektorlab/ctop

If everything went well, you should see all of the containers are up and running by executing the command above. You should also check if both of the dashboards are up. Both are protected by basic auth credentials:

  • Basic Auth Username: demyx
  • Basic Auth Password: demyx
  • Pi-hole Default Password: demyx
traefik.pi-hole.ml
pi-hole.ml/admin

Once you confirmed the services and URLs are up and running, we can now configure Pi-hole.


Pi-hole

Log-in using the password you put in WEBPASSWORD in the YAML, then go to the DNS tab in the Settings page. Be sure to hit Save at the bottom right of the page.

On the same page, click the Blocklists tab, add this url dbl.oisd.nl then click Save and Update.

  [i] Pi-hole blocking is enabled
  [i] Neutrino emissions detected...
  [✓] Pulling blocklist source list into range

  [i] Target: raw.githubusercontent.com (hosts)
  [✓] Status: Retrieval successful

  [i] Target: mirror1.malwaredomains.com (justdomains)
  [✓] Status: No changes detected

  [i] Target: sysctl.org (hosts)
  [✓] Status: No changes detected

  [i] Target: zeustracker.abuse.ch (blocklist.php?download=domainblocklist)
  [✓] Status: Retrieval successful

  [i] Target: s3.amazonaws.com (simple_tracking.txt)
  [✓] Status: No changes detected

  [i] Target: s3.amazonaws.com (simple_ad.txt)
  [✓] Status: No changes detected

  [i] Target: hosts-file.net (ad_servers.txt)
  [✓] Status: No changes detected

  [i] Target: dbl.oisd.nl (dbl.oisd.nl)
  [✓] Status: Retrieval successful

  [✓] Consolidating blocklists
  [✓] Extracting domains from blocklists
  [i] Number of domains being pulled in by gravity: 1641132
  [✓] Removing duplicate domains
  [i] Number of unique domains trapped in the Event Horizon: 1535639
  [i] Nothing to whitelist!
  [i] Number of regex filters: 0
  [✓] Parsing domains into hosts format
  [✓] Cleaning up stray matter

  [✓] DNS service is running
  [✓] Pi-hole blocking is Enabled

Notice from the output: Number of unique domains trapped in the Event Horizon: 1535639.

1.5+ MILLION!

Now go back to your dashboard and check the box on the far right.

One last thing is to add a cron job to auto update the block lists. I’m not sure Pi-hole container already does this but add it anyways:

docker exec demyx_pihole pihole updateGravity

OpenVPN

Notice in my YAML that I have 2 services of OpenVPN. This is to show per device queries used for Pi-hole dashboard logs. Also notice that each of the services are using different ports.

OpenVPN defaults to port 1194 but you can only use a port once on the host OS. What I’ve done is add an extra number at the end and increment it. You have to do this per device if you’re planning to add more and you will see why.

# Creating OpenVPN profiles

docker run --rm -it \
-v ovpn-data-demyx:/etc/openvpn \
--log-driver=none \
kylemanna/openvpn easyrsa build-client-full macbook nopass

docker run --rm -it \
-v ovpn-data-demyx:/etc/openvpn \
--log-driver=none \
kylemanna/openvpn easyrsa build-client-full iphone nopass

We’re gonna create our OpenVPN profiles per devices. As you can see, the commands are executed separately for each devices.

# Exporting OpenVPN profiles

docker run --rm \
-v ovpn-data-demyx:/etc/openvpn \
--log-driver=none \
kylemanna/openvpn ovpn_getclient macbook > macbook.ovpn

docker run --rm \
-v ovpn-data-demyx:/etc/openvpn \
--log-driver=none \
kylemanna/openvpn ovpn_getclient iphone > iphone.ovpn

Our profiles are exported but we’re not done yet. We need to change the default port (1194) that’s in the profiles by executing the commands below:

# Search and replace

sed -i 's/1194/11941/g' macbook.ovpn
sed -i 's/1194/11942/g' iphone.ovpn

Now you can export these profiles to your devices.


Connecting – Computer

OpenVPN has clients for all major operating systems but for some reason, the MacOS client is hidden deep in their website, link here. Linux does not need the client because all the major Desktop Environments (DE) have it built-in.

# Print out the macbook.ovpn profile

cat macbook.ovpn

Copy the output, create a file named macbook.ovpn, and save it. After you install the macOS OpenVPN client, import it.

Connecting – Phone

Repeat the steps from the macOS profile and either save it to your iCloud Drive, iMessage, or email it to yourself. I chose iCloud Drive. You will need to download the OpenVPN app from the App Store.

Connecting – Linux

I’m running Manjaro KDE so your settings will probably be different. Open your network manager and find a VPN panel.

Yes. You guessed correctly. I am running linux on an iMac.


Conclusion

As you can see, our devices are connected in the Network page. If you find any errors or want to improve upon this, then please leave a comment below. If you find this article useful then please share it!

Buy me a coffeeBuy me a coffee