The Problem

Deploying a WireGuard VPN with a usable web UI typically requires stitching together several components (the kernel module, a backend API, and a frontend). Operators end up writing custom Dockerfiles, wiring environment variables, and maintaining separate UI code bases, which adds operational overhead and creates a fragile deployment surface.

What This Does

wg‑easy bundles the WireGuard daemon, a Node‑based API, and a Vue‑styled admin UI into a single Docker image. The source tree places the UI under src/app/ (≈ 150 Vue components), the server logic under src/server/ (database schema, repositories, utils), and the CLI entry point in src/cli/index.ts. Build metadata lives in package.json (pnpm lockfile present) and container orchestration is defined in docker-compose.yml (production) and docker-compose.dev.yml (development). The Dockerfile produces the runtime image, while Dockerfile.dev supports hot‑reloading during development.

How It Is Wired

Execution starts at src/cli/index.ts, which parses command‑line flags (via commander in the source) and ultimately invokes the server startup routine (src/server/index.ts – not listed but reachable from the CLI). The server creates a SQLite database using Drizzle (src/drizzle.config.ts) and loads the schema defined in src/server/database/schema.ts. That schema imports repository modules such as src/server/database/repositories/client/schema.ts; these, together with the other repository schemas, form a circular import cycle (7 modules, e.g., schema.ts ↔ client/schema.ts ↔ hooks/schema.ts). The cycle inflates the module instability metric (up to 0.47) and makes any change to shared types risk breaking multiple layers.

After the database is ready, the HTTP layer (express/Koa‑style, defined in src/server/api/*.ts) registers routes that the UI consumes. The UI entry point src/app/app.vue mounts the router defined in src/app/router.ts (implicit) and renders pages under src/app/pages/ (admin, clients, login, setup). Shared UI helpers (dialogs, form fields, icons) are duplicated across many components (e.g., CidrDialog.vue, RestartInterfaceDialog.vue, SuggestDialog.vue, ConfigDialog.vue), leading to repeated 6‑line blocks in 67 files. Server‑side utilities like src/server/utils/types.ts exhibit deep nesting (7‑level indentation), which hampers readability.

The CLI also exposes a build.js script (src/cli/build.js) used by the Docker build process, and a version.sh helper (scripts/version.sh) that tags releases. All CI steps run via GitHub Actions defined in .github/workflows/ (build, lint, deploy).

How To Use It

# Clone the repo
git clone https://github.com/moses-y/wg-easy wg-easy
cd wg-easy

# Install pnpm (if not present) and install JS deps
corepack enable
pnpm install

# Build the production image
docker build -t wg-easy:latest .

# Run with the provided compose (creates a SQLite DB in ./data)
docker compose up -d

Configuration: The container expects environment variables documented in docs/content/getting-started.md (e.g., WG_EASY_USERNAME, WG_EASY_PASSWORD, WG_HOST). They are passed through docker-compose.yml under the environment: block. No secret files are committed; the repo includes a LICENSE and a lockfile (pnpm-lock.yaml).

Running the UI: Once the containers are up, the UI is reachable at http://localhost:51820 (default port defined in docker-compose.yml). The CLI can also be invoked directly: node src/cli/index.js --help.

Real‑World Use

A small office can spin up the stack with a single docker compose up -d, then point a reverse proxy (Caddy, Traefik, etc.) at the exposed port as shown in docs/content/examples/tutorials/caddy.md. Users log in via the web UI, generate client configs, and download QR codes for mobile WireGuard clients—all without touching the underlying system’s network configuration.

Code Health & Issues

  • High – Import cycles: 7 modules in src/server/database/* form circular dependencies (e.g., schema.ts, client/schema.ts). Break cycles by extracting shared types or inverting dependencies.
  • High – Duplicate UI code: Repeated dialog logic across > 60 Vue components. Consolidate into shared mixins or composables.
  • High – No test suite: 209 source files, zero test files. Add unit/integration tests and a CI step to run them.
  • Medium – Deep nesting: src/server/utils/types.ts has 7‑level indentation; refactor with guard clauses.
  • Medium – TODO/FIXME markers: 3 occurrences in src/server/utils/session.ts. Resolve or track as issues.
  • High – GitHub Actions not pinned: Workflows reference actions by tag (@v5, @v3). Pin to commit SHA to avoid supply‑chain risk.
  • Medium – Least‑privilege GITHUB_TOKEN: .github/workflows/deploy-pr.yml lacks explicit permissions. Add permissions: block.
  • Medium – Unpinned Docker base image: Dockerfile uses mutable node:jod-alpine tags. Pin by digest.
  • Medium – No dependency vulnerability scan: CI lacks a vulnerability‑review step. Add dependency-review-action or osv-scanner.
  • Medium – Checkout persists token: persist-credentials not set to false. Adjust checkout step.
  • Medium – Container runs as root: No USER directive. Create non‑root user.
  • Low – No workflow timeouts: Add timeout-minutes to jobs.
  • Low – Missing convention files: Add .editorconfig, .gitattributes, and a shared formatter config.

All findings are derived from deterministic static analysis; no additional speculation is included.

The Bottom Line

wg‑easy delivers a convenient, Docker‑first WireGuard management UI with a clear separation of UI and server code, but the codebase suffers from import cycles, duplicated Vue logic, and a complete lack of automated tests. It is suitable for teams that need a quick‑start VPN UI and can allocate effort to refactor the cycles and add test coverage before extending the platform.