wg-easy en Docker: WireGuard con interfaz web
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é se va a configurar
wg-easy es WireGuard con una interfaz web que se ejecuta en un único contenedor Docker. Gestiona 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 con 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 contrapartida es que pierde el control directo sobre ese archivo. Este es el tema de la configuración manual de WireGuard en un VPS.
Necesita un VPS KVM con una dirección IPv4 pública, Docker Engine con el complemento Compose y acceso 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 que encontrará 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 administrador, ambos 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 trasladaron 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 pide crear una cuenta de administrador en el navegador. No es un error. Es el nuevo flujo de configuración.
En octubre 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 permite una reversión limpia. El cambio a 15 sustituyó el archivo JSON de v14 por una base de datos SQLite.
El archivo compose
Cree un directorio para el stack y escriba en él el archivo compose oficial. Es el archivo del proyecto upstream, con las líneas opcionales comentadas omitidas.
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. Estos datos se almacenan en la base de datos SQLite wg-easy.db. Haga una copia de seguridad de ese volumen. Si reconstruye el stack, perderá todos los peers. Si prefiere ver esos archivos en el sistema de archivos del host, sustitúyalo por un bind mount. Antes, lea la diferencia entre bind mounts y volúmenes con nombre, porque los permisos se comportan de forma diferente.
Por qué necesita NET_ADMIN, SYS_MODULE y los sysctl
Un contenedor no puede modificar la pila de red de forma predeterminada. 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 termina 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 de 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 eso el directorio del host debe estar visible. En un kernel moderno, el módulo suele estar 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 este ajuste, un cliente se conecta, el handshake se completa y después se descarta cada paquete destinado a Internet. Por eso ping 1.1.1.1 agota el tiempo de espera aunque la VPN parezca conectada.
net.ipv4.conf.all.src_valid_mark=1 es el ajuste que más suele sorprender. WireGuard marca sus propios paquetes salientes para que no se enruten de nuevo por el túnel. El filtrado estricto de la 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. Así se evita que un túnel completo se interrumpa a sí mismo.
Inícialo y crea la cuenta de administrador
cd /etc/docker/containers/wg-easy
sudo docker compose up -d
sudo docker compose logs -fUsa docker compose up y docker compose down, no start ni stop. start sólo reinicia los contenedores que ya existen. Por tanto, no aplica ningún cambio que hayas hecho en el archivo compose, incluidos los ajustes de red y sysctl. up vuelve a crear todo lo que haya cambiado. Si quieres que la pila vuelva a iniciarse después de un reinicio, restart: unless-stopped ya se encarga de ello. El comportamiento de arranque de los servicios compose explica qué garantiza y qué no garantiza esa política.
La interfaz web escucha en TCP 51821. La primera vez que accedes, muestra una página de configuración en la que creas la cuenta de administrador y confirmas la dirección del host que usarán los clientes para conectarse al servidor. Esa dirección del host aparece en la línea Endpoint de la configuración de cada cliente. Por tanto, debe ser la IP pública o el nombre DNS del VPS. Si es incorrecta, el código QR que entregues a un teléfono apuntará a una dirección inaccesible y el handshake nunca se completará.
Hay otro aspecto relacionado con ese puerto: wg-easy 15 rechaza HTTP sin cifrar a menos que establezcas INSECURE=true. Puedes acceder mediante HTTPS con un certificado no confiable o terminar TLS en un reverse proxy situado delante de wg-easy. También puedes acceder mediante http://. Lo que no funciona es hacerlo con la configuración predeterminada.
No publique el puerto de la interfaz web en Internet
El archivo compose publica 51821 en todas las interfaces. Esa es la página de inicio de sesión de un sistema que puede encaminar su tráfico, por lo que no debe quedar abierta a todo el mundo. Publicar un puerto en Docker escribe reglas en la cadena DOCKER, que se evalúa antes que ufw. Por eso, una regla de denegación de ufw 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 enlazar la interfaz web a loopback y acceder a ella mediante un túnel SSH:
ports:
- "51820:51820/udp"
- "127.0.0.1:51821:51821/tcp"
environment:
- INSECURE=trueDespués, desde su portátil:
ssh -L 51821:127.0.0.1:51821 youruser@your.server.addressAbra http://127.0.0.1:51821 en el navegador de su portátil. El tráfico está cifrado por SSH, ningún otro equipo puede acceder al puerto y INSECURE=true es seguro en este caso porque el salto HTTP sin cifrar nunca sale de la interfaz loopback.
Abrir UDP 51820 y comprobar ambos firewalls
WireGuard necesita que UDP 51820 sea accesible desde Internet. Docker publica el puerto, 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 no aparece nada en esa línea, el contenedor nunca activó la interfaz y sudo docker compose logs wg-easy indicará el motivo.
Cree un cliente y escanéelo en 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 libre del túnel y genera el par de claves automáticamente. Cada fila de cliente ofrece un código QR y un archivo .conf descargable.
Instale la aplicación oficial de WireGuard en el teléfono, seleccione la opción para añadir un túnel desde un código QR y apunte la cámara al código que aparece en 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 autoalojado 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, mientras permanece tan privada como en la LAN. Las alertas funcionan en la dirección contraria mediante el mismo túnel. Un servidor ntfy autoalojado puede enviar un mensaje a ese 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. Trate el archivo como trataría una clave privada de SSH.
Cuándo dejar atrás la interfaz web
wg-easy es la herramienta adecuada mientras sus peers sean personas y teléfonos. La interfaz web es más rápida que editar archivos de configuración, y revocar un teléfono perdido requiere un solo clic.
Llegará a sus límites cuando necesite algo que la interfaz web no modele. 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 obstáculo. Los túneles divididos con reglas de enrutamiento por peer o una configuración generada por su 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 prefiere dejar de ejecutar por completo el plano de control, WireGuard comparado con Tailscale explica la opción administrada. 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 su red. El coste suele ser la siguiente pregunta, y qué cubre realmente el plan gratuito de Tailscale muestra que un hogar o un equipo pequeño puede no pagar nada. Después de ese límite, la facturación cuenta usuarios en lugar de dispositivos. Es una estructura de costes distinta de la de un VPS que ya paga, así que cuánto cuesta Tailscale cuando supera el plan gratuito es la cifra que debe comprobar antes de migrar un equipo. El túnel completo que acaba de crear tiene allí un equivalente directo, porque anunciar el VPS como nodo de salida de Tailscale le proporciona la misma ruta de salida a través del servidor, aprobada en la consola de administración en lugar de escrita en la configuración de cada cliente. El límite de las subredes también tiene un equivalente, porque anunciar una red privada completa desde el VPS entrega esa red a todos los dispositivos de la tailnet sin tener que editar el AllowedIPs de cada peer, que fue lo que le hizo dejar atrás la interfaz web. Si quiere ese panel y el enrutamiento automático de la malla, pero no el servidor de coordinación de otra persona, ejecutar su propio servidor NetBird en un VPS mantiene el plano de control en hardware que posee, a cambio de tener que configurar DNS y TLS, algo que wg-easy nunca le exigió.
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 mis variables 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 normalmente 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 /lib/modules permiten que el contenedor cargue el módulo cuando el host no lo tiene. En un host donde sudo modprobe wireguard ya se ejecuta correctamente, esa capacidad no se utiliza. Eliminarla es una medida de endurecimiento 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, ya que una copia editada manualmente suele perderlos. Si el reenvío está habilitado, 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, se comporta en el navegador exactamente como una conexión inactiva.
¿Cómo hago una copia de seguridad de mis clientes?
Todo se almacena en el volumen con nombre etc_wireguard. En la versión 15, esto corresponde a una base de datos SQLite ubicada en /etc/wireguard/wg-easy.db dentro del contenedor. La documentación de v15 no describe ninguna función de copia de seguridad ni de exportación, así que haga una copia del propio volumen: ejecute docker compose down, copie el contenido del volumen a una ubicación externa al servidor y vuelva a iniciar el stack con docker compose up -d. Hágalo antes de cualquier actualización. El archivo wg0.json que mencionan las guías antiguas pertenece a wg-easy 14. Es el archivo que exporta el botón de copia de seguridad de v14 y, en v15, sólo se utiliza como el archivo que se carga en el asistente de configuración al migrar una instalación antigua.
¿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.