Run WireGuard VPN Server in Docker Container with Docker Compose

Greetings and salutations. Our guide today aims to show you how to Run WireGuard VPN Server in Docker Container using Docker Compose. We have done other articles in our blog on WireGuard which I will list towards the end of the article. The main is to help you broaden your knowledge of WireGuard VPN.

WireGuard is a modern VPN that uses a cryptography mechanism to secure communication between a client and server machine. It is very fast and very secure compared to other VPN solutions. It is designed to be lean, lightweight yet employs high performance. It runs very effectively on embedded interfaces and supercomputers. WireGuard developers had Linux Kernel in mind to target Linux users but it has since become cross-platform running across Windows, macOS, BSD, iOS, and Android. It is the preferred VPN solution because it is widely deployable.

šŸ† BESTSELLER in Ubuntu eBooks

Best Selling Ultimate Ubuntu Desktop Handbook

Master Ubuntu like a pro - from beautiful desktop customization to powerful terminal automation. This eBook is perfect for developers, system admins, and power users who want total control of their Ubuntu Linux workspace.

Only $15 $30
Get Instant Access →

How does WireGuard VPN work?

WireGuard employs the same mechanism as SSH. In WireGuard a concept called Cryptokey Routing is employed. Here public keys are associated with a list of tunnel IP addresses that are to be allowed inside a tunnel. Each network interface has a private key while the peers have a public key. The peers authenticate each other by a public key. In the server, the peer/client will be able to send packets to the network interface with a source IP matching his corresponding list of allowed IPs.

In the server configuration, if a network interface wants to send a packet to a client, it will look at the packet destination IP and compares it to each peer’s list of allowed IPs to see which peer to send it to. When a client wants to send a packet to the server, it will encrypt the packets for a single peer with any destination IP address. Thus, when sending packets a list of allowed IPs is maintained to act as a routing table and when receiving packets, the list of allowed IPs behaves as an access control list. WireGuard uses UDP socket for sending and receiving encrypted packets.

Can I run WireGuard run in a container?

Absolutely, it is possible to run WireGuard in containers. What happens is packets will be sent and received using network namespaces on which the wireguard interface was created. WireGuard is created on the main network interface which accesses the internet then it is moved to the network namespace belonging to a Docker container as that container’s only interface. Hence the container is only able to access the network through the WireGuard tunnel. This article aims to do just this.

Let’s begin.

Run WireGuard VPN Server in a Container with Compose

I will begin the process of Docker and Docker-compose environments installation. Docker is an open-source platform for developing, testing, shipping, running, and deploying applications in containers. Docker-compose is a tool for building and running multi-container Docker applications. Docker-compose uses a YAML file to configure applications.

For this guide, I will use Ubuntu 24.04 and CentOS Stream 10.

Step 1: Update the system

Refresh the APT index and the RPM index.

### Debian Based ###
sudo apt update && sudo apt upgrade -y

### Redhat Based ###
sudo yum update -y

Remove old versions of Docker.

### Debian Based ###
for pkg in docker.io docker-doc docker-compose docker-compose-v2 podman-docker containerd runc; do sudo apt-get remove $pkg; done

### Redhat Based ###
sudo yum remove docker \
                  docker-client \
                  docker-client-latest \
                  docker-common \
                  docker-latest \
                  docker-latest-logrotate \
                  docker-logrotate \
                  docker-engine

Step 2: Install Docker & Docker compose

Let’s look at how we can install Docker on the major Linux distributions:

Ubuntu

Set up the repository:

# Add Docker's official GPG key:
sudo apt-get update
sudo apt-get install ca-certificates curl
sudo install -m 0755 -d /etc/apt/keyrings
sudo curl -fsSL https://download.docker.com/linux/ubuntu/gpg -o /etc/apt/keyrings/docker.asc
sudo chmod a+r /etc/apt/keyrings/docker.asc

