Tag: Docker

  • Installing WireGuard on Debian 13 with Docker (The Manual Way)

    Why I Built It This Way (And Why You Might Want To)

    There are dozens of tutorials showing how to install WireGuard in less than five minutes.

    Most of them look something like this:

    Install Docker.

    Run a container.

    Scan a QR code.

    Done.

    And honestly…there’s nothing wrong with that.

    Projects like wg-easy have made deploying WireGuard incredibly simple, and for many home users that’s exactly the right solution.

    But I wasn’t looking for the fastest deployment.

    Like all my projects I go through, I wanted to understand how everything actually worked so I could replicate at whim.

    I wanted to know where the keys were stored, how peers authenticated each other, how packets travelled from my phone to my home network, why IP forwarding was necessary, what the firewall was really doing, and how Docker fit into the picture.

    In other words, I wanted to build it and understand it—not just run it.

    That decision turned what should have been a one-hour installation into a weekend project full of experimentation, troubleshooting, and learning. Looking back, I wouldn’t change it.

    I now understand WireGuard far better than if I had simply copied a docker-compose.yml file from GitHub and hoped for the best.

    If you’re studying for networking certifications, building a homelab, working in IT, or simply enjoy understanding the technology you’re using, I think you’ll get much more out of the manual approach.


    What This Guide Covers

    By the end of this guide you’ll have:

    • A minimal Debian 13 server
    • Docker installed and configured
    • A manually configured WireGuard server
    • Secure VPN access from your phone or laptop
    • Access to your home LAN while away
    • The option to route all your Internet traffic through your home connection
    • A much better understanding of how WireGuard actually works

    Along the way we’ll also look at the networking concepts that many tutorials completely skip:

    • Public and private keys
    • Routing
    • NAT
    • IP forwarding
    • Docker host networking
    • Firewall rules
    • Port forwarding
    • Split tunnels versus full tunnels
    • Common troubleshooting techniques

    This isn’t just about getting a VPN working.

    It’s about understanding why it works.


    Why WireGuard?

    If you’ve worked in IT for any length of time, you’ve almost certainly encountered VPN technologies like:

    • OpenVPN
    • IPSec
    • L2TP
    • PPTP (hopefully only in legacy environments!)
    • SSL VPNs from vendors like Fortinet, Sophos or Palo Alto

    Most of them work well.

    Some have been around for decades.

    But WireGuard takes a very different approach.

    Instead of supporting dozens of encryption algorithms, configuration options and legacy compatibility modes, WireGuard keeps things intentionally small.

    Very small.

    The entire WireGuard codebase is only a fraction of the size of OpenVPN or IPSec implementations, making it significantly easier to audit and maintain.

    The result is a VPN that is:

    • Fast
    • Lightweight
    • Secure by default
    • Easy to configure once you understand the basics
    • Available on virtually every modern operating system

    It’s no surprise that many organisations are beginning to adopt WireGuard for remote access, site-to-site tunnels, cloud connectivity and even internal infrastructure.

    For a homelab, it’s hard to beat.


    Why Docker?

    Another question you might be asking is:

    “Why install WireGuard inside Docker instead of directly on Debian?”

    That’s a fair question.

    There’s absolutely nothing wrong with installing WireGuard directly using your package manager.

    In fact, if this were a dedicated VPN appliance, that’s probably the route I’d take.

    However, I wanted this machine to become more than just a VPN server.

    It would eventually host additional services such as:

    • AdGuard Home
    • Nginx Proxy Manager
    • Monitoring tools
    • Utility containers
    • Future self-hosted applications

    Docker makes that incredibly easy.

    Each application lives inside its own isolated container.

    Updating one service doesn’t affect another.

    Backing everything up becomes much simpler (see my post on How to Back Up a Docker Container Manually on Debian to rebuild in Minutes (Disaster Recovery Guide)).

    Rebuilding the server is largely a matter of restoring a few folders and starting the containers again.

    For homelabs, Docker has become one of those technologies that’s simply too useful to ignore.


    💡 Why this matters

    One of the biggest misconceptions about Docker is that it somehow replaces Linux networking.

    It doesn’t.

    Docker still relies on Linux networking underneath.

    That means concepts like routing, firewall rules, NAT and IP forwarding are still just as important.

    In fact, understanding them becomes even more valuable.

    As you’ll see later in this guide, several of the issues I encountered had nothing to do with WireGuard itself—they were Linux networking problems that happened to affect WireGuard.

    Understanding the layers involved makes troubleshooting dramatically easier.


    Why I Didn’t Use wg-easy

    This will probably be the most controversial decision in this article.

    There is an excellent Docker project called wg-easy.

    It provides:

    • A web interface
    • Automatic key generation
    • QR codes for phones
    • Peer management
    • Configuration downloads
    • Client statistics

    It’s fantastic.

    If your goal is simply to get a VPN working as quickly as possible, I’d happily recommend it.

    So why didn’t I use it?

    Because it hides all the interesting parts.

    When something goes wrong, I don’t want to wonder what the web interface generated.

    I want to open the configuration file and immediately understand what every single line is doing.

    I wanted to manually:

    • Generate every key
    • Write every configuration file
    • Build the Docker Compose file
    • Configure routing
    • Configure NAT
    • Configure firewall rules
    • Understand every command I was running

    That knowledge is worth far more than the few hours saved by using a management interface.

    Ironically, after completing the manual installation, I now understand exactly what tools like wg-easy are doing behind the scenes.


    ⚠️ Mistake I Made

    When you’re learning something new, it’s tempting to keep replacing components every time you hit a problem.

    Maybe it’s Docker.

    Maybe it’s Debian.

    Maybe it’s WireGuard.

    Maybe it’s the firewall.

    Maybe you should reinstall everything…

    Resist that temptation.

    Throughout this build I forced myself to solve each issue instead of starting over.

    That approach taught me far more than a clean installation ever would have.

    Many of the troubleshooting sections later in this guide exist because I deliberately followed problems to their root cause instead of wiping the machine and trying again.

    Those lessons have already proven useful in completely unrelated projects.


    What We’re Building

    At a high level, the design is surprisingly simple.

    Phone / Laptop
            │
            │ WireGuard VPN Tunnel
            ▼
    Internet
            │
    ISP Router
            │
    Firewall
            │
    Debian 13
    Docker
    WireGuard Container
            │
    Home Network

    When you’re away from home, your phone establishes an encrypted tunnel back to the WireGuard server.

    Once connected, it behaves almost as though it were physically plugged into your home network.

    Depending on how you configure the client, you can either:

    • Access only your home devices (split tunnel), or
    • Send all Internet traffic through your home connection (full tunnel).

    We’ll look at both options later.


    Before We Begin

    This guide assumes you’re comfortable with Linux and the command line.

    You don’t need to be an expert, but you should already know how to:

    • SSH into a server
    • Edit configuration files
    • Restart services
    • Read terminal output
    • Work with Docker Compose

    I’ll explain why we’re doing each step, but I won’t spend much time covering basic Linux administration.

    If you’re new to Docker or Debian, don’t worry—you’ll still be able to follow along, and hopefully you’ll come away with a much better understanding of both.


    Coming Up Next

    Now that we’ve covered the reasoning behind the project, it’s time to start building.

    In the next part we’ll prepare Debian for its new role by:

    • Installing the required packages
    • Creating a proper administrative user
    • Configuring a static IP address
    • Installing Docker
    • Verifying everything is ready before we touch WireGuard itself

    As with the rest of this guide, we’ll also explain why each step matters—not just which commands to copy and paste.


    Preparing Debian

    With the planning out of the way, it’s time to start building.

    One thing I’ve learned over the years is that taking an extra ten minutes to prepare a server properly almost always saves hours of troubleshooting later.

    Rather than jumping straight into installing WireGuard, we’ll first make sure the operating system is configured correctly. Think of this as laying the foundations before building the house.

    In this section we’ll:

    • Create a proper administrative user
    • Install the packages we’ll need later
    • Configure a static IP address
    • Install Docker
    • Verify everything is working before adding WireGuard

    None of these steps are particularly difficult, but each one serves a purpose.


    Starting with a Minimal Debian Installation

    For this guide I’m using a fresh installation of Debian 13 (Trixie).

    A minimal installation is ideal because it contains very little beyond the operating system itself. There are fewer running services, fewer packages to update, and a much smaller attack surface.

    If you’re building a dedicated server, that’s exactly what you want.

    Once the installation is complete, connect to the machine either directly or via SSH using the account created during installation.

    Before doing anything else, it’s always worth updating the package lists and installing any available updates.

    apt update
    apt upgrade -y

    Keeping a system fully updated before adding software avoids chasing problems that have already been fixed upstream.


    Creating a Proper Administrative User

    If you’re logged in as root, now is a good time to stop using it.

    Although it’s tempting to perform everything as the root user, it’s considered best practice to use a normal account with sudo privileges instead.

    This provides an additional layer of protection against accidental mistakes and makes it obvious which commands require elevated privileges.

    If sudo isn’t already installed, install it first.

    apt install sudo

    Then add your existing user to the sudo group.

    usermod -aG sudo <username>

    Log out and back in again so the new permissions take effect.

    Finally, confirm everything is working correctly.

    sudo whoami

    If everything has been configured correctly, the command should return:

    root

    From this point onwards, I’ll assume commands requiring elevated privileges are prefixed with sudo.


    💡 Why this matters

    Running everything as root works… until it doesn’t.

    Accidentally deleting a directory or overwriting a configuration file as root can have immediate consequences.

    Using sudo adds a small safety net and aligns your server with standard Linux administration practices.


    Assigning a Static IP Address

    A VPN server should always have a predictable address on your network.

    If your router assigns a different IP address after a reboot, port forwarding, firewall rules and monitoring systems may all stop working.

    There are two common ways to achieve this:

    • Reserve a DHCP lease on your router or firewall.
    • Configure a static address directly on the server.

    Both approaches are perfectly valid.

    Personally, I prefer assigning important infrastructure devices fixed addresses so I always know where they live on the network.

    Before editing anything, identify the name of your network interface.

    ip addr
    or
    ip a

    On modern Debian systems you’ll usually see names similar to:

    • enp1s0
    • ens18
    • eno1

    rather than the old eth0 naming convention.

    Edit your network configuration using whichever networking method your installation uses.

    Assign:

    • A static IP address
    • The correct subnet mask
    • Your default gateway
    • At least one DNS server

    After applying the changes, verify that the new address has been assigned correctly.

    ip addr show <interface>

    Then test connectivity.

    ping 1.1.1.1

    and

    ping google.com

    The first confirms basic network connectivity.

    The second confirms DNS resolution is also working.

    Both tests are important.


    ⚠️ Mistake I Made

    At one point I assumed the network was broken because external websites couldn’t be reached.

    The server could successfully ping public IP addresses, but hostnames failed to resolve.

    The culprit wasn’t routing at all—it was DNS.

    Always test both IP connectivity and DNS separately before assuming you have a networking problem.


    Installing Docker

    Although WireGuard can be installed directly on Debian, we’ll be running it inside a Docker container.

    Installing Docker from Docker’s own repositories ensures you’re using the latest supported version rather than an older package from the Debian repositories.

    First install the required dependencies.

    sudo apt install ca-certificates curl gnupg

    Then add Docker’s official repository and install Docker Engine along with Docker Compose.

    Follow the official Docker installation instructions for Debian, as these are kept up to date whenever packages or signing keys change.

    Once installation completes, verify everything is working.

    docker --version

    You should also confirm Docker Compose is available.

    docker compose version

    Both commands should return version information without errors.


    Allowing Your User to Run Docker

    By default, Docker commands require root privileges.

    Rather than prefixing every command with sudo, it’s common practice to add your account to the Docker group.

    sudo usermod -aG docker <username>

    Log out and back in again.

    Now try:

    docker ps

    If no permission errors appear, everything is configured correctly.


    💡 Why this matters

    Docker commands executed with sudo work perfectly well.

    Adding your user to the Docker group is largely a convenience.

    However, it’s worth remembering that Docker group membership effectively grants root-equivalent privileges on the machine.

    Only trusted administrators should belong to this group.


    Verifying the Environment

    Before installing WireGuard, spend a few minutes confirming the basics.

    Can the server:

    • Reach the Internet?
    • Resolve DNS names?
    • Pull Docker images?
    • Retain its static IP after a reboot?
    • Run containers successfully?

    A few simple checks now can save significant troubleshooting later.

    Try pulling a small image.

    docker run hello-world

    If Docker downloads the image and prints its success message, you’ve confirmed:

    • Internet connectivity
    • DNS resolution
    • Docker Engine
    • Container execution

    That’s a surprisingly useful health check.


    Building Good Habits Early

    It’s easy to get excited about deploying new services and skip straight to the interesting part.

    I’ve done it myself more times than I’d like to admit.

    But infrastructure has a habit of punishing shortcuts.

    Taking a little extra time to confirm the operating system is healthy before adding applications almost always pays dividends later.

    When something eventually goes wrong—and at some point, something always does—you’ll already know the underlying operating system wasn’t the problem.

    That narrows your troubleshooting considerably.


    Up Next

    Now that Debian is configured and Docker is running, we can finally turn our attention to WireGuard itself.

    In the next section we’ll generate our cryptographic keys, build the project directory structure, write the WireGuard configuration manually, and look at what every line in wg0.conf actually does before we ever start the container.

    Building the WireGuard Server

    With Debian prepared and Docker up and running, it’s finally time to build the VPN server itself.

    Unlike many tutorials, we’re not going to deploy a container that automatically generates configuration files behind the scenes.

    Instead, we’ll build everything manually.

    That means generating our own key pairs, creating the configuration file from scratch, understanding every option inside it, and finally letting Docker do what Docker does best—run the application.

    It might take a little longer, but by the end of this section you’ll understand why WireGuard works, rather than simply knowing that it does.


    Understanding How WireGuard Authenticates Devices

    One of the first things you’ll notice about WireGuard is that there are no usernames or passwords.

    Instead, every device is identified using a pair of cryptographic keys.

    Each peer has:

    • A private key, which must remain secret.
    • A public key, which can safely be shared with other peers.

    If you’ve ever used SSH key authentication, the concept is almost identical.

    The private key proves your identity.

    The public key tells the server who is allowed to connect.

    No passwords are exchanged.

    No certificates are required.

    No certificate authority needs to be maintained.

    Each device simply trusts the public keys you’ve explicitly configured.

    It’s an elegant design, and one of the reasons WireGuard is so lightweight.


    💡 Why this matters

    A common misconception is that WireGuard authenticates users.

    It doesn’t.

    It authenticates devices.

    If you connect from your phone and your laptop, those are two separate peers, each with its own unique key pair.

    Never reuse keys between devices.

    If one device is ever lost or compromised, you can revoke that single peer without affecting any of the others.


    Creating the Project Structure

    Although Docker doesn’t care where you store your project files, keeping everything organised makes administration much easier.

    A simple directory structure might look something like this:

    wireguard/
    ├── docker-compose.yml
    └── config/
        └── wg_confs/
            └── wg0.conf

    As the project grows, you’ll also have directories containing keys, backups and any supporting configuration.

    Keeping everything together makes moving the project to another server as simple as copying one folder.


    Generating the Server Keys

    WireGuard includes a small utility that generates cryptographic keys.

    Creating a key pair takes only a few seconds.

    Generate the server’s private key.

    wg genkey | tee server_private.key

    Then derive the matching public key.

    cat server_private.key | wg pubkey > server_public.key

    Repeat exactly the same process for your first client device.

    For example:

    wg genkey | tee phone_private.key
    cat phone_private.key | wg pubkey > phone_public.key

    You should now have four files:

    • Server private key
    • Server public key
    • Client private key
    • Client public key

    Each has a specific purpose.

    The server keeps its private key secret.

    The client keeps its private key secret.

    The server stores the client’s public key.

    The client stores the server’s public key.

    That’s the entire trust relationship.


    Keeping Your Private Keys Safe

    It should go without saying, but your private keys should remain exactly that—private.

    Never:

    • Email them
    • Upload them to GitHub
    • Store them in public repositories
    • Paste them into forums
    • Share screenshots containing them

    Anyone with your private key effectively becomes that device.

    Treat it the same way you would treat an SSH private key or a password manager database.

    Public keys, on the other hand, are designed to be shared.

    They’re useless on their own.


    Writing the Configuration Manually

    This is where WireGuard starts to make sense.

    Open a new configuration file.

    nano wg0.conf

    A basic configuration consists of two sections:

    • [Interface]
    • [Peer]

    The Interface section describes the WireGuard server itself.

    The Peer section describes another trusted device.

    A minimal configuration looks something like this:

    [Interface]
    Address = 10.100.100.1/24
    ListenPort = 51820
    PrivateKey = <server private key>
    
    [Peer]
    PublicKey = <client public key>
    AllowedIPs = 10.100.100.2/32

    At first glance it doesn’t look like much.

    That’s one of WireGuard’s greatest strengths.

    Compared to many VPN technologies, the configuration is remarkably small.

    The trick is understanding what each line actually means.


    Breaking Down the Interface Section

    Let’s look at each option individually.

    Address

    Address = 10.100.100.1/24

    This is not your server’s LAN address.

    Instead, it’s the IP address WireGuard assigns to its virtual VPN interface.

    Think of it as creating an entirely new network that exists solely for VPN traffic.

    Your physical network might be:

    192.168.1.0/24

    while your VPN network becomes:

    10.100.100.0/24

    Keeping these separate avoids routing conflicts and makes troubleshooting much easier.


    ListenPort

    ListenPort = 51820

    This tells WireGuard which UDP port to listen on.

    Port 51820 has become the de facto standard for WireGuard deployments, although you’re free to use another port if required.

    Remember that your firewall and router must also allow traffic to whichever port you choose.


    PrivateKey

    PrivateKey = ...

    This is the server’s identity.

    It should never leave the server.

    If this key is compromised, generate a new one and redistribute the corresponding public key to every client.


    Understanding the Peer Section

    Every device connecting to your VPN gets its own Peer block.

    Initially, you’ll only have one.

    Later you might have:

    • Phone
    • Laptop
    • Tablet
    • Work PC
    • Another server

    Each receives its own unique configuration.


    PublicKey

    The public key identifies the remote device.

    Unlike the private key, this value is intended to be shared.

    The server uses it to verify that the connecting device is trusted.


    AllowedIPs

    This single option causes more confusion than almost anything else in WireGuard.

    At first glance it appears to be a firewall rule.

    It isn’t.

    Nor is it an access control list.

    Instead, it tells WireGuard which IP addresses belong to that peer.

    For a single phone you might use:

    AllowedIPs = 10.100.100.2/32

    That simply means:

    “Packets destined for 10.100.100.2 should be sent to this peer.”

    Nothing more.

    Nothing less.


    🔍 Under the Hood

    One of the clever aspects of WireGuard is that AllowedIPs serves two purposes simultaneously.

    It acts as both:

    • A routing table
    • A peer identification mechanism

    When a packet needs to reach a destination, WireGuard checks which peer “owns” that address and automatically encrypts the traffic for that device.

    That’s why overlapping AllowedIPs between peers is generally a bad idea—it creates ambiguity about where packets should be sent.


    Routing Traffic Beyond the Tunnel

    At this point, the server and client could establish an encrypted tunnel.

    But there would still be a problem.

    The VPN network would exist in isolation.

    Your phone could reach the WireGuard server itself…

    …but nothing beyond it.

    If you wanted to access your NAS, your printer, another server, or even browse the Internet through your home connection, Linux needs to forward packets between networks.

    That’s where IP forwarding and Network Address Translation (NAT) enter the picture.

    WireGuard itself doesn’t magically route packets.

    Linux does.

    WireGuard simply encrypts them.

    We’ll configure routing and NAT shortly, but it’s important to understand that they’re separate technologies working together.


    ⚠️ Mistake I Made

    One of the most misleading moments during my build happened when everything appeared to be working.

    The VPN connected.

    The handshake completed successfully.

    I could even ping the WireGuard server’s VPN address.

    I assumed the installation was finished.

    It wasn’t.

    Nothing on my LAN was reachable.

    Internet traffic didn’t work.

    The VPN tunnel itself was healthy—the operating system simply wasn’t forwarding packets between the VPN network and the physical network.

    That distinction is incredibly important.

    A successful handshake only proves that two devices can establish an encrypted tunnel.

    It says nothing about whether traffic can actually travel anywhere afterwards.


    Up Next

    Now that we have a working WireGuard configuration, it’s time to containerise it.

    In the next section we’ll build the Docker Compose file, explain every directive it contains, discuss why network_mode: host makes sense for WireGuard, and finally launch the VPN server for the first time.

    Running WireGuard in Docker

    At this point we have everything needed to create a WireGuard server.

    We have:

    • A prepared Debian installation
    • Docker running correctly
    • Our cryptographic keys
    • A manually written WireGuard configuration

    The only thing left is to bring it all together.

    Although running WireGuard natively is perfectly valid, I wanted the VPN server to become just one part of a larger self-hosted environment. Docker makes that easy by isolating applications from one another while keeping deployments reproducible and easy to back up.

    Let’s build the container.


    Why Docker Makes Sense Here

    Docker isn’t just about making software easier to install.

    For infrastructure services like WireGuard it also provides several operational advantages.

    • Configuration is stored in one location.
    • Upgrades are usually as simple as pulling a newer image.
    • Backups become much easier. (again, how to back up docker containers)
    • Rebuilding the server takes minutes instead of hours.
    • Additional services can coexist without conflicting with one another.

    As my homelab grows, I know this server will eventually host several other services.

    Having each one live inside its own container keeps everything organised.


    💡 Why this matters

    Docker containers are not virtual machines.

    They all share the host’s Linux kernel.

    That means networking, routing, firewall rules and kernel modules still belong to Linux itself.

    WireGuard doesn’t stop needing Linux networking simply because it’s running inside a container.

    Understanding that distinction makes troubleshooting far easier later.


    Building the Docker Compose File

    Docker Compose allows us to describe an application in a simple YAML file.

    Instead of typing a long docker run command every time, we define everything once.

    Create a new file called:

    docker-compose.yml

    A basic configuration looks similar to this:

    services:
      wireguard:
        image: lscr.io/linuxserver/wireguard:latest
        container_name: wireguard
    
        cap_add:
          - NET_ADMIN
          - SYS_MODULE
    
        environment:
          - PUID=1000
          - PGID=1000
          - TZ=UTC
    
        volumes:
          - ./config:/config
          - /lib/modules:/lib/modules
    
        network_mode: host
    
        restart: unless-stopped

    If you’ve never looked closely at a Compose file before, some of those options probably seem a little mysterious.

    Let’s break them down.


    Choosing the Docker Image

    image: lscr.io/linuxserver/wireguard:latest

    The LinuxServer.io images are well maintained, thoroughly documented and widely used throughout the self-hosting community.

    Could you build your own image?

    Absolutely.

    For most deployments, though, there’s little reason to reinvent the wheel.

    Using a reputable image maintained by an active community strikes a good balance between convenience and transparency.


    Container Name

    container_name: wireguard

    This simply gives the container a friendly name.

    Without it, Docker generates random names that are much harder to remember.

    It makes administration a little cleaner.


    Linux Capabilities

    One of the most important sections is:

    cap_add:
      - NET_ADMIN
      - SYS_MODULE

    Containers normally operate with very limited permissions.

    That’s usually a good thing.

    WireGuard, however, needs permission to:

    • Create network interfaces
    • Manipulate routing tables
    • Configure firewall rules
    • Load kernel modules if required

    Those capabilities allow the container to perform exactly those tasks.

    Nothing more.


    🔍 Under the Hood

    Linux doesn’t simply have two permission levels—root and non-root.

    It actually divides privileged operations into smaller pieces called capabilities.

    Rather than granting every possible privilege, Docker can grant only the capabilities an application genuinely needs.

    That’s considerably safer than giving a container unrestricted access to the host.


    User and Group IDs

    PUID=1000
    PGID=1000

    These values determine which Linux user owns files created inside the mounted configuration directory.

    Using your normal user’s UID and GID prevents annoying permission problems later when editing configuration files directly from the host.

    If your user doesn’t happen to use ID 1000, simply substitute the correct values.

    You can check them with:

    id

    Persisting Configuration

    Containers are designed to be disposable.

    If you delete one, everything inside it disappears.

    That’s why Docker volumes are so important.

    volumes:
      - ./config:/config

    This tells Docker to store WireGuard’s configuration outside the container.

    If the container is ever recreated, your configuration remains untouched.

    The second volume:

    /lib/modules:/lib/modules

    gives the container access to the host’s kernel modules, which WireGuard may require depending on your kernel and deployment.


    Host Networking

    This line often surprises people.

    network_mode: host

    Normally Docker creates its own private virtual networks.

    Containers communicate through Docker’s internal bridge, and Docker performs Network Address Translation behind the scenes.

    For many applications that’s ideal.

    WireGuard is different.

    WireGuard’s job is routing network traffic.

    Adding another layer of Docker NAT simply complicates things.

    Using host networking allows the container to interact directly with the host’s network stack.

    From WireGuard’s perspective, it’s almost as though it were installed natively.

    That means:

    • Fewer moving parts
    • Simpler routing
    • Simpler firewall rules
    • Better performance
    • Easier troubleshooting

    For a VPN server, it’s generally the right choice.


    ⚠️ Mistake I Made

    One of the first rabbit holes I disappeared into involved Docker sysctls.

    Initially I tried enabling certain kernel networking options directly from inside the container.

    Docker refused.

    It turns out that containers using host networking cannot modify many host-level kernel parameters.

    The fix wasn’t inside Docker at all.

    Those settings belong on the Linux host itself.

    It was an important reminder that Docker doesn’t replace the operating system—it sits on top of it.


    Restart Policies

    The final line is small but important.

    restart: unless-stopped

    This tells Docker to automatically restart the container if:

    • The server reboots
    • Docker restarts
    • The container crashes unexpectedly

    Unless you explicitly stop it yourself, WireGuard should always come back online automatically.

    For infrastructure services, that’s exactly what you want.


    Starting the Container

    Once everything has been saved, starting WireGuard is refreshingly simple.

    docker compose up -d

    Docker will:

    • Download the image if necessary.
    • Create the container.
    • Mount the configuration directory.
    • Start WireGuard.

    Within a few moments, your VPN server should be running.


    Checking the Logs

    Before celebrating, always check the logs.

    docker logs wireguard

    Ideally you’ll see WireGuard initialise successfully without errors.

    If something has gone wrong, the logs are almost always the best place to begin troubleshooting.

    Learning to read application logs is one of the most valuable Linux administration skills you can develop.

    They usually tell you exactly what happened.

    You simply need to know where to look.


    💡 Geek.Click Tip

    Avoid the temptation to keep deleting and recreating containers every time something doesn’t work.

    Containers are disposable.

    Your configuration isn’t.

    If WireGuard refuses to start, investigate why before reaching for docker compose down.

    Nine times out of ten, the logs will point you towards the actual problem far faster than repeatedly rebuilding the container.


    Up Next

    At this point, WireGuard is running.

    But if you try connecting a client now, you’ll quickly discover that a VPN tunnel alone isn’t enough.

    In the next section we’ll configure Linux networking itself by enabling IP forwarding, adding the required NAT rules, and understanding how packets move between the VPN network, your LAN and the Internet.

    This is where everything finally comes together.

    Configuring Linux Routing

    If you’ve followed along so far, your WireGuard container should now be running happily.

    Unfortunately, that doesn’t mean your VPN is ready to use.

    This is one of the biggest misconceptions people have when deploying VPNs.

    A VPN tunnel simply creates an encrypted connection between two devices.

    It doesn’t automatically allow traffic to move between different networks.

    That’s the operating system’s job.

    Linux needs to know that it’s allowed to forward packets arriving from the VPN interface to your LAN, and vice versa.

    Without that, your phone might successfully connect to the VPN but be completely unable to reach anything else.


    Understanding Packet Flow

    Before changing any configuration, it’s worth understanding exactly what happens when you connect.

    Imagine you’re away from home and want to access your NAS.

    The packet follows a path similar to this:

    Phone
       │
    Encrypted WireGuard tunnel
       │
    Internet
       │
    Router / Firewall
       │
    WireGuard Server
       │
    Linux decrypts packet
       │
    Linux routes packet
       │
    NAS

    Notice something important.

    WireGuard’s job ends after decrypting the packet.

    Linux then decides where that packet should go next.

    If Linux isn’t configured to forward traffic between interfaces, the packet simply stops there.


    💡 Why this matters

    This distinction explains why many VPNs appear to “connect” successfully while nothing actually works.

    The encrypted tunnel exists.

    Authentication succeeded.

    The handshake completed.

    But routing never happens.

    When troubleshooting, always remember:

    Encryption and routing are two completely separate problems.


    Enabling IP Forwarding

    By default, Linux behaves like an endpoint.

    It receives traffic destined for itself and ignores everything else.

    Routers work differently.

    They receive packets destined for other networks and forward them accordingly.

    Since we’re turning our Debian server into a router between the VPN network and our LAN, Linux needs to be told to enable forwarding.

    Create a dedicated sysctl configuration file.

    echo "net.ipv4.ip_forward=1" | sudo tee /etc/sysctl.d/99-wireguard.conf

    Apply the change immediately.

    sudo sysctl --system

    Then verify it.

    sysctl net.ipv4.ip_forward

    The output should read:

    net.ipv4.ip_forward = 1

    If it doesn’t, don’t continue until you’ve identified why.


    🔍 Under the Hood

    Think of IP forwarding as enabling a routing engine inside Linux.

    Without it, Linux behaves like your laptop.

    With it enabled, Linux starts behaving like a router.

    WireGuard depends on this because VPN clients are connecting to an entirely different subnet.


    Network Address Translation (NAT)

    There’s still one more piece missing.

    Imagine your phone connects with the VPN address:

    10.100.100.2

    Your NAS lives on:

    192.168.1.50

    When the NAS replies, it has no idea where the 10.100.100.0/24 network is.

    It only knows about the local LAN.

    That’s where NAT comes in.

    Instead of forwarding packets with their original VPN address, Linux temporarily rewrites them so they appear to originate from the WireGuard server itself.

    To every device on your LAN, the traffic simply appears to come from your Debian server.

    Replies naturally find their way back, and Linux translates everything back before sending it through the VPN tunnel.

    The process is completely transparent.


    Understanding PostUp and PostDown

    Earlier we created a WireGuard configuration.

    You may have noticed two options we haven’t discussed yet.

    PostUp
    PostDown

    These commands execute automatically whenever the WireGuard interface starts or stops.

    They’re commonly used to:

    • Add firewall rules
    • Enable NAT
    • Configure routing
    • Remove those rules again when the interface shuts down

    It’s an elegant way of ensuring the operating system always matches the VPN’s current state.

    Rather than manually configuring firewall rules every time WireGuard starts, the interface does it for you.


    ⚠️ Mistake I Made

    One of the strangest issues I encountered came from something incredibly simple.

    I accidentally split a long PostUp command over multiple lines.

    WireGuard didn’t interpret it the way I expected.

    The interface refused to start correctly, and the resulting error messages weren’t especially helpful.

    The fix was simply keeping the command on a single line.

    Sometimes the smallest formatting mistake causes the biggest headaches.


    Firewall Configuration

    Your VPN server now knows how to route traffic.

    Unfortunately, your firewall probably doesn’t.

    If your server sits behind a firewall, you’ll need to allow incoming UDP traffic to your chosen WireGuard port.

    For most installations that’s:

    UDP 51820

    How you accomplish this depends entirely on your firewall.

    Whether you’re using:

    • OPNsense
    • pfSense
    • FortiGate
    • UniFi
    • MikroTik
    • OpenWrt
    • A consumer ISP router

    the principle remains exactly the same.

    Incoming UDP traffic must be forwarded to your WireGuard server.


    Double NAT

    Many home users unknowingly have two routers.

    For example:

    Internet
         │
    ISP Router
         │
    Your Firewall, or a WiFi Router
         │
    WireGuard Server

    This is known as double NAT.

    In that situation, forwarding the port on only one device isn’t enough.

    The ISP router must forward the traffic to your firewall.

    The firewall must then forward it to the WireGuard server.

    Miss either step and the VPN will never receive the packets.

    This catches a surprising number of people because everything inside the network appears to be configured correctly.

    The traffic simply never arrives.


    💡 Geek.Click Tip

    When debugging port forwarding problems, always work backwards.

    Ask yourself:

    • Does the server see the packet?
    • Does the firewall see the packet?
    • Does the ISP router see the packet?

    Eventually you’ll discover where the traffic disappears.

    Networking problems are often solved by following the packet rather than guessing.


    Configuring the Client

    Creating the client configuration is straightforward.

    Each client receives:

    • Its own private key
    • The server’s public key
    • A VPN IP address
    • The server endpoint
    • An AllowedIPs definition

    One option deserves special attention.

    AllowedIPs

    On the client, this setting means something completely different than it did on the server.

    It tells the client which traffic should enter the VPN.

    For example:

    0.0.0.0/0

    means:

    Send absolutely everything through the VPN.

    Whereas:

    192.168.1.0/24

    means:

    Only send traffic destined for my home network through the tunnel.

    Everything else uses the phone’s normal Internet connection.


    Split Tunnel vs Full Tunnel

    Neither approach is universally better.

    It depends entirely on your goals.

    A split tunnel is ideal if you simply want to reach your home devices.

    Internet browsing continues using your local Wi-Fi or mobile connection.

    A full tunnel routes all traffic through your home network.

    This is useful when:

    • Using public Wi-Fi
    • Accessing geo-restricted services
    • Protecting traffic on untrusted networks
    • Appearing online from your home IP address

    Personally, I like having both configurations available depending on where I’m connecting from.


    Testing the Installation

    One of the biggest lessons from this project was learning not to jump straight to:

    “Can I browse the Internet?”

    Instead, test each layer individually.

    1. Does the WireGuard handshake complete?
    2. Can you reach the VPN interface?
    3. Can you reach another LAN device?
    4. Can you reach the Internet?
    5. Does your public IP appear correctly?

    Each successful step eliminates an entire category of possible problems.

    Good troubleshooting is really just a process of elimination.


    Lessons Learned

    Every project teaches you something.

    This one taught me quite a lot.

    Some of the biggest lessons were:

    • A successful handshake doesn’t mean routing works.
    • Docker doesn’t replace Linux networking.
    • IP forwarding is essential.
    • Understanding packet flow makes troubleshooting dramatically easier.
    • DNS issues often masquerade as networking problems.
    • Following packets is far more effective than randomly changing settings.
    • Reading logs usually saves more time than reinstalling software.

    Perhaps the biggest lesson of all was that spending time understanding why something works is never wasted.

    Even if I eventually decide to deploy a management interface like wg-easy, I’ll know exactly what it’s doing behind the scenes.

    That knowledge is far more valuable than memorising a list of commands.


    Where to Go From Here

    A working WireGuard server opens the door to a surprising number of possibilities.

    Some ideas for future projects include:

    • Adding multiple peers for family members or colleagues.
    • Using Dynamic DNS so changing public IP addresses are handled automatically.
    • Integrating AdGuard Home for network-wide DNS filtering, even while travelling.
    • Running WireGuard behind OPNsense or another dedicated firewall.
    • Creating site-to-site tunnels between multiple locations.
    • Connecting cloud servers securely back to your home network.
    • Building a complete self-hosted infrastructure around Docker.

    Many of these are projects I’ll be covering here on Geek.Click in future articles.


    Final Thoughts

    When I started this project, my goal was simply to build a VPN server.

    What I ended up building was a much deeper understanding of Linux networking.

    WireGuard itself turned out to be refreshingly simple.

    The real learning came from understanding everything around it—routing, NAT, Docker networking, firewalls, packet flow and troubleshooting.

    That’s one of the reasons I deliberately chose the manual route instead of hiding everything behind a web interface.

    The deployment took longer.

    I made mistakes.

    I spent hours chasing problems that turned out to have surprisingly simple solutions.

    But those lessons have already carried over into other projects, and they’ve made me far more confident when working with networking in general.

    If you’re building a homelab or looking to deepen your understanding of modern VPNs, I encourage you to take the same approach.

    Don’t just aim to get it working.

    Take the time to understand why it works.

    Your future self will thank you