wg-easy: WireGuard com painel web no Docker
Configure o wg-easy no Docker Compose: portas, NET_ADMIN, sysctls necessários e QR codes para celulares, incluindo a mudança de configuração da versão 15.
O que você vai criar
wg-easy é o WireGuard com uma interface web, executado como um único contentor Docker. Gere a interface WireGuard por si e adiciona uma interface no navegador para criar clientes. Cada cliente criado recebe um ficheiro de configuração e um código QR. Assim, um telefone entra na VPN ao apontar a câmara para o ecrã.
O túnel usa o WireGuard normal. O módulo do kernel move os pacotes, pelo que o débito é igual ao de uma configuração escrita manualmente. O que ganha é a gestão do ciclo de vida dos clientes: pode adicionar, desativar e eliminar peers sem editar um ficheiro de configuração através de SSH. O que perde é o controlo direto desse ficheiro. Esse é o tema de a configuração manual do WireGuard numa VPS.
Precisa de um host Linux que possa administrar, como uma VPS KVM, com um endereço IP público ou um nome de domínio que aponte para esse host, uma CPU x86_64 ou arm64, Docker Engine com o plugin Compose e acesso root. A virtualização de contentores que partilha o kernel do host, como OpenVZ ou LXC, normalmente não consegue carregar o módulo WireGuard. Nesse caso, o contentor não conseguirá ativar a interface.
A versão 15 retirou as definições do ambiente
A maioria dos guias disponíveis foi escrita para o wg-easy 14. Nessa versão, definia WG_HOST com o endereço do servidor e PASSWORD_HASH com um hash bcrypt da palavra-passe de administrador, ambos como variáveis de ambiente. A versão 15 é uma reescrita. As notas oficiais de migração indicam claramente que a v15 não usa as mesmas variáveis de ambiente da v14 e que a maioria delas foi transferida para o painel de administração na interface web.
Por isso, WG_HOST e PASSWORD_HASH já não têm qualquer efeito. Se copiar um ficheiro compose antigo, o contentor arranca, ignora essas linhas e pede-lhe para criar uma conta de administrador no browser. Isto não é um erro. É o novo fluxo de configuração.
Em outubro de 2026, a tag principal a fixar é 15. Fixe a versão principal em vez de usar latest, porque uma atualização de versão principal altera o formato da configuração em disco e não permite um rollback limpo. A mudança para a versão 15 substituiu o ficheiro JSON da v14 por uma base de dados SQLite.
O ficheiro compose
Crie um diretório para a stack e escreva nele o ficheiro compose oficial. Este é o ficheiro upstream, sem as linhas opcionais comentadas.
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.ymlO conteúdo é semelhante a este:
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 é um volume nomeado que contém a chave do servidor e todos os clientes que criar, armazenados na base de dados SQLite wg-easy.db. Faça uma cópia de segurança desse volume. Caso contrário, uma recriação elimina todos os seus peers. Se preferir ver esses ficheiros no sistema de ficheiros do host, substitua-o por uma montagem bind e leia a diferença entre montagens bind e volumes nomeados antes de o fazer, porque as permissões comportam-se de forma diferente.
Por que ele precisa de NET_ADMIN, SYS_MODULE e dos sysctls
Um container não pode acessar a pilha de rede por padrão, e cada uma destas linhas remove um bloqueio específico.
NET_ADMIN permite que o container crie a interface wg0, atribua-lhe um endereço e escreva rotas. Sem isso, o container inicia e depois termina ao ativar a interface, porque ip link add wg0 type wireguard retorna Operation not permitted.
SYS_MODULE, juntamente com a montagem somente leitura de /lib/modules, permite que o container carregue o módulo WireGuard do kernel se o host ainda não o tiver carregado. O módulo fica no kernel do host, não dentro da imagem, por isso o diretório do host precisa estar visível. Num kernel moderno, o módulo normalmente já está incorporado, e pode confirmar isso com sudo modprobe wireguard && echo ok no host.
net.ipv4.ip_forward=1 faz o kernel encaminhar pacotes que não são destinados ao próprio servidor. Sem isso, um cliente conecta-se, o handshake é concluído e, depois, todos os pacotes para a Internet são descartados. Por isso, ping 1.1.1.1 expira enquanto a VPN parece estar conectada.
net.ipv4.conf.all.src_valid_mark=1 é o que mais surpreende. O WireGuard marca os próprios pacotes de saída para que eles não sejam encaminhados de volta para o túnel. A filtragem rigorosa do caminho inverso identifica um pacote cujo endereço de origem não corresponde à rota esperada e descarta-o. Este sysctl instrui o kernel a aceitar pacotes marcados, impedindo que um túnel completo interrompa o próprio funcionamento.
Inicie o serviço e crie a conta de administrador
cd /etc/docker/containers/wg-easy
sudo docker compose up -d
sudo docker compose logs -fUse docker compose up e docker compose down, não start e stop. start apenas reinicia os contentores que já existem. Por isso, qualquer alteração feita no ficheiro compose, incluindo as definições de rede e sysctl, não é aplicada. up recria tudo o que foi alterado. Se quiser que a stack volte a arrancar depois de um reboot, restart: unless-stopped já trata disso. O comportamento de arranque dos serviços compose explica o que essa política garante e o que não garante.
A interface Web escuta em TCP 51821. Na primeira visita, apresenta uma página de configuração onde cria a conta de administrador e confirma o endereço do host que os clientes usarão para chegar ao servidor. Esse endereço do host é incluído na linha Endpoint de cada configuração de cliente. Por isso, tem de ser o IP público ou o nome DNS da VPS. Se estiver incorreto, o código QR fornecido a um telefone aponta para um destino inacessível e o handshake nunca é concluído.
Há mais um aspeto relacionado com essa porta: o wg-easy nunca fornece TLS por si próprio e não recusa HTTP simples. Em http://, a interface continua a carregar e apresenta um aviso. A alteração predefinida afeta o cookie de sessão, que inclui a flag Secure. Por isso, um browser que aceda à interface por HTTP simples através de um endereço público descarta o cookie e o login não fica ativo. INSECURE=true remove essa flag e não faz mais nada. Se colocar um reverse proxy à frente para terminar o TLS, mesmo com um certificado autoassinado, não precisa de alterar nada no wg-easy.
Não publique a porta da interface de gestão na Internet
O ficheiro compose publica a porta 51821 em todas as interfaces. Essa é uma página de início de sessão para um sistema que pode encaminhar o seu tráfego e não deve estar aberta a toda a Internet. A publicação de uma porta no Docker escreve regras na cadeia DOCKER, que é avaliada antes do ufw. Por isso, uma regra ufw deny não a fecha. Vale a pena compreender este problema isoladamente. Porque as portas publicadas pelo Docker ignoram o ufw explica-o em detalhe.
A solução simples é associar a interface de gestão ao loopback e aceder-lhe através de um túnel SSH:
ports:
- "51820:51820/udp"
- "127.0.0.1:51821:51821/tcp"
environment:
- INSECURE=trueDepois, a partir do seu portátil:
ssh -L 51821:127.0.0.1:51821 youruser@your.server.addressAbra http://127.0.0.1:51821 no navegador do seu portátil. O tráfego é cifrado pelo SSH e a porta não responde a mais ninguém. INSECURE=true apenas remove o sinalizador Secure do cookie de sessão para que o navegador mantenha a sessão iniciada neste endereço HTTP simples. Isto é seguro neste caso porque o salto HTTP simples nunca sai da interface de loopback.
Abra a porta UDP 51820 e verifique as duas firewalls
O WireGuard precisa que a porta UDP 51820 esteja acessível a partir da internet. O Docker publica essa porta, mas muitos provedores colocam uma firewall de rede separada à frente do VPS, que não é conhecida pelo Docker. Abra a porta nos dois locais. Se gerir a firewall do host com ufw, as regras básicas do ufw para um VPS são uma opção mais simples do que escrever regras nftables manualmente.
Verifique se o contentor está efetivamente a escutar:
sudo ss -ulnp | grep 51820Deverá ver um socket UDP em estado de escuta. Se não aparecer nada nessa linha, o contentor nunca ativou a interface, e sudo docker compose logs wg-easy indicará o motivo.
Crie um cliente e faça a leitura no telemóvel
Na interface, crie um cliente e atribua-lhe um nome que reconheça mais tarde, como o dispositivo a que pertence. O wg-easy atribui o próximo endereço livre do túnel e gera o par de chaves por si. Cada linha de cliente disponibiliza um código QR e um ficheiro .conf para transferência.
Instale a aplicação oficial WireGuard no telemóvel, escolha adicionar um túnel a partir de um código QR e aponte a câmara para o código no ecrã. O túnel aparece com o nome que introduziu. Ative-o. A linha do cliente na interface começa a apresentar contadores de transferência e a hora do handshake mais recente. Quando um telemóvel está ligado ao túnel, pode aceder a serviços que nunca publicou na internet. É assim que um telemóvel continua a carregar fotografias para um servidor de fotografias autoalojado a partir de qualquer lugar, sem que esse servidor tenha uma única porta aberta para o mundo. O mesmo princípio aplica-se a conteúdos multimédia. Uma biblioteca Jellyfin reconstruída como uma videoclube dos anos 90 é agradável de consultar num quarto de hotel, mantendo-se tão privada como na sua LAN. Os alertas funcionam no sentido inverso no mesmo túnel. Um servidor ntfy autoalojado pode enviar uma mensagem para esse telemóvel no momento em que uma tarefa de backup falha, sem nunca responder a um pedido da internet pública.
Um cliente que não apresenta nenhum handshake depois de ser ativado não está a chegar ao servidor. Isso aponta para a UDP 51820, seja na firewall do fornecedor, seja no endereço do endpoint incluído na configuração. Um cliente que apresenta um handshake, mas não tem acesso funcional à internet, aponta para o encaminhamento ou para o DNS.
Num desktop, transfira o ficheiro .conf e importe-o para o cliente WireGuard, em vez de o introduzir novamente. A chave privada desse ficheiro é gerada uma vez e apresentada uma vez. Trate o ficheiro como trataria uma chave privada SSH.
Quando deixar de usar a interface
O wg-easy é a ferramenta certa enquanto os seus peers são pessoas e telefones. A interface é mais rápida do que editar ficheiros de configuração, e revogar o acesso de um telefone perdido requer apenas um clique.
Os limites surgem quando precisa de algo que a interface não representa. O encaminhamento site-to-site, em que o AllowedIPs de um peer abrange toda uma sub-rede remota em vez de um único endereço, costuma ser o primeiro obstáculo. Túneis divididos com regras de encaminhamento por peer ou uma configuração gerada pela sua ferramenta de provisionamento são os passos seguintes. Nessa altura, a configuração manual não é mais difícil. É apenas diferente, e o guia de WireGuard simples mostra o mesmo túnel criado a partir de wg0.conf. Se preferir deixar de executar completamente o plano de controlo, WireGuard comparado com Tailscale apresenta a opção gerida. A vantagem dessa troca depende do que o servidor de coordenação consegue realmente alcançar, e vale a pena ler o modelo de confiança do Tailscale antes de lhe entregar a sua rede. O custo costuma ser a questão seguinte, e o que o plano gratuito do Tailscale cobre realmente é até seis utilizadores sem custos, apenas para uso não comercial. Assim, uma família não paga nada, enquanto uma equipa de uma pequena empresa não é elegível, independentemente de ter poucos elementos. A partir desse ponto, a faturação contabiliza utilizadores, não dispositivos. Isto resulta numa fatura diferente da de um VPS que já paga, por isso quanto custa o Tailscale depois de ultrapassar o plano gratuito é o valor que deve verificar antes de migrar uma equipa. O túnel completo que acabou de criar tem ali um equivalente direto, porque anunciar o VPS como um exit node do Tailscale fornece a mesma rota de saída através do servidor. A aprovação é feita na consola de administração, em vez de ser escrita na configuração de cada cliente. A limitação das sub-redes também tem um equivalente, porque anunciar uma rede privada completa a partir do VPS disponibiliza essa rede a todos os dispositivos da tailnet, sem a edição de AllowedIPs por peer que o levou a deixar de usar a interface. Se quiser esse dashboard e o encaminhamento automático em mesh, mas não quiser usar o servidor de coordenação de outra entidade, executar o seu próprio servidor NetBird num VPS mantém o plano de controlo em hardware que possui. Em contrapartida, terá de configurar o DNS e o TLS, algo que o wg-easy nunca lhe exigiu.
Se a sintaxe do Compose apresentada acima era a parte desconhecida, e não o WireGuard, noções básicas de Docker Compose num VPS explica o formato do ficheiro e os comandos usados no dia a dia.
FAQ
Porque é que o wg-easy ignora WG_HOST e PASSWORD_HASH?
Essas variáveis pertencem ao wg-easy 14. A versão 15 foi reescrita, e o projeto upstream transferiu quase toda a configuração para o painel de administração da interface web. O contentor não lê nenhuma das duas variáveis. Por isso, inicia normalmente e pede-lhe para criar uma conta de administrador no primeiro acesso. Defina o endereço do host visível para os clientes nessa página de configuração.
Preciso de SYS_MODULE se o meu kernel já tiver WireGuard?
Não. SYS_MODULE e a montagem /lib/modules existem para que o contentor possa carregar o módulo quando o host não o tiver. Num host onde sudo modprobe wireguard já seja executado com sucesso, essa capacidade não é usada. Removê-la é uma medida razoável de hardening, e NET_ADMIN continua a ser necessário em qualquer caso.
O cliente liga-se, mas não há Internet. Qual é o problema?
Um handshake sem tráfego significa quase sempre que o encaminhamento não está ativo. Confirme se net.ipv4.ip_forward=1 e net.ipv4.conf.all.src_valid_mark=1 continuam no ficheiro compose, porque uma cópia editada manualmente perde-os frequentemente. Se o encaminhamento estiver ativo, verifique o servidor DNS recebido pelo cliente. Um túnel que envia todo o tráfego pela VPN, mas aponta para um servidor DNS ao qual já não consegue aceder, aparece num browser exatamente como uma ligação inativa.
Como faço uma cópia de segurança dos meus clientes?
Tudo está no volume nomeado etc_wireguard. Na versão 15, isso significa uma base de dados SQLite em /etc/wireguard/wg-easy.db dentro do contentor. A documentação da v15 não descreve nenhuma funcionalidade de cópia de segurança ou exportação. Por isso, faça uma cópia do próprio volume: execute docker compose down, copie o conteúdo do volume para fora do servidor e depois volte a iniciar a stack com docker compose up -d. Faça isto antes de qualquer atualização. O ficheiro wg0.json mencionado nos guias antigos pertence ao wg-easy 14. É o ficheiro exportado pelo botão de cópia de segurança da v14. Na v15, a sua única utilização é ser o ficheiro carregado no assistente de configuração quando migra uma instalação antiga.
Posso executar o wg-easy atrás de um reverse proxy?
Sim. Coloque o proxy à frente da porta da interface web, termine o TLS nesse proxy e deixe INSECURE sem valor. O contentor aceita normalmente a ligação HTTP simples proveniente do proxy, e os exemplos oficiais para Caddy e Traefik não definem qualquer variável INSECURE. Como o browser comunica por HTTPS com o proxy, deve manter o atributo Secure do cookie de sessão. É o único atributo que INSECURE=true remove. Mantenha a porta UDP 51820 publicada diretamente, porque o tráfego VPN usa UDP e não passa por um proxy HTTP.