Sign inSign up

nullata/nullguard

By nullata

•Updated 5 days ago

WireGuard VPN manager with web UI and REST API. Auto-gen configs and QR codes.

Image
Networking
Security
Web servers
0

2.8K

nullata/nullguard repository overview

Logo

⁠nullGuard

WireGuard VPN management application for creating and managing VPN servers and clients through a web UI and REST API.

⁠Quick Start

Pull the image and start with Docker Compose:

docker pull nullata/nullguard:latest

Create a docker-compose.yml. This example uses MySQL/MariaDB, the default backend — see PostgreSQL⁠ and SQLite⁠ below for the alternatives:

services:
  nullguard:
    image: nullata/nullguard:latest
    ports:
      - "8080:8080"
      # WireGuard UDP ports (add udp ports for each WireGuard server instance)
      - "51820:51820/udp"
    environment:
      SERVER_PORT: "8080"
      SERVER_SSL_ENABLED: false
      DB_TYPE: mysql
      DB_HOST: localhost
      DB_PORT: 3306
      DB_NAME: nullguard
      DB_USER: nullguard
      DB_PASS: changeme
      # Optional values
      SESSION_SECRET_KEY: ""
      WG_SERVER_DEFAULT_NAME: wg0
      WG_SERVER_DEFAULT_COMMENT: nullguard
      WG_SERVER_DEFAULT_ADDR: 10.252.0.1/24
      WG_SERVER_DEFAULT_PORT: 51820
      WG_SERVER_DEFAULT_SUPERNET: 128.0.0.0/1, 0.0.0.0/1
      WG_SERVER_DEFAULT_KEEPALIVE: 30
      WG_SERVER_CONF_PATH: /etc/wireguard
      WG_CLIENT_DEFAULT_NAME: my-wg-client
      AUTO_START_SERVERS: false
      COOKIE_SECURE: false
      SESSION_MAX_AGE: 3600
      ENV: production
    restart: unless-stopped
    cap_add:
      - NET_ADMIN
    sysctls:
      - net.ipv4.ip_forward=1
⁠PostgreSQL

To use PostgreSQL (DB_TYPE=postgres), point the DB_* variables at your Postgres server and optionally set DB_SSLMODE (default disable — a dev default; use require or verify-full for a remote server):

services:
  nullguard:
    image: nullata/nullguard:latest
    ports:
      - "8080:8080"
      - "51820:51820/udp"
    environment:
      SERVER_PORT: "8080"
      SERVER_SSL_ENABLED: false
      DB_TYPE: postgres
      DB_HOST: localhost
      DB_PORT: "5432"
      DB_NAME: nullguard
      DB_USER: nullguard
      DB_PASS: changeme
      DB_SSLMODE: disable
      # ... same optional values as above
    restart: unless-stopped
    cap_add:
      - NET_ADMIN
    sysctls:
      - net.ipv4.ip_forward=1

Note one behavioral difference from MySQL: Postgres unique indexes are case-sensitive, so e.g. two admin usernames differing only by case are allowed on Postgres but not on MySQL's default collation.

⁠SQLite

To run without a database server, drop the five DB_* variables and use SQLite instead. Mount a named volume so the database file persists across container restarts and removals:

services:
  nullguard:
    image: nullata/nullguard:latest
    ports:
      - "8080:8080"
      - "51820:51820/udp"
    environment:
      SERVER_PORT: "8080"
      SERVER_SSL_ENABLED: false
      DB_TYPE: sqlite
      DATABASE_URL: /data/nullguard.db
      # ... same optional values as above
    restart: unless-stopped
    cap_add:
      - NET_ADMIN
    sysctls:
      - net.ipv4.ip_forward=1
    volumes:
      - sqlite_data:/data

volumes:
  sqlite_data:

Note that switching an existing deployment between backends is not an in-app migration — copy the data separately (e.g. mysqldump/pg_dump), since AutoMigrate only handles fresh schemas.

Then run:

docker compose up -d

Access the web UI at http://localhost:8080. On first visit, you'll be prompted to set up an admin password.

