Build the Ultimate Security Stack with Docker on Raspberry Pi

If you click our links and make a purchase, we may earn an affiliate commission. Learn more

If you’ve ever wished you had a device you could plug in for security and privacy, you’re in the right place. Today, I’ll show you how to turn your Raspberry Pi into a security stack that works whether you’re at home or on the go.

Services like WireGuard, Pi-hole, and Unbound can be combined to create a security stack on a Raspberry Pi. They can be linked to run together via Docker containers to make setup convenient and portable.

Let’s start with a brief explanation of why you’d want to do this and how it works. Then, I’ll guide you through installing and testing everything.

If you’re new to Raspberry Pi or Linux, I’ve got something that can help you right away!
Download my free Linux commands cheat sheet – it’s a quick reference guide with all the essential commands you’ll need to get things done on your Raspberry Pi. Click here to get it for free!

Why Create a Security Stack with a Raspberry Pi?

banner pi server tweaks - Joshua Sortino / Unsplash / TDyan / RPTips

With a Raspberry Pi security stack, you can encrypt your internet requests, block ads, and speed up browsing. It works whether you’re on the road or at home, as long as your device is connected to your Pi.

There are different ways to achieve this goal. But if you’re like me, you don’t want complicated enterprise solutions at home, just something convenient.

That’s why the Raspberry Pi is perfect for this task:

  • Its operating system environment supports many network tools.
  • It runs 24/7 while using barely any electricity.
  • It takes up very little space.
  • It’s inexpensive compared to dedicated network appliances.

Using it for privacy was the first reason I bought my Raspberry Pi, and it’s why I love using the Raspberry Pi for network projects.

Key Software for a Raspberry Pi Security Stack

When I say security stack, I mean a set of software that will protect you while using the internet. This can mean different things to different people, but to keep this tutorial simple, I’ll be giving an example with the three tools below.

Let’s briefly explain what each component will accomplish in our stack.

Prefer reading without ads and popups?
Members get an ad-free version of every guide, plus exclusive project support.
Join the Community | Sign In

WireGuard

A self-hosted VPN server is the key to this setup. It lets you connect to your home network while you’re away. For our security stack, we’ll be using WireGuard, a fast and light VPN protocol.

To learn more about the concepts involved, check out our WireGuard installation guide. In this guide, however, I’ll be showing you a different way to install it alongside other tools.

Pi-Hole

pihole logo

Pi-hole is a free and open-source ad-blocker. Besides ads, Pi-hole also blocks trackers to keep your data private, even on mobile devices where blocking is more difficult. A nice side effect is faster web browsing, since these elements don’t even get a chance to load.

If you’re curious to learn more about how it works, check out our guide to Pi-hole here. I’ll be using a different installation method to roll it into our stack, so it’ll be a different process than the one outlined in the linked article.

Lost in the terminal? Grab My Pi Cheat-Sheet!
Download the free PDF, keep it open, and stop wasting time on Google.
Download now

Unbound

Don’t want your ISP to snoop on your browsing habits, tech companies to sell your data, or attackers to intercept your traffic? An answer to these problems is to roll your own DNS server. We covered similar concepts before in a different article (Install Cloudflared on Raspberry Pi).

But in this tutorial, we’ll be installing Unbound. Unbound is a self-hosted DNS server that will let you cache your DNS requests—hiding some of your traffic—while also encrypting them.

Putting It All Together

Let’s summarize how all of these pieces work together. You connect to WireGuard to access your home network and all of its goodies. When you use the internet, Pi-hole hands off your traffic requests to Unbound to hide them from prying eyes. Meanwhile, Pi-Hole will block any ads or trackers that try to snoop.

Pretty sweet, right?

Requirements for a Raspberry Pi Security Stack

Before I show you how to build this security stack, let’s confirm you have the hardware and software needed.

Hardware

  • Raspberry Pi – I recommend the Pi 4 or Pi 5.
    (It might work on the Pi 3B+, or it may be too slow.)
  • Memory – 2GB or more recommended.
  • Storage – 32GB recommended (here’s my favorite SD card).
  • Ethernet connection – Wired for speed and stability. Wi-Fi is a bad idea here.

Software

  • Operating System – Install a lightweight OS, like Raspberry Pi OS Lite. This will devote more system resources to processing traffic as quickly as possible.

    For this guide, I installed Raspberry Pi OS Lite in headless mode (follow our guide here). This will let you copy and paste commands as you follow along from your PC.

    (If you must have a graphical interface, this guide will still work on a desktop system.)
  • Docker – This container manager lets us deploy the entire setup as one integrated stack.

    Follow our guide to install the newest version of Docker—which includes Docker Compose—and then come back here when you’re done.

    (For those of you on desktops, you can install Portainer if you prefer to work from a GUI.)