# Add the repository to Apt sources:
echo \
  "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.asc] https://download.docker.com/linux/ubuntu \
  $(. /etc/os-release && echo "${UBUNTU_CODENAME:-$VERSION_CODENAME}") stable" | \
  sudo tee /etc/apt/sources.list.d/docker.list > /dev/null
sudo apt-get update

To install the latest version, run:

sudo apt-get install docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin

Debian

Set up the repository:

# Add Docker's official GPG key:
sudo apt-get update
sudo apt-get install ca-certificates curl
sudo install -m 0755 -d /etc/apt/keyrings
sudo curl -fsSL https://download.docker.com/linux/debian/gpg -o /etc/apt/keyrings/docker.asc
sudo chmod a+r /etc/apt/keyrings/docker.asc

# Add the repository to Apt sources:
echo \
  "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.asc] https://download.docker.com/linux/debian \
  $(. /etc/os-release && echo "$VERSION_CODENAME") stable" | \
  sudo tee /etc/apt/sources.list.d/docker.list > /dev/null
sudo apt-get update

To install the latest version:

sudo apt-get install docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin

RHEL-based Distros

Set up the repository:

sudo dnf -y install dnf-plugins-core
sudo dnf config-manager --add-repo https://download.docker.com/linux/rhel/docker-ce.repo

Install Docker packages:

sudo dnf install docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin

CentOS Stream

Set up the repository:

sudo dnf -y install dnf-plugins-core
sudo dnf config-manager --add-repo https://download.docker.com/linux/centos/docker-ce.repo

Install Docker packages:

sudo dnf install docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin

Fedora Linux

Set up the repository:

sudo dnf -y install dnf-plugins-core
sudo dnf-3 config-manager --add-repo https://download.docker.com/linux/fedora/docker-ce.repo

Install the Docker packages:

sudo dnf install docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin

Step 3: Start and Enable the Docker Engine

For Ubuntu and Debian-based distros, the docker engine start automatically but for the rest, it doesn’t, which is why this step is necessary.

This configures the Docker systemd service to start automatically when you boot your system. If you don’t want Docker to start automatically, useĀ sudo systemctl start dockerĀ instead.

sudo systemctl enable --now docker

Check the status of the docker daemon:

The Docker daemon always runs as theĀ rootĀ user, hence the commands must always be run with sudo. If want to run the docker commands sudoless, add your user to the docker group. The docker group should have already been created by the system if docker was installed via the package manager. When the Docker daemon starts, it creates a Unix socket accessible by members of theĀ dockerĀ group.

If docker was not installed via the system package manager, add the group as follows:

sudo groupadd docker

Then add your user to the group:

sudo usermod -aG docker $USER
newgrp docker

Now you are all set.

Confirm the docker and docker-compose version is installed.

$ docker compose version
Docker Compose version v2.39.1

Add local user to the docker group

sudo usermod -aG docker $USER
newgrp docker

Test you docker installation by running the hello-world container:

docker run hello-world

The output is as below.

$ docker run hello-world
Unable to find image 'hello-world:latest' locally
latest: Pulling from library/hello-world
2db29710123e: Pull complete 
Digest: sha256:10d7d58d5ebd2a652f4d93fdd86da8f265f5318c6a73cc5b6a9798ff6d2b2e67
Status: Downloaded newer image for hello-world:latest

Hello from Docker!
This message shows that your installation appears to be working correctly.

To generate this message, Docker took the following steps:
 1. The Docker client contacted the Docker daemon.
 2. The Docker daemon pulled the "hello-world" image from the Docker Hub.
    (amd64)
 3. The Docker daemon created a new container from that image which runs the
    executable that produces the output you are currently reading.
 4. The Docker daemon streamed that output to the Docker client, which sent it
    to your terminal.

To try something more ambitious, you can run an Ubuntu container with:
 $ docker run -it ubuntu bash

Share images, automate workflows, and more with a free Docker ID:
 https://hub.docker.com/

For more examples and ideas, visit:
 https://docs.docker.com/get-started/

Step 3: Create Docker Configuration for Wireguard VPN Server

Docker configuration file will help manage docker container with WireGuard.