⚠️ Important: Remember to configure port forwarding on your router to forward UDP port 51820 (or whichever ports you're using for WireGuard) to your Docker host's IP address. Without proper port forwarding, external clients won't be able to connect to your VPN.

⁠Requirements

The container requires:

  • NET_ADMIN capability (for managing WireGuard interfaces)
  • net.ipv4.ip_forward=1 sysctl (for routing VPN traffic)
  • wireguard-tools (included in the image)
  • MySQL/MariaDB, PostgreSQL, or SQLite database
⁠Database Setup

nullGuard supports MySQL/MariaDB (default), PostgreSQL, and SQLite, selected with the DB_TYPE environment variable. Tables are created automatically on first run.

MySQL/MariaDB (DB_TYPE=mysql, the default) — create a database and user for nullguard:

CREATE DATABASE nullguard;
CREATE USER 'nullguard'@'%' IDENTIFIED BY 'your_password';
GRANT ALL PRIVILEGES ON nullguard.* TO 'nullguard'@'%';
FLUSH PRIVILEGES;

PostgreSQL (DB_TYPE=postgres) — point DB_HOST, DB_PORT, DB_NAME, DB_USER, and DB_PASS at your Postgres server. TLS is controlled by DB_SSLMODE (default disable, a dev default — set require or verify-full for remote servers). One behavioral difference from MySQL: Postgres unique indexes are case-sensitive, so e.g. two admin usernames differing only by case are allowed on Postgres but not on MySQL's default collation.

SQLite (DB_TYPE=sqlite) — no database server needed. The database file is created automatically at the path given by DATABASE_URL (default nullguard.db in the working directory).

⁠Environment Variables

VariableDescriptionDefault
SERVER_PORTPort the app listens on8080
DB_TYPEDatabase backend (mysql, postgres, or sqlite)mysql
DATABASE_URLPath to the SQLite database file (only for sqlite)nullguard.db
DB_SSLMODEPostgres TLS mode (only for postgres). disable is only a dev default — use require (or verify-full) for any non-local Postgres serverdisable
DB_HOSTDatabase host(required for mysql/postgres)
DB_PORTDatabase port(required for mysql/postgres)
DB_NAMEDatabase name(required for mysql/postgres)
DB_USERDatabase user(required for mysql/postgres)
DB_PASSDatabase password(required for mysql/postgres)
SESSION_SECRET_KEYSecret key for session encryption(auto-generated)
WG_SERVER_CONF_PATHPath to store WireGuard config files./
AUTO_START_SERVERSAuto-start all WireGuard servers on startupfalse
ENVSet to production for secure session cookies(empty)

Optional defaults for new servers:

  • WG_SERVER_DEFAULT_NAME - Default server name
  • WG_SERVER_DEFAULT_ADDR - Default server address
  • WG_SERVER_DEFAULT_PORT - Default server port
  • WG_SERVER_DEFAULT_KEEPALIVE - Default keepalive (0-600 seconds)
  • WG_SERVER_DEFAULT_SUPERNET - Default supernet CIDR
  • WG_SERVER_DEFAULT_COMMENT - Default server comment

Optional defaults for new clients:

  • WG_CLIENT_DEFAULT_NAME - Default client name

⁠Features

  • Web UI - Dashboard, server/client management, QR codes, config downloads
  • REST API - Full programmatic control of servers and clients
  • Auto-generation - WireGuard keys and client IPs generated automatically
  • Real-time Status - Monitor server and client connection status
  • Easy Config - Download client configs or scan QR codes for mobile devices

⁠API Quick Start

  1. Create an admin password at http://localhost:8080/setup
  2. Generate an API token from Admin Settings > API Token Management
  3. Create a WireGuard server:
curl -X POST http://localhost:8080/api/v1/create-server \
  -H "Authorization: Bearer <your-token>" \
  -H "Content-Type: application/json" \
  -d '{
    "interfaceName": "wg0",
    "address": "10.0.0.1/24",
    "port": 51820,
    "wanAddress": "vpn.example.com"
  }'
  1. Start the server:
curl -X POST http://localhost:8080/api/v1/deploy-server \
  -H "Authorization: Bearer <your-token>" \
  -H "Content-Type: application/json" \
  -d '{
    "serverId": 1,
    "interfaceName": "wg0"
  }'
  1. Create a client:
curl -X POST http://localhost:8080/api/v1/create-client \
  -H "Authorization: Bearer <your-token>" \
  -H "Content-Type: application/json" \
  -d '{
    "serverId": 1,
    "name": "my-laptop"
  }'
  1. Restart the server to apply client changes:
curl -X POST http://localhost:8080/api/v1/restart-server \
  -H "Authorization: Bearer <your-token>" \
  -H "Content-Type: application/json" \
  -d '{
    "serverId": 1,
    "interfaceName": "wg0"
  }'

⁠API Documentation

⁠Authentication