How to Create the Security Stack with Containers

Sure, you could install everything on bare metal. In this guide, however, I show you how to do it with Docker instead.

Using containers lets us overcome any dependency requirements on the Raspberry Pi and lets us combine our stack into a single text file.

In short, this approach makes it easy, portable, and reproducible.
The big picture installation steps are:

  • Create a Docker Compose file to define the software needed.
  • Edit the environment file to set secrets like passwords.
  • Deploy the container to bring the security stack online.

Create a Docker Compose File

First, we will create a giant Docker configuration file with all of our services.

  • In your user home, create a folder with any name to hold your project:
    mkdir pistack
    cd pistack
  • Inside, create a new Docker Compose file:
    (It’s just a text file.)
    nano compose.yaml
  • Paste the configuration below (the indentation matters!).
  • Save & exit (CTRL+X).
networks:
  private_network:
    ipam:
      driver: default
      config:
        - subnet: 10.2.0.0/24

services:
  unbound:
    image: "mvance/unbound-rpi:latest"  #for ARM architecture
    container_name: unbound
    restart: unless-stopped
    hostname: "unbound"
    networks:
      private_network:
        ipv4_address: 10.2.0.200

  pihole:
    depends_on:
      - unbound
    container_name: pihole
    image: pihole/pihole:latest
    restart: unless-stopped
    hostname: pihole
    environment:
      TZ: ${TIMEZONE}
      FTLCONF_webserver_api_password: ${PIHOLE_PASSWORD}
      FTLCONF_dns_listeningMode: 'all'
      FTLCONF_dns_upstreams: 10.2.0.200;10.2.0.200  #set twice to override defaults
      ServerIP: 10.2.0.100  #internal IP of pihole
    volumes:
      - "./etc-pihole/:/etc/pihole/"
      - "./etc-dnsmasq.d/:/etc/dnsmasq.d/"
    cap_add:
      - NET_ADMIN
      - CAP_SYS_NICE
    networks:
      private_network:
        ipv4_address: 10.2.0.100

  wg-easy:
    depends_on:
      - unbound
      - pihole
    environment:
      - WG_HOST=${PUBLIC_IP}
      - PASSWORD_HASH=${WG_PASSWORD_HASH}
      - WG_PORT=51820
      - WG_DEFAULT_ADDRESS=10.6.0.x
      - WG_DEFAULT_DNS=10.2.0.100
    image: ghcr.io/wg-easy/wg-easy:14
    container_name: wg-easy
    volumes:
      - "./wireguard:/etc/wireguard"
    ports:
      - "51820:51820/udp"
      - "51821:51821/tcp"
    restart: unless-stopped
    cap_add:
      - NET_ADMIN
      - SYS_MODULE
    sysctls:
      - net.ipv4.ip_forward=1
      - net.ipv4.conf.all.src_valid_mark=1
    dns:
      - 10.2.0.100  #points to pihole
      - 10.2.0.200  #points to unbound
    networks:
      private_network:
        ipv4_address: 10.2.0.3

(Adapted from the work of IAmStoxe, 10h30, and m-karakus with modifications.)

That’s everything we need—WireGuard, Pi-Hole, and Unbound—in one giant text file.

Notes for the curious:

  • The unbound-rpi image is specifically used to support Raspberry Pi architecture.
  • The wg-easy image is the VPN and also provides a web panel for easy configuration.
  • The container creates a network on the 10.2.0.xxx range.
    Pi-hole gets 10.2.0.100; Unbound gets 10.2.0.200; WireGuard clients get 10.6.0.xxx.

Edit the Environment File

Next, we will create an environment file. You may have noticed that I left some key bits out of the Compose file. That’s because the best practice for secrets, like passwords, is to keep them separate from the container. Plus, we can change them more easily when needed.

Lost in the terminal? Grab My Pi Cheat-Sheet!
Download the free PDF, keep it open, and stop wasting time on Google.
Download now

Here’s what to put in your ENV file:

  • In your project folder, create an environment file:
    nano .env
  • Paste the lines below:
# Set variables here for the Docker Compose file
# Set your public IP address
PUBLIC_IP="my.ddns.net"

# Set your time zone
TIMEZONE="America/Los_Angeles"

# Set the password for Pi-hole
PIHOLE_PASSWORD="passwd"

