wg-easy: WireGuard con interfaz web en Docker
Configura wg-easy con Docker Compose: puertos, NET_ADMIN, sysctls necesarios y códigos QR para conectar teléfonos a WireGuard sin editar peers por SSH.
Qué va a crear
wg-easy es WireGuard con una interfaz web que se ejecuta en un único contenedor de Docker. Administra la interfaz de WireGuard y añade una interfaz web para crear clientes. Cada cliente que cree obtiene un archivo de configuración y un código QR. Así, un teléfono se conecta a la VPN al apuntar la cámara a la pantalla.
El túnel utiliza WireGuard estándar. El módulo del kernel mueve los paquetes, por lo que el rendimiento es el mismo que en una configuración escrita manualmente. La ventaja es la gestión del ciclo de vida de los clientes: puede añadir, deshabilitar y eliminar peers sin editar un archivo de configuración mediante SSH. La desventaja es que pierde el control directo de ese archivo. Esto se explica en la configuración manual de WireGuard en un VPS.
Necesita un VPS KVM con una dirección IPv4 pública, Docker Engine con el plugin Compose y acceso como root. La virtualización de contenedores que comparte el kernel del host, como OpenVZ o LXC, normalmente no puede cargar el módulo de WireGuard. Por eso, el contenedor no podrá activar la interfaz.
La versión 15 trasladó la configuración fuera del entorno
La mayoría de las guías disponibles se escribieron para wg-easy 14. En esa versión, se definía WG_HOST con la dirección del servidor y PASSWORD_HASH con un hash bcrypt de la contraseña de administración, ambas como variables de entorno. La versión 15 es una reescritura. Las notas oficiales de migración indican claramente que v15 no usa las mismas variables de entorno que v14 y que la mayoría se trasladó al panel de administración de la interfaz web.
Por tanto, WG_HOST y PASSWORD_HASH ya no tienen ningún efecto. Si copia un archivo compose antiguo, el contenedor se inicia, ignora esas líneas y después le solicita crear una cuenta de administración en el navegador. No es un error. Es el nuevo flujo de configuración.
En julio de 2026, la etiqueta principal que debe fijar es 15. Fije la versión principal en lugar de usar latest, porque una actualización de versión principal cambia el formato de configuración en disco y no se puede revertir correctamente.
El archivo de compose
Cree un directorio para el stack y escriba en él el archivo oficial de compose. Es el archivo del proyecto original, sin cambios.
sudo mkdir -p /etc/docker/containers/wg-easy
sudo curl -o /etc/docker/containers/wg-easy/docker-compose.yml \
https://raw.githubusercontent.com/wg-easy/wg-easy/master/docker-compose.ymlEl contenido es el siguiente:
volumes:
etc_wireguard:
services:
wg-easy:
image: ghcr.io/wg-easy/wg-easy:15
container_name: wg-easy
networks:
wg:
ipv4_address: 10.42.42.42
ipv6_address: fdcc:ad94:bacf:61a3::2a
volumes:
- etc_wireguard:/etc/wireguard
- /lib/modules:/lib/modules:ro
ports:
- "51820:51820/udp"
- "51821:51821/tcp"
restart: unless-stopped
cap_add:
- NET_ADMIN
- SYS_MODULE
sysctls:
- net.ipv4.ip_forward=1
- net.ipv4.conf.all.src_valid_mark=1
- net.ipv6.conf.all.disable_ipv6=0
- net.ipv6.conf.all.forwarding=1
- net.ipv6.conf.default.forwarding=1
networks:
wg:
driver: bridge
enable_ipv6: true
ipam:
driver: default
config:
- subnet: 10.42.42.0/24
- subnet: fdcc:ad94:bacf:61a3::/64etc_wireguard es un volumen con nombre que contiene la clave del servidor y todos los clientes que cree. Haga una copia de seguridad de ese volumen. De lo contrario, una reconstrucción elimina todos sus peers. Si prefiere ver esos archivos en el sistema de archivos del host, sustitúyalo por un bind mount y lea la diferencia entre bind mounts y volúmenes con nombre antes de hacerlo, porque los permisos funcionan de otra manera.
Por qué necesita NET_ADMIN, SYS_MODULE y los sysctls
De forma predeterminada, un contenedor no puede modificar la pila de red, y cada una de estas líneas elimina un bloqueo concreto.
NET_ADMIN permite que el contenedor cree la interfaz wg0, le asigne una dirección y escriba rutas. Sin este permiso, el contenedor se inicia y después falla al activar la interfaz, porque ip link add wg0 type wireguard devuelve Operation not permitted.
SYS_MODULE, junto con el montaje de solo lectura de /lib/modules, permite que el contenedor cargue el módulo de kernel WireGuard si el host todavía no lo ha cargado. El módulo reside en el kernel del host, no dentro de la imagen, por lo que el directorio del host debe estar visible. En un kernel moderno, normalmente el módulo está integrado. Puede confirmarlo con sudo modprobe wireguard && echo ok en el host.
net.ipv4.ip_forward=1 hace que el kernel reenvíe los paquetes que no están dirigidos al propio equipo. Sin esta opción, un cliente se conecta, el handshake se completa y después todos los paquetes destinados a Internet se descartan, por lo que ping 1.1.1.1 agota el tiempo de espera mientras la VPN parece estar conectada.
net.ipv4.conf.all.src_valid_mark=1 es la opción que más sorprende. WireGuard marca sus propios paquetes salientes para que no se enruten de nuevo por el túnel. El filtrado estricto de ruta inversa detecta un paquete cuya dirección de origen no coincide con la ruta esperada y lo descarta. Este sysctl indica al kernel que acepte los paquetes marcados, lo que evita que un túnel completo se interrumpa a sí mismo.
Inícielo y cree la cuenta de administrador
cd /etc/docker/containers/wg-easy
sudo docker compose up -d
sudo docker compose logs -fUse docker compose up y docker compose down, no start ni stop. El proyecto upstream advierte que ejecutar start en un contenedor creado con una configuración diferente deja la red en un estado incoherente. Si quiere recuperar la pila después de un reinicio, restart: unless-stopped ya lo gestiona, y el comportamiento durante el arranque de los servicios de compose explica qué garantiza esa política y qué no.
La interfaz web escucha en TCP 51821. En la primera visita muestra una página de configuración donde se crea la cuenta de administrador y se confirma la dirección del host que usarán los clientes para acceder al servidor. Esa dirección termina en la línea Endpoint de la configuración de cada cliente, por lo que debe ser la IP pública o el nombre DNS del VPS. Si es incorrecta, el código QR que se entrega a un teléfono apunta a una dirección inaccesible y el handshake nunca se completa.
Hay otro aspecto relacionado con ese puerto: wg-easy 15 rechaza HTTP sin cifrar a menos que se establezca INSECURE=true. Es válido acceder mediante HTTPS con un certificado no confiable o terminar TLS en un reverse proxy situado delante. Acceder mediante http:// con la configuración predeterminada no es válido.
No publiques el puerto de la interfaz web en Internet
El archivo de Compose publica 51821 en todas las interfaces. Es una página de inicio de sesión para un equipo que puede enrutar tu tráfico y no debería estar abierta a todo Internet. Publicar un puerto en Docker escribe reglas en la cadena DOCKER, que se evalúa antes que ufw, por lo que una regla ufw deny no lo cierra. Conviene entender este problema por separado; por qué los puertos publicados por Docker ignoran ufw lo explica en detalle.
La solución sencilla es vincular la interfaz web a loopback y acceder mediante un túnel SSH:
ports:
- "51820:51820/udp"
- "127.0.0.1:51821:51821/tcp"
environment:
- INSECURE=trueDespués, desde tu portátil:
ssh -L 51821:127.0.0.1:51821 youruser@your.server.addressAbre http://127.0.0.1:51821 en el navegador de tu portátil. SSH cifra el tráfico, nadie más puede conectarse al puerto y INSECURE=true es seguro en este caso porque el tramo HTTP sin cifrar nunca sale de la interfaz loopback.
Abra el puerto UDP 51820 y compruebe ambos firewalls
WireGuard necesita que el puerto UDP 51820 sea accesible desde Internet. Docker lo publica, pero muchos proveedores colocan un firewall de red independiente delante del VPS que Docker no puede controlar. Abra el puerto en ambos lugares. Si administra el firewall del host con ufw, las reglas básicas de ufw para un VPS son una opción más breve que escribir las reglas de nftables manualmente.
Compruebe que el contenedor esté escuchando realmente:
sudo ss -ulnp | grep 51820Debería ver un socket UDP en estado de escucha. Si esa línea no aparece, el contenedor no ha levantado la interfaz y sudo docker compose logs wg-easy indicará el motivo.
Cree un cliente y escanéelo con un teléfono
En la interfaz, cree un cliente y asígnele un nombre que pueda reconocer más adelante, como el dispositivo al que pertenece. wg-easy asigna la siguiente dirección de túnel libre y genera el par de claves automáticamente. Cada fila de cliente incluye un código QR y un archivo .conf descargable.
Instale la aplicación oficial de WireGuard en el teléfono, elija añadir un túnel desde un código QR y apunte la cámara al código de la pantalla. El túnel aparece con el nombre que introdujo. Actívelo. La fila del cliente en la interfaz empezará a mostrar los contadores de transferencia y la hora del último handshake. Cuando un teléfono está conectado al túnel, puede acceder a servicios que nunca se publicaron en Internet. Así, el teléfono puede seguir cargando fotos en un servidor de fotos autohospedado desde cualquier lugar sin que ese servidor tenga un solo puerto abierto al exterior. El mismo método sirve para los contenidos multimedia. Una biblioteca de Jellyfin reconstruida como un videoclub de los años 90 resulta cómoda de explorar desde la habitación de un hotel y permanece tan privada como en la LAN. Las alertas funcionan en sentido contrario dentro del mismo túnel. Un servidor ntfy autohospedado puede enviar un mensaje al teléfono en cuanto falle una tarea de copia de seguridad, sin responder nunca a una solicitud de Internet pública.
Un cliente que no muestra ningún handshake después de activarlo no está llegando al servidor. El problema suele estar en UDP 51820, ya sea en el firewall del proveedor o en la dirección del endpoint incluida en la configuración. Un cliente que muestra un handshake, pero no tiene acceso funcional a Internet, suele indicar un problema de forwarding o de DNS.
En un equipo de escritorio, descargue el archivo .conf e impórtelo en el cliente de WireGuard en lugar de volver a escribirlo. La clave privada de ese archivo se genera una sola vez y se muestra una sola vez. Proteja el archivo como protegería una clave privada de SSH.
Cuándo dejar atrás la interfaz web
wg-easy es la herramienta adecuada mientras tus peers sean personas y teléfonos. La interfaz web es más rápida que editar archivos de configuración, y revocar el acceso de un teléfono perdido requiere un solo clic.
Sus límites aparecen cuando necesitas algo que la interfaz web no puede modelar. El enrutamiento de sitio a sitio, donde el AllowedIPs de un peer cubre una subred remota completa en lugar de una sola dirección, suele ser el primer límite. Los túneles divididos con reglas de enrutamiento por peer o una configuración generada por tu herramienta de aprovisionamiento son los siguientes. En ese punto, la configuración manual no es más difícil; simplemente es diferente, y la guía de WireGuard sin interfaz muestra el mismo túnel creado a partir de wg0.conf. Si prefieres dejar de ejecutar el plano de control, WireGuard comparado con Tailscale explica la opción gestionada. Que sea un intercambio razonable depende de lo que el servidor de coordinación pueda alcanzar realmente, y conviene leer el modelo de confianza de Tailscale antes de entregarle acceso a tu red. El coste suele ser la siguiente cuestión, y qué cubre realmente el plan gratuito de Tailscale muestra que un hogar o un equipo pequeño puede no pagar nada. A partir de ese punto, la facturación cuenta usuarios en lugar de dispositivos. Es una estructura de costes distinta de la de un VPS que ya pagas, así que cuánto cuesta Tailscale cuando superas el plan gratuito es la cifra que debes comprobar antes de migrar un equipo. El túnel completo que acabas de crear tiene allí un equivalente directo, ya que anunciar el VPS como nodo de salida de Tailscale proporciona la misma ruta de salida a través del servidor. La aprobación se realiza en la consola de administración en lugar de escribirla en la configuración de cada cliente. La limitación de las subredes también tiene un equivalente, porque anunciar una red privada completa desde el VPS entrega esa red a todos los dispositivos del tailnet sin tener que editar el AllowedIPs de cada peer, que es lo que te llevó a dejar atrás la interfaz web. Si quieres ese panel y el enrutamiento automático de malla, pero no el servidor de coordinación de otra persona, ejecutar tu propio servidor NetBird en un VPS mantiene el plano de control en hardware que controlas. A cambio, tendrás que configurar el DNS y TLS, algo que wg-easy nunca te pidió.
Si la sintaxis de Compose anterior era la parte desconocida, y no WireGuard, conceptos básicos de Docker Compose en un VPS explica el formato del archivo y los comandos habituales.
FAQ
¿Por qué wg-easy ignora WG_HOST y PASSWORD_HASH?
Esas variables pertenecen a wg-easy 14. La versión 15 es una reescritura y el proyecto trasladó casi toda la configuración al panel de administración de la interfaz web. El contenedor no lee ninguna de las dos variables, por lo que se inicia con normalidad y después solicita crear una cuenta de administrador en la primera visita. Establezca la dirección del host que usarán los clientes en esa página de configuración.
¿Necesito SYS_MODULE si mi kernel ya tiene WireGuard?
No. SYS_MODULE y el montaje de /lib/modules existen para que el contenedor pueda cargar el módulo cuando el host no lo tiene. En un host donde sudo modprobe wireguard ya se ejecuta correctamente, esta capacidad no se utiliza. Eliminarla es una medida de refuerzo razonable, y NET_ADMIN sigue siendo necesario en cualquier caso.
El cliente se conecta, pero no hay Internet. ¿Cuál es el problema?
Un handshake sin tráfico casi siempre indica un problema de reenvío. Confirme que net.ipv4.ip_forward=1 y net.ipv4.conf.all.src_valid_mark=1 sigan en el archivo compose, porque una copia editada manualmente suele perderlos. Si el reenvío está activo, compruebe el servidor DNS que recibió el cliente. Un túnel que envía todo el tráfico a través de la VPN, pero apunta a un servidor DNS que ya no puede alcanzar, parece exactamente una conexión inactiva en el navegador.
¿Cómo hago una copia de seguridad de mis clientes?
Todo se almacena en el volumen con nombre etc_wireguard, en un archivo wg0.json. La interfaz también tiene un botón de copia de seguridad que exporta los mismos datos. Copie ese archivo fuera del servidor antes de cualquier actualización. La restauración se realiza mediante una carga durante el paso de configuración de un contenedor nuevo.
¿Puedo ejecutar wg-easy detrás de un reverse proxy?
Sí. Coloque el proxy delante de TCP 51821, termine TLS allí y establezca INSECURE=true en el contenedor para que acepte el salto HTTP sin cifrar desde el proxy. Mantenga UDP 51820 publicado directamente, porque el tráfico de la VPN usa UDP y no pasa por un proxy HTTP.