Begin by making a directory /opt/wireguard-server.

sudo mkdir /opt/wireguard-server && cd /opt/wireguard-server

Create a docker-compose YAML configuration file inside the folder.

sudo vim docker-compose.yaml

Paste the following code in the YAML configuration file. This code is from linuxserver.io .Please visit the link to see what these guys are doing.They are doing a tremendous job in building and maintaining community images.

These configurations are for my Ubuntu server : 192.168.1.101. Issue the commands on your WireGuard server.

services:
  wireguard:
    image: linuxserver/wireguard
    container_name: wireguard
    cap_add:
      - NET_ADMIN
      - SYS_MODULE
    environment:
      - PUID=1000
      - PGID=1000
      - TZ=Africa/Nairobi #set correct timezone
      - SERVERPORT=51820 #optional
      - PEERS=1 #optional
      - PEERDNS=auto #optional
      - ALLOWEDIPS=0.0.0.0/0 #Peer addresses allowed
      - INTERNAL_SUBNET=10.13.13.0/24 #Subnet used in VPN tunnel
      - SERVERURL=192.168.1.101 #Wireguard VPN server address
    volumes:
      - /opt/wireguard-server/config:/config
      - /usr/src:/usr/src # location of kernel headers
      - /lib/modules:/lib/modules
    ports:
      - 51820:51820/udp
    sysctls:
      - net.ipv4.conf.all.src_valid_mark=1
    restart: always

Make sure you change the SERVER URL to match the public IP Address of your wireguard server.

Lets now start our Docker container.

$ docker compose up -d
[+] Running 10/10
 āœ” wireguard Pulled                                                                                                                                                                                 12.6s
   āœ” d5960bdef641 Pull complete                                                                                                                                                                      2.1s
   āœ” f6a4c3e338ed Pull complete                                                                                                                                                                      2.1s
   āœ” ea31c94376c4 Pull complete                                                                                                                                                                      2.1s
   āœ” ff491f4a747a Pull complete                                                                                                                                                                      4.0s
   āœ” 7dcfb82a88d7 Pull complete                                                                                                                                                                      4.0s
   āœ” 2afdd610027c Pull complete                                                                                                                                                                      4.3s
   āœ” c2d9d26244c3 Pull complete                                                                                                                                                                      5.2s
   āœ” a8c77d5e0082 Pull complete                                                                                                                                                                      8.0s
   āœ” 0b36f263f118 Pull complete                                                                                                                                                                      8.0s
[+] Running 2/2
 āœ” Network wireguard-server_default  Created                                                                                                                                                         0.0s
 āœ” Container wireguard               Started                                                                                                                                                         0.4s

Alternatively, start a container by this command:

docker compose start wireguard

To restart a docker compose container.

docker compose restart wireguard

To list docker containers:

$ docker compose ps
NAME        IMAGE                   COMMAND   SERVICE     CREATED          STATUS          PORTS
wireguard   linuxserver/wireguard   "/init"   wireguard   59 seconds ago   Up 59 seconds   0.0.0.0:51820->51820/udp, [::]:51820->51820/udp

To allow port 51820 through firewall:

### on Redhat Based ###
sudo firewall-cmd --permanent --add-port=51820/udp
sudo firewall-cmd --reload

### On Debian Based ###
sudo apt install ufw
sudo ufw allow 51820/udp

Check your WireGuard server status with wg command.

$ docker exec -it wireguard wg
interface: wg0
  public key: fp+ghcKBrtG0VTTHcu1kx385+YcVIJlSo6eDtnEZRFg=
  private key: (hidden)
  listening port: 51820

peer: yQro2idpJKAuEf0afwf6JRs+O9w/pDMWJzoriR+GMAk=
  preshared key: (hidden)
  allowed ips: 10.13.13.2/32

Notice that one peer is created. This is because we specified 1 peer in our YAML file.

Lets ls inside our folder to see the contents:

$ ls /opt/wireguard-server
config

A new folder called config has been created. If you change directory to the folder and list its contents, you will see the configuration file of the wireguard server.

