Setting up a WireGuard server in 2026 is trivial. AI writes the wg0.conf, docker compose up and wg-easy is running in a minute, generating peers takes one click. The hard part hasn't been setup for years.
The hard part is what happens after the second server.
The moment you have two, the friction shifts. It's no longer about commands or configs. The questions change. Who has access to what? When was this peer issued? Whose pubkey lives where? How do I revoke a contractor in one place? Setup is solved. Operations are not.
By the time my "one server" had quietly turned into several, I was managing them through separate dashboards with credentials in separate password manager entries. The fleet just... happened. Not because anything was hard. Each addition was a 30-minute task that made local sense, and the tools around WireGuard kept treating "one server" as the unit of operation.
Multiple servers, multiple teams, no shared state. No central place to answer the basic question: who has access to what.
Below: where every WireGuard admin tool falls off the cliff at server 2, and what I ended up building when nothing existing scaled.
TL;DR
- Most WG admin UIs (wg-easy, wg-portal, WireGuard-UI) work great for one server. Past that, you have N independent dashboards and no shared state.
- The platforms that do scale (Tailscale, NetBird, Firezone post-1.0) solve a different problem (mesh/ZTNA), not classic publicly addressable WG fleets.
- Nobody's solving the gap I was sitting in: many hub-and-spoke WG servers, central operator UI, REST API on each box.
- I split it into a Console (central source of truth) and a Node (per-server REST agent). Two-node walkthrough below.
How a bastion becomes a fleet
I've heard versions of this story from enough people now that I'm pretty sure the path is universal. It starts the way mine did: one VPN server, because it beats exposing SSH or internal APIs to the public internet. Then something forces a second, and the trigger is almost never architectural. It's stuff like:
- Staging is reachable by all engineers; prod-write only by SRE. Two networks, two policies, two servers.
- Compliance carved off a data-VPC, and that one needs its own access controls because the auditor will ask.
- An acquisition or partnership brings somebody else's networks into the picture, and now you're keeping their VPN running too.
None of these felt like a project. Each one was a 30-minute task. That's the trap: my unit of operation was "one server," and every tool around WireGuard was designed around that same unit. New container in two commands. Peer generated through the UI in a click. Config sent to the user via Slack DM or 1Password share, whichever's lying around. Done. Move on.
For me, the moment I realized I was in a fleet wasn't about the count. It was a Tuesday in January when a contractor's project wrapped and I had to revoke her access. Her pubkey was scattered across multiple wg0.conf files, two Ansible inventories, and one wg-easy database. There was no clean way to find every place it lived, let alone log what actually got removed. After that, "one server at a time" stopped looking like a strategy. It looked like technical debt I'd built on purpose, and was now stuck maintaining.
That's where the existing tools start cracking.
What I tried, and why each one cracked
Writing my own thing was the last option, not the first. I went through everything else I could find. Table for skimmers, longer breakdown after.
| Tool | Where it broke for me |
|---|---|
| wg-easy, wg-portal, WireGuard-UI | Single-server UIs. Past server #2, you have N independent dashboards with no shared state |
| Firezone (1.0+) | Pivoted to a full ZTNA platform. Heavier than the problem deserved |
| Tailscale, Headscale | Mesh, not hub-and-spoke. Solves a different problem |
| NetBird, Defguard | Same family as Tailscale, same answer |
| pfSense / OPNsense | WG inside a firewall appliance. Not viable for cloud VMs |
| Ansible / Terraform | Operator UX is a PR pipeline, not a UI |
wg-easy. It's great for what it is. One server, one Docker container, one web UI. I ran it for months on a single box and was happy. Adding a second node gave me two completely independent dashboards with no shared state. Issuing a peer now meant deciding which dashboard, remembering its admin password, and tracking the assignment somewhere outside the tool. Three nodes in, my "somewhere outside the tool" was a Notion page that lied to me twice a week.
wg-portal, WireGuard-UI, and friends. Same architectural shape. Single-server UIs, some prettier than others. None of them are control planes. They're admin panels.
Firezone. I was excited about Firezone for a stretch. Somewhere around their 1.0, though, it pivoted from "nice WireGuard admin panel" to a full ZTNA platform: identity providers, policy engine, gateway model, deeper Kubernetes integration. Probably the right direction for them as a company. But my mental model is still "I have these WG boxes, please let me edit their peers from one screen," and Firezone got too heavy for that.
Tailscale and Headscale. Completely different model. Mesh, not hub-and-spoke. Every node knows every node, NAT traversal happens through DERP relays, and the "exit node" abstraction replaces the idea of a regional egress server. I use Tailscale for my own devices and it's lovely for "connect my laptop to my home server." But once the requirement becomes "user traffic has to egress from a known public IP in country X," or "give a contractor access to a specific VPC without installing a Tailscale agent on their machine," the mesh model is solving a problem I don't have. My nodes are already publicly addressable. That isn't a constraint I'm trying to work around.
NetBird, Defguard. Same family as Tailscale. Overlay networks with identity baked in. Same answer for the same reason.
pfSense / OPNsense. WireGuard is a feature of the firewall. Lovely if your edge is already pfSense. If it isn't, putting pfSense on every cloud VM just to get a UI is not the move.
Ansible / Terraform. They work, in the sense that I can make YAML or HCL do almost anything if I'm stubborn enough. But turning "create a peer" into a pull request with a 30-second pipeline is not the operator UX I want, and git log is not an audit trail I want to read at 2 a.m.
What I noticed after going through all of them: each tool either solves a smaller version of my problem (one server) or a structurally different one (mesh/ZTNA). The gap I was sitting in (classic hub-and-spoke WireGuard servers, several of them, with one place to manage peers) just wasn't anybody's target.
The pattern that worked
After the third time I almost wrote a wg-easy fork that supported multiple backends, I split the system properly. Two services:
Console. Central control plane. Web UI, database, and the only thing I touch as an operator. Holds the inventory of nodes, peers, and who got issued what and when.
Node. Runs on each WG server. Owns the localwginterface and the address pool. Exposes a REST API that the Console calls over an API key.
A few things this split bought me that a single binary couldn't:
Console outage doesn't kill the VPN. Nodes keep serving existing peers. They can optionally persist their peer store on disk, so they survive a reboot even with the Console offline.
One pane of glass without tight coupling. Adding a node is running another container and pasting its API endpoint and key into the UI. No shared database, no orchestrator dependency, no service mesh.
The API is the API. Anything the UI does, a script can do. No part of operations requires SSH-ing into a box to runwg set.
Node upgrades don't lose peer state. Peer store is decoupled from the binary version.
It's the same control-plane pattern Kubernetes and friends have used for years. The reason it doesn't show up in small infra tools is simple: nobody's first server needs it, and by the time the second one shows up, you've already started building the wrong way.
A two-node minimal setup
Here's what that looks like end-to-end. Console on one machine, two Nodes on two others. This is WGKeeper, the thing I ended up building: AGPL-3.0, Docker images on GHCR, full walkthrough in the Quick start docs. What's below is the compressed version, just enough to convey the shape.
Console — anywhere reachable from your browser; your laptop works for a first try.
First, generate a secret key for session signing (keep it stable across restarts — rotating it invalidates all sessions):
openssl rand -hex 32
Then drop the output into SECRET_KEY below:
# docker-compose.yaml
services:
wgkeeper-console:
image: ghcr.io/wgkeeper/console:latest
container_name: wgkeeper-console
ports:
- "8000:8000"
environment:
PORT: 8000
DATABASE_URL: file:/app/data/wgkeeper-console.db
SECRET_KEY: REPLACE_WITH_OUTPUT_OF_openssl_rand_hex_32
BOOTSTRAP_ADMIN_PASSWORD: change-me-now
COOKIE_SECURE: "false"
volumes:
- wgkeeper-console-data:/app/data
restart: unless-stopped
volumes:
wgkeeper-console-data:
Node runs on each WireGuard server. Let's call them de-1 and nl-1. Each one needs its own config — and its own API key. Generate one per node with the same command:
openssl rand -hex 32
Then put it into config.yaml:
# config.yaml
server:
port: 51821
auth:
api_key: "REPLACE_WITH_OUTPUT_OF_openssl_rand_hex_32"
wireguard:
interface: "wg0"
subnet: "10.10.0.0/24" # different /24 per node
server_ip: "10.10.0.1"
listen_port: 51820
routing:
wan_interface: "eth0" # check with `ip route` — varies by cloud
# docker-compose.yaml
services:
wireguard:
image: ghcr.io/wgkeeper/node:latest
cap_add: [NET_ADMIN, SYS_MODULE]
volumes:
- ./config.yaml:/app/config.yaml:ro
- ./wireguard:/etc/wireguard
ports:
- "51820:51820/udp"
- "51821:51821"
sysctls:
- net.ipv4.conf.all.src_valid_mark=1
- net.ipv4.ip_forward=1
restart: always
Bring up the Console, then both Nodes. In the Console UI, click Nodes → Add node twice. Paste each Node's API endpoint and its API key, click Check node to verify connectivity, then Save node. Both should flip to online within seconds.
The Nodes list is the main operator view. Every connected node shows its status, running version, and whether an update is available. Same UI handles two nodes or twenty. You see what's running, what's stale, what's unreachable, in a single screen.
Click any node to drill into it. The Peers tab is where most operational work lives: issuing access, revoking it, checking who's actually connected versus who just has a config sitting around. Every peer shows its allowed IPs, public key, status, last handshake, creation date, and expiry. Revocation is one click. This is the audit trail that was missing the day I had to chase that contractor's pubkey across multiple files and databases.
To create a peer on de-1: the Console allocates an IP from de-1's 10.10.0.0/24, generates the keypair, and hands you a wg-quick-compatible config to deliver to the user. Same flow on nl-1 produces a config in a different subnet. One operator UI, two regions, no SSH involved anywhere.
For anything past a tinker setup (TLS in front of the Console, HTTPS for the Node API, wireguard.peer_store_file so peers survive reboots, the per-node Prometheus endpoint feeding the bundled Grafana dashboard), see the docs.
What WGKeeper is not
A few things to be direct about, because it saves time:
No SaaS. You self-host both Console and Nodes.
Not a Tailscale clone. No mesh, no NAT traversal, no DERP relays. Nodes need to be publicly reachable on UDP. Clients are plain hub-and-spoke peers using the standard WireGuard client.
Not a ZTNA platform. No identity providers, no per-app policies, no device posture. If those are hard requirements, Firezone or Defguard fit better.
Not a firewall. It manages WireGuard. Thenftablesrules around it are still on you.
No magic. The Node useswireguard-toolsunder the hood. You still need to understand WireGuard.
If any of those are dealbreakers, good. You saved an evening.
Where it fits
WGKeeper is for people whose problem reduces to: "I run multiple publicly addressable WireGuard servers and I want one place to manage their peers."
The shapes I see most often:
Cloud bastions across regions. Separate VPCs ineu-central-1,us-east-1, and friends, each with its own access list.
Per-environment isolation. Separate WG networks for prod, staging, dev, or for compliance-isolated data VPCs that have their own audit requirements.
MSPs and agencies running per-client isolated WireGuard environments across providers.
Homelab fleets with regional egress nodes for streaming, latency-sensitive games, or country-locked APIs.
Self-hosters sharing access with family or friends across multiple boxes, plus anyone who outgrew per-server wg-easy installs as the fleet grew past one or two.
Where it doesn't fit: single-server setups, where wg-easy is straight-up better since it's less to run. Anything mesh-shaped (use Tailscale or NetBird). And anywhere identity-aware policy is the hard requirement (Firezone, Defguard).
Why AGPL?
AGPL-3.0, because I want the project to stay open. If you're self-hosting WGKeeper, or running it on company infrastructure, the AGPL part doesn't really apply to you. It behaves like the GPL: nothing new to worry about. What the AGPL actually targets is anyone tempted to wrap WGKeeper into a hosted SaaS without contributing back. Modifications have to come upstream. That keeps the door open for the project to grow as a community thing instead of as someone's closed fork.
State of the project
WGKeeper lives at wgkeeper/wgkeeper on GitHub. That's the umbrella repo, with links to the Console and Node component repos. Docs are at wgkeeper.github.io: install guides, configuration reference, the Grafana dashboard, the API reference. Docker images on GHCR.
Honestly: this is a side project. Built and maintained in spare time. No startup behind it, no roadmap deck, no investor narrative. The GitHub org isn't bigger than it looks. I'm posting this because for a project like this to find the people it could actually help, those people have to randomly read about it, and that's not happening on its own.
It's early. There are rough edges I know about, and probably some I don't. If the problem I described matches yours, I'd love issues, feedback, and especially "this is the part I expected to work and it didn't" reports. PRs welcome too. Review can take a while since the team is small, but the bar isn't high and the codebase isn't scary.
Project links
Documentation — install, configure, operate
Quick start — one Console, one Node, working VPN
Umbrella repo on GitHub- Component repos: Console, Node



SOCIAL SHARE CARD GENERATOR