All API endpoints require a Bearer token in the Authorization header:

Authorization: Bearer <your-api-token>

All responses follow this format:

{
  "timestamp": "2025-01-01T12:00:00Z",
  "status": "success",
  "message": "Description",
  "data": null
}
⁠Server Endpoints
MethodEndpointDescription
GET/api/v1/list-serversList all servers
POST/api/v1/create-serverCreate a new server
POST/api/v1/fetch-serverGet server details
PUT/api/v1/update-serverUpdate a server (must be stopped)
DELETE/api/v1/delete-serverDelete a server (must be stopped)
POST/api/v1/deploy-serverStart a server
POST/api/v1/stop-serverStop a server
POST/api/v1/restart-serverRestart a running server
POST/api/v1/toggle-auto-restartEnable/disable auto-restart on client changes
⁠Client Endpoints
MethodEndpointDescription
GET/api/v1/list-clients/{serverId}List all clients for a server
POST/api/v1/create-clientCreate a new client
PUT/api/v1/update-clientUpdate a client
DELETE/api/v1/delete-clientDelete a client
GET/api/v1/client/{serverId}/{clientId}/configGet client config file
GET/api/v1/client/{serverId}/{clientId}/qrcodeGet client QR code (PNG)
GET/api/v1/client/{serverId}/{clientId}/downloadDownload client config
⁠Create Server

POST /api/v1/create-server

{
  "interfaceName": "wg0",
  "address": "10.0.0.1/24",
  "port": 51820,
  "wanAddress": "vpn.example.com",
  "publicKey": "",         // Auto-generated if omitted
  "privateKey": "",        // Auto-generated if omitted
  "postUp": "",            // Optional iptables rules
  "postDown": "",          // Optional iptables rules
  "supernetCidr": "",      // Optional supernet CIDR
  "bridgeNetworks": "",    // Optional comma-separated CIDRs of server-side LANs clients may bridge into
  "defaultKeepAlive": 30,  // 0-600 seconds
  "comment": ""            // Optional description
}
⁠Create Client

POST /api/v1/create-client

{
  "serverId": 1,
  "name": "my-laptop",
  "address": "",           // Auto-assigned if omitted
  "allowedIps": "",        // Derived from server if omitted
  "dnsServers": "8.8.8.8, 1.1.1.1",  // Optional, max 2
  "fullTunnel": false,     // Route all traffic through VPN
  "keepalive": 30,         // 0-600 seconds, uses server default if omitted
  "exposedLans": ""        // Optional comma-separated CIDRs of LANs reachable through this client (LAN-to-LAN)
}

Response includes ready-to-use config:

{
  "status": "success",
  "message": "Client created",
  "data": {
    "config": "[Interface]\nPrivateKey = ...\n..."
  }
}
⁠Important Notes
  • Client changes require server restart - After creating, updating, or deleting clients, restart the server for changes to take effect
  • Servers must be stopped before updating or deleting
  • Auto-generation - WireGuard keys, IPs, and allowed IPs are auto-generated when not provided
  • Interface name verification - Destructive operations require both serverId and interfaceName to prevent accidents
  • Interface name length - Linux limits interface names to 15 characters
  • Update-server clears omitted fields - On update-server, omitting an optional field (or sending it as "") clears the stored value. Applies to postUp, postDown, supernetCidr, bridgeNetworks, and defaultKeepAlive - always include the fields you want preserved
  • Bridge networks - A server's bridgeNetworks lists server-side LANs that the web UI surfaces as checkboxes on each client form; ticking one appends the CIDR to that client's AllowedIPs
  • LAN-to-LAN (exposedLans) - A client may declare CIDRs reachable through itself; those CIDRs are added to that peer's server-side AllowedIPs so other clients can route to them. Requires net.ipv4.ip_forward=1 and NAT/masquerade (or LAN-side static routes) on the exposing client's host - see the full README for the Ubuntu setup walkthrough

⁠Common Issues

"Server is active. Please stop the server before updating/deleting"

  • Servers must be stopped before updating or deleting. Use /api/v1/stop-server first.

"Server is not currently active"

  • The restart-server endpoint requires a running server. Use deploy-server to start it.

Client changes not taking effect

  • Client modifications require a server restart. Use /api/v1/restart-server.

⁠License

Licensed under the Elastic License 2.0

Copyright 2026 nullata

Tag summary

Content type

Image

Digest

sha256:99c84c6e5…

Size

26.7 MB

Last updated

5 days ago

docker pull nullata/nullguard