# Set the password hash to access the Wireguard UI
# You can generate a hash here: https://bcrypt-generator.com 
# Default password is "passwd"
WG_PASSWORD_HASH='$2a$12$cQy4dZa0FNvD3056euNMdeymf5u6LuMvU8zyv3lQAlx1hEbe.ORe.'
  • Change these entries:
    • PUBLIC_IP to your public IP address (e.g.,104.21.68.125).
      Or set it to your dynamic DNS address if you have one, like my.ddns.net.
    • TIMEZONE to your time zone identifier.
    • PIHOLE_PASSWORD to the password you want for the Pi-hole admin panel.
      (Do this later after you get everything working.)
    • WG_PASSWORD_HASH to the password you want for the WireGuard admin panel.
      (Do this later after you get everything working. Note the single quotes.)
  • Save & exit (CTRL+X).
  • Protect your environment file by restricting its permissions:
    chmod 600 .env

When you’re done with configuration, your folder should have only 2 files, like this:

These files tell Docker what to build and deploy.

Deploy the Container

Okay, it’s time to get this show on the road!
Third, let’s launch your Pi security stack.

From inside your project folder, run:
docker compose up

Docker should start pulling (downloading) and running the containers you set in your YAML file.
Give it a few moments, and your security stack should come online!

How to Use Your Raspberry Pi Security Stack

Now that you’re done deploying your Raspberry Pi security stack, let’s test it out.
In this section, you’ll learn how to:

  • Connect to your VPN when away from home.
  • Check if ads and trackers are being blocked.
  • Check if DNS requests are being protected.

Connect to the VPN

To use the security stack, you’ll need to approve which devices are allowed to connect remotely. Start by doing these steps at home while on the same local network as your Pi.

Enable Port Forwarding for WireGuard

To be able to reach your Raspberry Pi over the internet, you’ll need to enable port forwarding on your network router. Hopefully, you’ve already assigned your Raspberry Pi a static IP address before setting this up.

Log in to your router’s admin panel, and enable port forwarding to your Pi on port 51820, the default port for WireGuard.

Add VPN Clients

To approve which client devices can connect, we’ll be using the WireGuard-UI web interface:

  • From a PC on your home network, visit http://IP:51821 in your browser.
    Replace IP with your Raspberry Pi’s local IP address (e.g., 192.168.1.130:51821).
  • Log in with your password.
    The default was ‘passwd’ unless you changed it.
  • Click the “+ New Client” button.
    You’ll be asked to give the client device a name.
    The next steps will be slightly different depending on the WireGuard app on your iPhone / Android / Linux / PC / or Mac device.
    • Example 1 – smartphone: I enter ‘phone’ for my first device. I click the “Show QR code” icon to generate a QR code.

      On my phone, I install the WireGuard client from the app store. I add a new VPN connection, followed by “Create from QR code.” I scan the code from earlier and give the server a name, like “pivpn” (or any name you’d like).
    • Example 2 – PC: I add another client to approve my Windows laptop. I press the “Download Configuration” button to generate a WG config file.

      On my Windows laptop, I import that file into its WireGuard client to create the connection.
Lost in the terminal? Grab My Pi Cheat-Sheet!
Download the free PDF, keep it open, and stop wasting time on Google.
Download now

Repeat these steps for every client device you want to have access when you’re away from home: 1) add the device via the web panel, and 2) import the connection to the device’s client app.

Enable VPN Split Tunnelling

IMPORTANT! You’ll want to configure VPN split-tunnelling on every client device. The goal of this stack is to provide privacy and security, but we don’t want to encrypt all traffic, as that’ll slow things down and require more complexity.

To enable VPN split tunnelling on each WireGuard client:

  • Edit the settings for your VPN connection.
    For example, on my Windows laptop, I right-click and choose “Edit selected tunnel…”
  • On the “AllowedIPs” line, replace 0.0.0.0/0 with the 10.2.0.0/24 subnet.
  • Unmark the “Block untunneled traffic (kill-switch)” checkbox.
    This makes it so only accessing Pi-Hole/Unbound goes through the VPN.
  • The full line should now look like this:
    10.2.0.0/24, ::/1, 8000::/1
    (If you’re using a phone or tablet, you may have to manually enter this full line.)
  • Optional: If you want to access other machines on your home network, like a NAS, you can also add the local subnet for them. For example:
    10.2.0.0/24, 192.168.1.0/24, ::/1, 8000::/1

Test the VPN Connection

Now that the setup is complete, activate the VPN connection on your laptop/phone/tablet.

Note: You’ll want to connect to your VPN from outside of your home network to test things. In my case, I turned off my phone’s Wi-Fi and used the cellular network.

Did you want to test it or use it while at home instead?
If so, see this FAQ question at the bottom.

Voilà, you’re connected!
Two quick tests will tell us if our VPN is working as intended:

  • Visit http://10.2.0.100/admin in your web browser.
    Does the Pi-hole admin panel load?
    If yes, that means the VPN is tunnelling into your local network correctly.
  • Visit an external site, like https://raspberrytips.com, in your web browser.
    Does the page load?
    If yes, that means split-tunnelling was also set up properly.