$ ls /opt/wireguard-server/config
coredns  peer1  server  templates  wg_confs
$ ls /opt/wireguard-server/config/wg_confs
wg0.conf

See the configuration details for wg0.conf configuration file.

$ cat /opt/wireguard-server/config/wg_confs/wg0.conf
[Interface]
Address = 10.13.13.1
ListenPort = 51820
PrivateKey = GJAqfIxQhYeDw+K+TtD598e+2TiPb0SPd3A338eB0mM=
PostUp = iptables -A FORWARD -i %i -j ACCEPT; iptables -A FORWARD -o %i -j ACCEPT; iptables -t nat -A POSTROUTING -o eth+ -j MASQUERADE
PostDown = iptables -D FORWARD -i %i -j ACCEPT; iptables -D FORWARD -o %i -j ACCEPT; iptables -t nat -D POSTROUTING -o eth+ -j MASQUERADE

[Peer]
# peer1
PublicKey = yQro2idpJKAuEf0afwf6JRs+O9w/pDMWJzoriR+GMAk=
PresharedKey = ijNpdBphSyPmHJTjtlJV1LH6eE8IzweTQRLRtEQyssc=
AllowedIPs = 10.13.13.2/32

To see what the server contents:

$ ls  /opt/wireguard-server/config/server/
privatekey-server  publickey-server

Step 4: Connect a peer to the wireguard server

To connect a peer to the Wireguard server, do the following.

For this guide, i will use my CentOS Stream 10 as my peer.

1. List the contents in peer1 directory.
$ ls /opt/wireguard-server/config/peer1/
peer1.conf  peer1.png  presharedkey-peer1  privatekey-peer1  publickey-peer1

We will use peer1.conf and distribute it to the peers/clients.This is our wireguard configuration file. The peer1.conf has both the private key and the public key.

$ cat /opt/wireguard-server/config/peer1/peer1.conf
[Interface]
Address = 10.13.13.2
PrivateKey = SALMtLb3wZBZk561tV5v9G2MK6Zq0ycFyg+W/2G4yFI=
ListenPort = 51820
DNS = 10.13.13.1

[Peer]
PublicKey = fp+ghcKBrtG0VTTHcu1kx385+YcVIJlSo6eDtnEZRFg=
PresharedKey = ijNpdBphSyPmHJTjtlJV1LH6eE8IzweTQRLRtEQyssc=
Endpoint = 192.168.1.101:51820
AllowedIPs = 0.0.0.0/0
2. Install WireGuard client on CentOS Stream 10.

The installation details can be found on the official WireGuard installation guides.

Install wireguard tools and make you sure you accept the GPG KEYS prompt by a “Y“.

sudo yum -y install wireguard-tools

3 – Once the WireGuard client is installed, copy the peer1.conf from the VPN server to your peer (client) i.e in my case CentOS Stream 8.

$ scp /opt/wireguard-server/config/peer1/peer1.conf username@serverIP:~/peer1.conf
The authenticity of host '192.168.1.163 (192.168.1.163)' can't be established.
ED25519 key fingerprint is SHA256:Nf3JHYt8VWlL+neNzivRXZ+Y+7YrLqomwoUrYxSwBDY.
This key is not known by any other names.
Are you sure you want to continue connecting (yes/no/[fingerprint])? yes
Warning: Permanently added '192.168.1.163' (ED25519) to the list of known hosts.
[email protected]'s password:
peer1.conf                                                                                                                                                              100%  306   965.2KB/s   00:00

You can as well create new file and paste contents copied from server /opt/wireguard-server/config/peer1/peer1.conf path:

vim ~/peer1.conf

Confirm contents and ensure Endpoint address points to server IP address where WireGuard is installed and running:

Endpoint = 192.168.1.101:51820 #192.168.201.13 is my WireGuard server address

Next move your file to wg0.conf file.

sudo mv peer1.conf /etc/wireguard/wg0.conf

This are the contents of my configuration file on client machine:

