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.
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.
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.