That was the hardest part, making sure you’re able to VPN remotely to your Raspberry Pi at home. Now let’s verify if the other security/privacy measures of our stack are functional.

Is Pi-hole Blocking?

Let’s check if Pi-hole is blocking ads/trackers/malware while on the VPN:

  • Visit the Pi-hole admin panel (http://10.2.0.100/admin) in your browser.
    Enter the password you set earlier, and the Pi-hole dashboard should appear.
    This verifies that your Pi-Hole server is operational.
  • Visit AdBlock Tester, a site that tells you how effective your adblocking is.
    At the bottom of the page, you’ll see your score.
    I scored 96 out of 100 with this setup, meaning it’s blocking almost everything nasty.

Is Unbound Protecting DNS?

Unbound is supposed to protect our DNS requests, so let’s verify if it’s actually doing its job.

First, let’s check if Unbound is in charge of your connection’s DNS. Go to your Pi-hole admin panel again, and navigate to Settings > DNS. The boxes at the top (Google, Quad9, etc.) should all be unchecked, and the Custom DNS Servers should look like this:

The Unbound server at 10.2.0.200 is handling all DNS requests.

Second, let’s verify if DNSSEC validation is active.
In your browser, visit dnscheck.tools, and you should pass with flying colors:

Third, let’s check if DNS encryption is in effect.
Visit https://one.one.one.one/help/, and it should say that ‘DoT’ is enabled:

That’s everything! Now, any time you want to be secure while on the go, you can connect to your Raspberry Pi, and it’ll handle the rest for you, wherever you are. And if you ever need to recreate this setup somewhere else, all it takes is pasting the two text files above.


🛠 This tutorial doesn't work anymore? Report the issue here, so that I can update it!

Want to connect with other Raspberry Pi fans? Join the RaspberryTips Community. Ask questions, share your projects, and learn from each other. Join now.

FAQ

Can I also use my Pi security stack locally?

Lost in the terminal? Grab My Pi Cheat-Sheet!
Download the free PDF, keep it open, and stop wasting time on Google.
Download now

Yes, it works locally as long as you’re connected to the WireGuard VPN.

To make it work, open the VPN configuration on your client device.
Modify “Endpoint” to point to your Raspberry Pi’s local IP address (e.g., 192.168.1.130).

It might look like this:

Then, your device should be able to connect to the Raspberry Pi’s VPN even if you’re at home, and the rest of the protection will kick in.

How do I run the Docker container in the background?

On the container’s status screen, you can type ‘d’ to send it to the background.

Alternatively, you can launch the container with the detach flag:
docker compose up -d

How do I port over my Raspberry Pi security stack?

The best thing about this setup is how simple it is to recreate:

  • If you want to start fresh, copy only the text inside the compose.yaml and .env files.
  • If you have settings you want to keep, like approved VPN clients, copy the entire project folder over to your new setup.

Bring the containers up on your new system, and you’re back in business in no time.
These same steps can be used for making backups, too.

How do I update my Raspberry Pi security stack?

To update your containers, run these commands from inside your project folder:

  • Download the latest images:
    docker compose pull
  • Relaunch the containers:
    docker compose up

Pi-hole and Unbound should update to the latest version this way. WireGuard will not, as I’ve pinned the image to a stable release (v14) to avoid unexpected breakage. Advanced users can figure out how to load the latest version if they’re willing to beta test it.

Why didn’t you make Unbound run as a recursive DNS resolver?

I don’t recommend running Unbound as a recursive DNS resolver for this particular use.
It creates too many tradeoffs:

  • Requires more complex configuration that can break in unexpected ways.
  • Slows down the internet connection for clients.
  • Loses the ability to encrypt DNS requests.

Advanced users can still run Unbound recursively if they really want. Here’s a hint: mount the volume in the YAML to get the unbound.conf file, and then modify it.

Whenever you’re ready, here are other ways I can help you:

Test Your Raspberry Pi Level (Free): Not sure why everything takes so long on your Raspberry Pi? Take this free 3-minute assessment and see what’s causing the problems.

The RaspberryTips Community: Need help or want to discuss your Raspberry Pi projects with others who actually get it? Join the RaspberryTips Community and get access to private forums, exclusive lessons, and direct help.

Master your Raspberry Pi in 30 days: If you are looking for the best tips to become an expert on Raspberry Pi, this book is for you. Learn useful Linux skills and practice multiple projects with step-by-step guides.

Master Python on Raspberry Pi: Create, understand, and improve any Python script for your Raspberry Pi. Learn the essentials step-by-step without losing time understanding useless concepts.

You can also find all my recommendations for tools and hardware on this page.

Similar Posts