[Interface]
Address = 10.13.13.2
PrivateKey = SALMtLb3wZBZk561tV5v9G2MK6Zq0ycFyg+W/2G4yFI=
ListenPort = 51820
DNS = 10.13.13.1

[Peer]
PublicKey = fp+ghcKBrtG0VTTHcu1kx385+YcVIJlSo6eDtnEZRFg=
PresharedKey = ijNpdBphSyPmHJTjtlJV1LH6eE8IzweTQRLRtEQyssc=
Endpoint = 192.168.1.101:51820
AllowedIPs = 0.0.0.0/0
Enable the service to start at system boot:
$ sudo systemctl enable wg-quick@wg0
Created symlink /etc/systemd/system/multi-user.target.wants/[email protected] → /usr/lib/systemd/system/[email protected].

Then relabel the SELinux type on the WireGuard config to avoid permission issues:

# Fix SELinux context to system configuration type
sudo restorecon -v /etc/wireguard/wg0.conf

Then install systemd-resolved required by wg-quick to update DNS on your CentOS system.

sudo dnf install -y systemd-resolved
sudo systemctl enable --now systemd-resolved

Reboot your system after enabling the service:

sudo reboot

Wait for it to start then check service status:

$ systemctl status wg-quick@wg0
ā— [email protected] - WireGuard via wg-quick(8) for wg0
     Loaded: loaded (/usr/lib/systemd/system/[email protected]; enabled; preset: disabled)
     Active: active (exited) since Mon 2025-11-03 12:56:50 EAT; 13s ago
 Invocation: fe183b22dcdd4bde8b24d0ae1fc35119
       Docs: man:wg-quick(8)
             man:wg(8)
             https://www.wireguard.com/
             https://www.wireguard.com/quickstart/
             https://git.zx2c4.com/wireguard-tools/about/src/man/wg-quick.8
             https://git.zx2c4.com/wireguard-tools/about/src/man/wg.8
    Process: 2595 ExecStart=/usr/bin/wg-quick up wg0 (code=exited, status=0/SUCCESS)
   Main PID: 2595 (code=exited, status=0/SUCCESS)
   Mem peak: 3M
        CPU: 49ms

