add wireguard as community container

Signed-off-by: Simon L. <szaimen@e.mail.de>
This commit is contained in:
Simon L.
2026-08-25 15:23:19 +02:00
parent 50636f2adc
commit d4e41ed4ed
8 changed files with 219 additions and 1 deletions
+74
View File
@@ -0,0 +1,74 @@
name: wg-easy-update
on:
workflow_dispatch:
schedule:
- cron: '00 12 * * *'
permissions:
contents: read
jobs:
run_update:
name: update wg-easy to the latest major release
runs-on: ubuntu-latest
permissions:
contents: write
pull-requests: write
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: Run wg-easy-update
run: |
set -euo pipefail
CONTAINER_FILE="./community-containers/wireguard/wireguard.json"
# The container pins the rolling major tag, so only major bumps need a PR
pinned="$(jq -r '.aio_services_v1[0].image_tag' "$CONTAINER_FILE")"
if ! [[ "$pinned" =~ ^[0-9]+$ ]]; then
echo "Expected a major tag like \"15\" but found \"$pinned\"!"
exit 1
fi
# Filter out pre-releases like v16.0.0-beta.1
latest="$(
git ls-remote --tags https://github.com/wg-easy/wg-easy \
| cut -d/ -f3 \
| grep -v '\^{}' \
| grep -E '^v[0-9]+\.[0-9]+\.[0-9]+$' \
| sed -E 's/^v([0-9]+)\..*/\1/' \
| sort -n \
| tail -1
)"
if [ -z "$latest" ]; then
echo "Could not get the latest wg-easy version!"
exit 1
fi
if [ "$latest" -le "$pinned" ]; then
echo "wg-easy is up to date at v$pinned"
exit 0
fi
sed -i "s|\"image_tag\": \"$pinned\",|\"image_tag\": \"$latest\",|" "$CONTAINER_FILE"
if [ "$(jq -r '.aio_services_v1[0].image_tag' "$CONTAINER_FILE")" != "$latest" ]; then
echo "Failed to update image_tag!"
exit 1
fi
- name: Create Pull Request
uses: peter-evans/create-pull-request@5f6978faf089d4d20b00c7766989d076bb2fc7f1 # v7
with:
token: ${{ secrets.GITHUB_TOKEN }}
commit-message: wg-easy-update automated change
signoff: true
title: wg-easy major update
body: |
Automated wg-easy community container update to a new major release.
:warning: Major releases may contain breaking changes. Please check the
[release notes](https://github.com/wg-easy/wg-easy/releases) and that the
`INIT_*` [setup variables](https://github.com/wg-easy/wg-easy/blob/master/docs/content/advanced/config/unattended-setup.md)
still work before merging.
labels: dependencies, 3. to review
milestone: next
branch: wg-easy-container-update
+1
View File
@@ -59,6 +59,7 @@ flowchart TD
LDAP(["👥 LLDAP\nLight LDAP / Users"]):::community
CADDY(["🌐 Caddy\nReverse Proxy + Geoblocking"]):::community
NPMPLUS(["🌐 NPMplus\nNginx Proxy Manager"]):::community
WG(["🔑 WireGuard Easy\nVPN server with web UI"]):::community
end
```
+22
View File
@@ -0,0 +1,22 @@
## WireGuard Easy
This container bundles [wg-easy](https://github.com/wg-easy/wg-easy), a WireGuard VPN server with a web UI for managing client connections, and auto-configures it for you.
### Notes
- ⚠️ This container opens a VPN entry point into your server's network. Only add it if you understand the implications and keep it updated!
- ⚠️ This container is only intended to be used on a server in a trusted home network. Do **not** add it on a public VPS or any other server whose IP address is directly reachable from the internet!
- ⚠️ The web UI on port `51821/tcp` is served via plain HTTP (`INSECURE=true`) and is published on **all** interfaces. So if you still run this on a server that is reachable from the internet, you need to block port `51821/tcp` with a firewall, as otherwise the admin UI is reachable by everyone and the credentials that you enter there are transmitted unencrypted.
- ⚠️ This container only works on Linux hosts with Docker installed natively. Docker Desktop (Windows, macOS and Linux) is **not** supported since the containers run inside a virtual machine there whose kernel does not provide the WireGuard module to the container.
- ⚠️ This container requires the `wireguard` kernel module on the host and fails to start without it. It ships as a loadable module (`CONFIG_WIREGUARD=m`) on all common distributions with kernel 5.6 or later, so you may need to load it once via `sudo modprobe wireguard` and make that persistent, e.g. via `echo wireguard | sudo tee /etc/modules-load.d/wireguard.conf`. If your kernel is older, you need to install the module on the host first.
- The admin account is created automatically on the first start. The username is `admin` and the password is the one that you can see next to the container in the AIO interface.
- After adding and starting the container, you can visit `http://ip.address.of.this.server:51821` in order to log in and create clients. Each client can be exported as a config file or QR code.
- Port `51820/udp` needs to be forwarded in your router to this server in order to be able to connect from the internet. Port `51821/tcp` (the web UI) must **not** be forwarded!
- The VPN host address that clients will connect to is set to your Nextcloud domain by default. You can change this in the web UI if you want to connect via a different address.
- This container may conflict with the fail2ban community container as both modify iptables rules. Watch out for unexpected behavior if you enable both.
- The configuration and clients of WireGuard will be automatically included in AIOs backup solution!
- See https://github.com/nextcloud/all-in-one/tree/main/community-containers#community-containers how to add it to the AIO stack
### Repository
https://github.com/wg-easy/wg-easy
### Maintainer
https://github.com/szaimen
@@ -0,0 +1,66 @@
{
"aio_services_v1": [
{
"container_name": "nextcloud-aio-wireguard",
"display_name": "WireGuard Easy",
"documentation": "https://github.com/nextcloud/all-in-one/tree/main/community-containers/wireguard",
"image": "ghcr.io/wg-easy/wg-easy",
"image_tag": "15",
"internal_port": "51821",
"init": false,
"restart": "unless-stopped",
"ports": [
{
"ip_binding": "",
"port_number": "51820",
"protocol": "udp"
},
{
"ip_binding": "",
"port_number": "51821",
"protocol": "tcp"
}
],
"cap_add": [
"NET_ADMIN",
"SYS_MODULE"
],
"sysctls": [
"net.ipv4.ip_forward=1",
"net.ipv4.conf.all.src_valid_mark=1",
"net.ipv6.conf.all.disable_ipv6=0",
"net.ipv6.conf.all.forwarding=1",
"net.ipv6.conf.default.forwarding=1"
],
"environment": [
"TZ=%TIMEZONE%",
"INSECURE=true",
"PORT=51821",
"INIT_ENABLED=true",
"INIT_USERNAME=admin",
"INIT_PASSWORD=%WIREGUARD_PASSWORD%",
"INIT_HOST=%NC_DOMAIN%",
"INIT_PORT=51820"
],
"volumes": [
{
"source": "nextcloud_aio_wireguard",
"destination": "/etc/wireguard",
"writeable": true
},
{
"source": "/lib/modules",
"destination": "/lib/modules",
"writeable": false
}
],
"backup_volumes": [
"nextcloud_aio_wireguard"
],
"ui_secret": "WIREGUARD_PASSWORD",
"secrets": [
"WIREGUARD_PASSWORD"
]
}
]
}
+8 -1
View File
@@ -38,6 +38,13 @@
"pattern": "^[A-Z_]+$"
}
},
"sysctls": {
"type": "array",
"items": {
"type": "string",
"pattern": "^net\\.(ipv4|ipv6|mptcp|unix)\\.[a-z0-9_.]+=[a-zA-Z0-9_.:/ -]+$"
}
},
"depends_on": {
"type": "array",
"items": {
@@ -220,7 +227,7 @@
},
"source": {
"type": "string",
"pattern": "^((nextcloud_aio_[a-z_]+)|(%[A-Z_]+%)|(/dev)|(/run/udev))$"
"pattern": "^((nextcloud_aio_[a-z_]+)|(%[A-Z_]+%)|(/dev)|(/run/udev)|(/lib/modules))$"
},
"writeable": {
"type": "boolean"
+2
View File
@@ -27,6 +27,8 @@ readonly class Container {
public bool $enableNvidiaGpu,
/** @var string[] */
public array $capAdd,
/** @var string[] */
public array $sysctls,
public int $shmSize,
public bool $apparmorUnconfined,
/** @var string[] */
+6
View File
@@ -308,6 +308,11 @@ readonly class ContainerDefinitionFetcher {
$capAdd = $entry['cap_add'];
}
$sysctls = [];
if (isset($entry['sysctls'])) {
$sysctls = $entry['sysctls'];
}
$shmSize = -1;
if (isset($entry['shm_size'])) {
$shmSize = $entry['shm_size'];
@@ -370,6 +375,7 @@ readonly class ContainerDefinitionFetcher {
$devices,
$enableNvidiaGpu,
$capAdd,
$sysctls,
$shmSize,
$apparmorUnconfined,
$backupVolumes,
+40
View File
@@ -229,6 +229,24 @@ readonly class DockerActionManager {
}
}
/**
* Whether the aio network is ipv6 enabled. If it is not, ipv6 sysctls must not
* be applied to containers since setting them fails on hosts whose kernel was
* booted with ipv6.disable=1.
*/
private function IsIpv6EnabledOnNetwork(string $network = 'nextcloud-aio'): bool {
$url = $this->BuildApiUrl(sprintf('networks/%s', urlencode($network)));
try {
$response = $this->sendHttpRequest('GET', $url);
} catch (RequestException $e) {
return false;
}
$responseBody = json_decode((string)$response->getBody(), true, 512, JSON_THROW_ON_ERROR);
return ($responseBody['EnableIPv6'] ?? false) === true;
}
public function CreateContainer(Container $container): void {
$volumes = [];
foreach ($container->volumes->GetVolumes() as $volume) {
@@ -401,6 +419,28 @@ readonly class DockerActionManager {
$requestBody['HostConfig']['CapAdd'] = $capAdds;
}
// Only namespaced sysctls can be set per container. The schema restricts
// these to net.ipv4/net.ipv6/net.mptcp/net.unix which live in the network
// namespace. Non-namespaced ones like vm.overcommit_memory (redis),
// vm.max_map_count (fulltextsearch) and net.core.rmem_max (apache) must
// still be set on the host. See the linked discussions in the docs.
$sysctls = [];
$isIpv6Enabled = count($container->sysctls) > 0 && $this->IsIpv6EnabledOnNetwork();
foreach ($container->sysctls as $sysctl) {
// Setting net.ipv6.* fails if the host kernel was booted with ipv6.disable=1
// and has no effect if the aio network itself is not ipv6 enabled.
if (!$isIpv6Enabled && str_starts_with($sysctl, 'net.ipv6.')) {
continue;
}
$sysctlParts = explode('=', $sysctl, 2);
if (count($sysctlParts) === 2) {
$sysctls[$sysctlParts[0]] = $sysctlParts[1];
}
}
if (count($sysctls) > 0) {
$requestBody['HostConfig']['Sysctls'] = $sysctls;
}
// Disable arp spoofing
if (!in_array('NET_RAW', $capAdds, true)) {
$requestBody['HostConfig']['CapDrop'] = ['NET_RAW'];