Nov 03 12:56:50 centos-10.cloudlabske.io wg-quick[2595]: [#] ip -4 address add 10.13.13.2 dev wg0
Nov 03 12:56:50 centos-10.cloudlabske.io wg-quick[2595]: [#] ip link set mtu 1420 up dev wg0
Nov 03 12:56:50 centos-10.cloudlabske.io wg-quick[2617]: [#] resolvconf -a wg0 -m 0 -x
Nov 03 12:56:50 centos-10.cloudlabske.io wg-quick[2595]: [#] wg set wg0 fwmark 51820
Nov 03 12:56:50 centos-10.cloudlabske.io wg-quick[2595]: [#] ip -4 rule add not fwmark 51820 table 51820
Nov 03 12:56:50 centos-10.cloudlabske.io wg-quick[2595]: [#] ip -4 rule add table main suppress_prefixlength 0
Nov 03 12:56:50 centos-10.cloudlabske.io wg-quick[2595]: [#] ip -4 route add 0.0.0.0/0 dev wg0 table 51820
Nov 03 12:56:50 centos-10.cloudlabske.io wg-quick[2595]: [#] sysctl -q net.ipv4.conf.all.src_valid_mark=1
Nov 03 12:56:50 centos-10.cloudlabske.io wg-quick[2595]: [#] nft -f /dev/fd/63
Nov 03 12:56:50 centos-10.cloudlabske.io systemd[1]: Finished [email protected] - WireGuard via wg-quick(8) for wg0.

If you check the network interfaces you’ll see wg0 with tunnel IP address assigned.

$ ip ad
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN group default qlen 1000
    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
    inet 127.0.0.1/8 scope host lo
       valid_lft forever preferred_lft forever
    inet6 ::1/128 scope host noprefixroute
       valid_lft forever preferred_lft forever
2: ens18: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc fq_codel state UP group default qlen 1000
    link/ether bc:24:11:88:55:cc brd ff:ff:ff:ff:ff:ff
    altname enp0s18
    altname enxbc24118855cc
    inet 192.168.1.163/24 brd 192.168.1.255 scope global dynamic noprefixroute ens18
       valid_lft 5945sec preferred_lft 5945sec
    inet6 fe80::be24:11ff:fe88:55cc/64 scope link noprefixroute
       valid_lft forever preferred_lft forever
4: wg0: <POINTOPOINT,NOARP,UP,LOWER_UP> mtu 1420 qdisc noqueue state UNKNOWN group default qlen 1000
    link/none
    inet 10.13.13.2/32 scope global wg0
       valid_lft forever preferred_lft forever

Try ping the server to test connectivity:

$ ping -c 4 10.13.13.1
PING 10.13.13.1 (10.13.13.1) 56(84) bytes of data.
64 bytes from 10.13.13.1: icmp_seq=1 ttl=64 time=0.344 ms
64 bytes from 10.13.13.1: icmp_seq=2 ttl=64 time=0.377 ms
64 bytes from 10.13.13.1: icmp_seq=3 ttl=64 time=0.430 ms
64 bytes from 10.13.13.1: icmp_seq=4 ttl=64 time=0.446 ms

--- 10.13.13.1 ping statistics ---
4 packets transmitted, 4 received, 0% packet loss, time 3081ms
rtt min/avg/max/mdev = 0.344/0.399/0.446/0.040 ms

On the server side, issue the command:

$ docker exec -it wireguard wg
interface: wg0
  public key: fp+ghcKBrtG0VTTHcu1kx385+YcVIJlSo6eDtnEZRFg=
  private key: (hidden)
  listening port: 51820

peer: yQro2idpJKAuEf0afwf6JRs+O9w/pDMWJzoriR+GMAk=
  preshared key: (hidden)
  endpoint: 192.168.1.163:51820
  allowed ips: 10.13.13.2/32
  latest handshake: 1 minute, 19 seconds ago
  transfer: 948 B received, 860 B sent
Allow more clients and server connection

When adding extra clients you’ll need to configure Wireguard on the server-side to allow new connection between the client and the server

# Edit on the server side
$ sudo vim /opt/wireguard-server/config/wg0.conf
# peer2
PublicKey = 7ANB0SuBUsnetjqHrL99YIhpbqetJ9yYy0CRsNiuzls=
PresharedKey = gbMDUgQM7levlYLcwhyf1E1dHF/PG489UGeeSHr7tro=
AllowedIPs = 10.13.13.3/32

Wrapping up

That is how you Run WireGuard VPN Server in Docker Container using Docker Compose. Enjoy using your VPN.

Join our Linux and open source community. Subscribe to our newsletter for tips, tricks, and collaboration opportunities!

Recent Post

Unlock the Right Solutions with Confidence

At CloudSpinx, we don’t just offer services - we deliver clarity, direction, and results. Whether you're navigating cloud adoption, scaling infrastructure, or solving DevOps challenges, our seasoned experts help you make smart, strategic decisions with total confidence. Let us turn complexity into opportunity and bring your vision to life.

Leave a Comment

Your email address will not be published. Required fields are marked *

Related Post

Microsoft SQL Server, one of the most powerful relational database management systems (RDBMS) in use today, is no longer tied […]

In building a virtual machine in OpenStack, one of the first procedures actually involves preparing a basic image of an […]

Thank you for keeping it techviewleo, we really appreciate your continued support, and thank you for making us your number […]

Let's Connect

Unleash the full potential of your business with CloudSpinx. Our expert solutions specialists are standing by to answer your questions and tailor a plan that perfectly aligns with your unique needs.
You will get a response from our solutions specialist within 12 hours
We understand emergencies can be stressful. For immediate assistance, chat with us now

Contact CloudSpinx today!

Download CloudSpinx Profile

Discover the full spectrum of our expertise and services by downloading our detailed Company Profile. Simply enter your first name, last name, and email address.