New here? Read this first 👋

Never heard of Marzban, a "VPS", or "a panel"? Perfect — this page explains the whole idea in plain words. Zero experience needed.

In one sentence: NexusPanel is software that lets you run your own VPN service and sell access to it — a bit like running your own little Netflix, except what you're selling is a private, unblocked, faster internet connection.

You rent a cheap server, install NexusPanel on it, and you get a clean web dashboard. From that dashboard you create customers, and each customer gets a link they paste into a free app on their phone. They tap "connect" — and their internet now runs through your server. You charge them monthly. That's the whole business.

The shop analogy

If you can picture running a small shop, you already understand NexusPanel. Here's the whole thing mapped out:

🧑‍💼
You

The owner

You run the business, set prices, and add customers. You do not need to be a programmer.

🖥️
VPS

Your shop building

A computer you rent in a data center (≈ $5/month). Your panel lives here. Providers: Hetzner, Contabo, DigitalOcean…

🎛️
NexusPanel

Your till + shelves

The control panel you log into in a browser. Add customers, watch traffic, get paid — all from here.

🌍
Node

A branch abroad

An extra server in, say, Germany or Finland so customers can choose where to connect. Optional — start with none.

🔗
Sub link

The membership card

One web link you hand each customer. It carries all their connection settings — that's all they ever need.

📱
Client app

The customer's door

A free app (Happ, v2rayNG, Streisand…). They paste the link once, tap connect, done. You never touch their phone.

So what's "Marzban"?
Marzban is a popular free tool people have used for years to do exactly this. It works — but you're on your own: no support, manual updates, and none of the modern anti-blocking features. NexusPanel is the grown-up, supported version. If you already run Marzban, you can move everything across in one command and your customers won't notice a thing (their links keep working). If you've never touched it — even better, you start fresh and skip the hard parts. Running Remnawave instead? Same deal — see Migrate from Remnawave.

How the money actually flows

  1. Someone wants private or unblocked internet and pays you (collect crypto automatically through the built-in Telegram bot, or take payment however you like).
  2. You open NexusPanel and create a user for them — set how long it lasts and how much data they get. Takes about 10 seconds.
  3. You send them their subscription link.
  4. They paste it into a free app and tap connect. They're online through your server.
  5. Next month they pay again to stay active. Repeat with as many customers as you want.
You don't need to understand the deep tech
Words like VLESS, Xray, Reality or Hysteria are just different kinds of tunnels the data travels through. NexusPanel picks sensible defaults — you can run a whole business without ever learning what they mean. When you get curious, the glossary explains each one in a single line.

Ready? Pick your starting point

Head to Where to Start and choose one of three paths: try the free trial (no install), install fresh on a new server, or migrate from Marzban.

Words you'll see 📖

Every bit of jargon in these docs, explained in one plain sentence. Skim it now; come back whenever a word trips you up.

VPS virtual private server
A computer you rent in a data center, by the month. It's where your panel runs. ~$4–6/month is plenty to start.
Panel
The web dashboard you log into to run everything — NexusPanel itself. It lives on your VPS.
Node
An extra server in another location, linked to your panel, so customers can choose where to connect. Completely optional.
User a.k.a. customer
One person you sell access to. Each has an expiry date, a data limit, and their own subscription link.
Subscription link "sub link"
The single URL you give a customer. Their app reads it to learn how to connect. If you migrate from Marzban, these keep working.
Client app
The free app a customer installs — e.g. Happ, v2rayNG, Streisand, Hiddify. They paste the sub link into it once.
Marzban
The older, free, do-it-yourself panel many operators started on. NexusPanel is the upgraded, supported successor — and can import a Marzban setup in one command.
Remnawave
Another panel some operators run. NexusPanel can import a Remnawave setup in one command too, existing subscription links included.
License
Your key to run NexusPanel. Grab a free 14-day Pro trial or a paid plan from the Telegram bot. Without one, the panel runs in trial mode.
Domain
A name like panel.yoursite.com that points at your VPS. Needed for the browser padlock (HTTPS). Optional but strongly recommended.
SSL / HTTPS
The padlock in the browser — encryption that keeps logins safe. NexusPanel sets it up automatically when you have a domain.
Xray
The free engine under the hood that actually moves the encrypted traffic. You rarely touch it directly.
VLESS / VMess / Trojan / Shadowsocks
Different kinds of tunnels Xray can use. Think different makes of car — all get you there. VLESS is the usual default.
Reality / XHTTP / ECH / Finalmask
Tricks that make your traffic look like ordinary browsing so it's harder to block. Toggled per-host; the defaults are fine to begin with.
Hysteria 2
A different, very fast tunnel type that shines on bad or throttled networks. Optional; runs alongside Xray.
Middle server
A cheap relay placed in front of your real server to dodge blocks. Advanced — ignore it until you actually need it.
Inbound / Host
A specific "entry door" into your server (a protocol + port + settings). The panel ships sensible ones; add more when you want.
Admin / Reseller
Extra logins you create. A reseller manages their own customers within limits you set — handy when others sell under you.
IP / device limit
A cap on how many phones or computers one customer can use at once — stops password-sharing from eating your bandwidth.
That's the hard part over
If those make rough sense, you know enough to run NexusPanel. Head to Where to Start and pick a path.

Where to Start

Four paths. Pick the one that matches you and you'll be running in under 10 minutes.

NexusPanel is a multi-tenant VPN panel — you sell sub-accounts, your customers connect through any v2ray client, you keep everything in one dashboard. If you're new, the fastest way to see what it does is the free trial. If you already run Marzban, the migration tool brings everything across in one command — users, admins, hosts, certs, even your existing subscription URLs keep working. Running Remnawave instead? There's a one-command migration for that too — see Migrate from Remnawave.

Path A · No install

Try the Free Trial

Open the Telegram bot, type /start, get a free 14-day Pro trial license — no card required. You can spin up your own panel with that license and play with everything before paying.

⏱ 2 min 💳 No card needed
Open Telegram bot →
Path B · New VPS

Install on a Fresh Server

One command on a clean Ubuntu 20.04+ VPS. The script asks for your license, domain, and admin password — that's it. SSL is auto-configured if you point a domain at the server.

⏱ 5 min 🖥 1 GB RAM minimum
See install command ↓
Path C · Coming from Marzban

Migrate from Marzban

Same VPS, no client reconfiguration. The migration tool is dry-run-first and reversible — you preview every change before anything is touched, and you can roll back any time before the final cutover.

⏱ 10 min 🔁 Resumable + reversible
Migration guide ↓
Path D · Coming from Remnawave

Migrate from Remnawave

Same idea as the Marzban path: dry-run by default, nothing written until you pass --run, and customers' existing subscription links keep resolving after cutover.

⏱ 10 min 🔁 Dry-run first + reversible
Migration guide ↓
Why migration is safe
The tool never writes to your Marzban database. It snapshots Marzban into a read-only copy, builds the Nexus state from that copy, and only swaps containers at the very last step. If anything looks wrong before that swap, you abort and Marzban keeps running untouched. Your existing subscription URLs continue to work after migration too — Nexus carries forward Marzban's JWT secret so every https://your-panel/sub/<token> link your users already have keeps resolving.

What you'll need

  • A VPS with Ubuntu 20.04+ (or any Debian-family distro), 1 GB RAM minimum, 2 GB recommended
  • Root SSH access to that VPS
  • A domain pointed at the VPS (optional — gives you a real, warning-free HTTPS certificate; without one the installer still serves HTTPS, just with a self-signed certificate that shows a one-time browser warning)
  • A NexusPanel license (grab one from the Telegram bot, free 14-day Pro trial works)

How to get help

If anything fails, try these in order:

  1. Check the panel logs: cd /opt/panel && docker compose logs --tail 100
  2. Read the relevant section of these docs (sidebar on the left)
  3. Message us on Telegram — link in the bot, response time is hours not days

What is NexusPanel

In plain words
It's the dashboard you log into to run a VPN business: add customers, hand out connection links, see who's online, and get paid — all in one place. New to all this? Start with New here? Read this first.

NexusPanel is a modern, feature-rich proxy management panel built for VPN providers and network administrators. It provides a unified dashboard to manage users, nodes, subscriptions, and analytics across multiple servers.

Under the hood, the full feature set includes:

  • Multi-protocol support — VMess, VLESS, Trojan, Shadowsocks via Xray-core, plus Hysteria 2 as a separate sidecar. Transport & obfuscation extensions (XHTTP, Reality, ECH, TLS fragments, Finalmask) are configured per-host in the panel.
  • Distributed nodes — connect unlimited remote servers from a single panel
  • Real per-user limits — data, expiry, IP & device caps actually enforced via Xray access log parsing
  • Admin roles & host scoping — owner, admin, reseller tiers with traffic quotas; assign specific hosts to specific admins
  • REST API — 75+ endpoints for automation and integration
  • Grafana-style analytics — traffic-over-time, user growth, protocol/status donuts, top consumers, node bandwidth load (auto-refresh)
  • Telegram bot — customer-facing payment bot (crypto via NOWPayments) plus admin notifications
  • License system — trial → paid tiers with 6-hour heartbeat & update notifications for Docker images
  • Encrypted Happ links — real RSA-4096 happ://crypt4/ deeplinks that hide the underlying subscription URL
  • 2FA — TOTP with QR & recovery codes
  • Mobile-ready — responsive dashboard with bottom-bar nav and overflow drawer
  • Code protection — sensitive Python modules Cython-compiled to .so binaries
  • In-app actionable notifications — expiring users, data caps, offline nodes, license expiry

Requirements

ComponentMinimumRecommended
OSUbuntu 20.04+ / Debian 11+Ubuntu 22.04 LTS
RAM1 GB2 GB+
CPU1 vCPU2 vCPU
Disk10 GB20 GB+ (SSD)
Docker20.10+Latest stable
DomainOptionalRecommended (for SSL)
Note
Docker and Docker Compose are installed automatically by the quick install script if not present.

Quick Install

Run this single command on a fresh VPS to install NexusPanel with default settings:

bash
curl -sL https://nexuspanel.store/install | bash

The script will prompt you for:

  1. License Key & Client ID — from @nexuspanelpayment_bot (the free 14-day Pro trial works here too, but the installer always requires a key — there's no license-less install)
  2. Domain — for SSL via Let's Encrypt (skip for IP-only)
  3. Admin username & password — for the dashboard
  4. Panel port — defaults to 8443

Then it will:

  1. Install Docker & Docker Compose if missing
  2. Pull ghcr.io/haitovs/nexus:latest (Cython-protected production image)
  3. Create /opt/panel/ with .env and docker-compose.yml (container name: nexus-panel)
  4. Seed xray_config.json with access log enabled (required for IP/device limit enforcement)
  5. Start the panel and print dashboard URL + credentials
Update notifications
Once installed, the panel heartbeats your license server every 6 hours. Auto-update is off by default: when a new version is published, the panel shows an “update available” notice and you apply it by running nexus update on the host.

One-Command Install (Detailed)

The install script accepts optional flags to customize the setup:

bash
# Recommended — pass your License Key + Client ID (from @nexuspanelpayment_bot)
curl -sL https://nexuspanel.store/install | bash -s -- \
  --license YOUR_LICENSE_KEY \
  --client YOUR_CLIENT_ID \
  --domain panel.example.com \
  --port 8443

# Same thing with environment variables instead of flags
curl -sL https://nexuspanel.store/install | LICENSE_ID=YOUR_LICENSE_KEY CLIENT_ID=YOUR_CLIENT_ID DOMAIN=panel.example.com bash

# IP-only (no domain) — just omit --domain, you'll still get HTTPS (self-signed cert)
curl -sL https://nexuspanel.store/install | bash -s -- \
  --license YOUR_LICENSE_KEY \
  --client YOUR_CLIENT_ID

The License Key and Client ID are required — get them from @nexuspanelpayment_bot. Run bash -s -- --help to see all flags (--port defaults to 8443, plus --username, --password, --ssl, --migrate).

When the script finishes it prints your dashboard URL, admin username, and admin password — use those to log in. Don't reuse example values from these docs (like myadmin/securepass123); they won't work.

The panel is accessible at https://YOUR_DOMAIN:8443/dashboard/ (or https://YOUR_IP:8443/dashboard/ for an IP-only install).

"Not Secure" warning on IP-only installs
Without a domain, the panel still serves HTTPS, but with a self-signed certificate (there's no domain for Let's Encrypt to issue a real one against). Your browser will show a one-time "Not Secure" / "Your connection is not private" warning the first time you open the dashboard — click Advanced → Proceed (Chrome) or Visit this website (Safari) to continue. This is expected and safe; the connection is still encrypted, it's just not signed by a public authority. Point a domain at the server later for a warning-free padlock.

First Steps in the Panel

Once you're logged in, here's the fastest path to your first working connection:

  1. Create a user — Dashboard → UsersAdd User. Set an expiry date and data limit, then copy their subscription link and hand it to a client app (Happ, v2rayNG, Streisand…).
  2. Add a node (optional) — Dashboard → NodesAdd New Node, copy the generated quick-install command, run it on the node server, then come back and fill in Name + Address to connect it. See Node Install for details.
  3. Set your subscription domain — if you're serving subscription links from a different host/domain than the admin panel itself, set XRAY_SUBSCRIPTION_URL_PREFIX in the Env Editor (Settings → Env) and use Save & Restart — this setting only takes effect after a full restart.

Manual Install

The install script above is the supported path — it authenticates to the private image registry, writes a working .env, and sets up the firewall for you. To assemble it by hand instead:

bash
# 1. The image is private — a bare `docker pull` fails with "denied" until
#    you authenticate. Exchange your license for a short-lived pull token:
curl -s -X POST https://nexuspanel.store/api/registry-token \
  -H 'Content-Type: application/json' \
  -d '{"license_id":"YOUR_LICENSE_KEY","client_id":"YOUR_CLIENT_ID"}'
# → {"token": "...", "username": "..."} — log in with it:
echo $TOKEN | docker login ghcr.io -u $USERNAME --password-stdin

# 2. Pull the image
docker pull ghcr.io/haitovs/nexus:latest

# 3. Write /opt/panel/.env — see Env Reference below for every key. Minimum:
#    UVICORN_PORT, SUDO_USERNAME, SUDO_PASSWORD, SQLALCHEMY_DATABASE_URL,
#    LICENSE_ID, CLIENT_ID
mkdir -p /opt/panel /var/lib/panel /var/lib/nexus
nano /opt/panel/.env

# 4. Start with the docker-compose.yml from the Docker Compose Examples section below
cd /opt/panel
docker compose up -d

# View logs
docker compose logs -f

With SSL (Certbot)

To enable HTTPS with a free Let's Encrypt certificate:

bash
# Install certbot
apt install -y certbot

# Obtain certificate (stop panel first if using port 80)
docker compose down
certbot certonly --standalone -d panel.example.com

# Add to .env
UVICORN_SSL_CERTFILE="/etc/letsencrypt/live/panel.example.com/fullchain.pem"
UVICORN_SSL_KEYFILE="/etc/letsencrypt/live/panel.example.com/privkey.pem"

# Mount certs in docker-compose.yml and restart
docker compose up -d

Add a cron job for automatic renewal:

bash
0 3 * * * certbot renew --quiet && docker compose -C /opt/nexuspanel restart

With PostgreSQL

For production deployments, PostgreSQL is recommended over SQLite. Set BACKEND_MODE=modern and use the driver the panel actually ships — psycopg2 (sync), not asyncpg:

bash
# Set in .env
BACKEND_MODE=modern
SQLALCHEMY_DATABASE_URL="postgresql+psycopg2://nexus:${POSTGRES_PASSWORD}@127.0.0.1:5432/nexus"
REDIS_URL="redis://127.0.0.1:6379/0"
POSTGRES_PASSWORD=$(openssl rand -hex 24)

Don't hand-write the compose file — use the repo's maintained docker-compose.modern.yml (repo root), which brings up Postgres 16 + Redis 7 alongside the panel on network_mode: host:

bash
# From your install dir (e.g. /opt/panel)
cp docker-compose.modern.yml docker-compose.yml
docker compose up -d
asyncpg isn't installed
The panel ships psycopg2-binary, not asyncpg — a postgresql+asyncpg:// URL will fail to find a driver. Always use postgresql+psycopg2://.

Docker Compose Examples

Classic (SQLite) — what the installer actually writes

yaml — /opt/panel/docker-compose.yml
services:
  panel:
    image: ghcr.io/haitovs/nexus:latest
    container_name: nexus-panel
    restart: always
    env_file: .env
    network_mode: host
    dns: [8.8.8.8, 1.1.1.1]
    volumes:
      - /var/lib/panel:/var/lib/panel
      - /var/lib/nexus:/var/lib/nexus
      - ./.env:/app/env.live
      - /var/run/docker.sock:/var/run/docker.sock:ro
    environment:
      NEXUS_HOST_ENV_FILE: /app/env.live
    healthcheck:
      test: ["CMD", "curl", "-skf", "http://127.0.0.1:8443/api/v1/health"]
      interval: 30s
      timeout: 5s
      start_period: 30s
      retries: 3

network_mode: host is required — the panel and any co-located node/middle-relay bind ports directly on the host, and the docker socket mount (read-only) is what drives the one-click node/middle-server SSH installs. /var/lib/nexus isn't optional: it's where license enforcement caches its state — omit that mount and the panel can't tell it's licensed. Swap 8443 in the healthcheck for whatever UVICORN_PORT you set; if you're serving a domain, also mount /etc/letsencrypt:/etc/letsencrypt:ro and point UVICORN_SSL_CERTFILE/UVICORN_SSL_KEYFILE at the issued cert.

Full Stack (PostgreSQL + Redis)

See With PostgreSQL above — use the repo's maintained docker-compose.modern.yml rather than hand-writing a Postgres compose file.

Configuration Reference

NexusPanel is configured entirely through environment variables. Set them in your .env file or pass them directly to Docker.

Tip
Copy .env.example to .env and uncomment the variables you need. All variables have sensible defaults.

Server

VariableDefaultDescription
UVICORN_HOST0.0.0.0Bind address for the server
UVICORN_PORT8000HTTP port
UVICORN_UDSUnix domain socket path (overrides host/port)
UVICORN_SSL_CERTFILEPath to SSL certificate (fullchain.pem)
UVICORN_SSL_KEYFILEPath to SSL private key
UVICORN_SSL_CA_TYPEpublicCA type: public or private
DASHBOARD_PATH/dashboard/URL path for the web dashboard
ALLOWED_ORIGINSComma-separated CORS origins
SUDO_USERNAMEInitial super admin username
SUDO_PASSWORDInitial super admin password
JWT_ACCESS_TOKEN_EXPIRE_MINUTES1440Token expiration in minutes (default 24h)

Database

VariableDefaultDescription
SQLALCHEMY_DATABASE_URLsqlite:///db.sqlite3Database connection string
SQLALCHEMY_POOL_SIZE10Connection pool size
SQLIALCHEMY_MAX_OVERFLOW30Max connections above pool size
BACKEND_MODEclassicclassic (SQLite/Postgres, default) or modern (adds Redis-backed event queue)
REDIS_URLRedis connection string; required when BACKEND_MODE=modern
PostgreSQL Connection String
Use postgresql+asyncpg://user:pass@host:5432/dbname for async PostgreSQL.
Modern backend mode
Set BACKEND_MODE=modern and provide REDIS_URL to enable Redis-backed event queuing. Use docker-compose.modern.yml which ships a redis:7 service alongside the panel. Most deployments don't need this.

Xray

VariableDefaultDescription
XRAY_JSONxray_config.jsonPath to Xray core configuration
XRAY_EXECUTABLE_PATH/usr/local/bin/xrayPath to Xray binary
XRAY_ASSETS_PATH/usr/local/share/xrayPath to geoip.dat and geosite.dat
XRAY_SUBSCRIPTION_URL_PREFIXPublic URL prefix for subscription links (e.g. https://sub.example.com). Changes take effect only after a full panel restart — use Save & Restart in the Env Editor, not a container restart.
XRAY_SUBSCRIPTION_PATHsubURL path segment for subscriptions
XRAY_EXCLUDE_INBOUND_TAGSSpace-separated inbound tags to exclude
XRAY_FALLBACKS_INBOUND_TAGInbound tag used for fallback routing

Subscription

VariableDefaultDescription
SUB_PROFILE_TITLESubscriptionDisplay name shown in client apps
SUB_SUPPORT_URLSupport link included in subscription info
SUB_UPDATE_INTERVAL12Client auto-update interval (hours)
EXTERNAL_CONFIGExternal config URL for client integration
USE_CUSTOM_JSON_DEFAULTfalseEnable custom JSON config for default client
USE_CUSTOM_JSON_FOR_V2RAYNfalseEnable custom JSON for V2RayN
USE_CUSTOM_JSON_FOR_V2RAYNGfalseEnable custom JSON for V2RayNG
USE_CUSTOM_JSON_FOR_STREISANDfalseEnable custom JSON for Streisand
USE_CUSTOM_JSON_FOR_HAPPfalseEnable custom JSON for Happ
SUB_RATE_LIMIT_PER_MINUTE60Max subscription fetches per IP per minute (in-process, resets on restart)
SUB_ENABLE_ETAGtrueReturn ETag / honour If-None-Match to save bandwidth on unchanged subs
SUB_GZIP_MIN_SIZE512Gzip-compress subscription responses larger than this many bytes

Templates

VariableDefaultDescription
CUSTOM_TEMPLATES_DIRECTORY/var/lib/panel/templates/Base directory for custom templates
SUBSCRIPTION_PAGE_TEMPLATEsubscription/index.htmlTemplate for the user subscription page
HOME_PAGE_TEMPLATEhome/index.htmlTemplate for the panel home page
CLASH_SUBSCRIPTION_TEMPLATEclash/default.ymlClash subscription template
CLASH_SETTINGS_TEMPLATEclash/settings.ymlClash settings template
V2RAY_SUBSCRIPTION_TEMPLATEv2ray/default.jsonV2Ray subscription template
V2RAY_SETTINGS_TEMPLATEv2ray/settings.jsonV2Ray settings template
SINGBOX_SUBSCRIPTION_TEMPLATEsingbox/default.jsonSing-box subscription template
SINGBOX_SETTINGS_TEMPLATEsingbox/settings.jsonSing-box settings template
MUX_TEMPLATEmux/default.jsonMultiplex config template
USER_AGENT_TEMPLATEuser_agent/default.jsonUser-agent parsing template
GRPC_USER_AGENT_TEMPLATEuser_agent/grpc.jsongRPC user-agent template

Telegram

VariableDefaultDescription
TELEGRAM_API_TOKENBot token from @BotFather
TELEGRAM_ADMIN_IDComma-separated Telegram user IDs for admins
TELEGRAM_LOGGER_CHANNEL_IDChannel ID for log messages
TELEGRAM_DEFAULT_VLESS_FLOWxtls-rprx-visionDefault VLESS flow for bot-created users
TELEGRAM_PROXY_URLProxy URL for Telegram API connections

Notifications

VariableDefaultDescription
NOTIFY_STATUS_CHANGEtrueNotify when user status changes
NOTIFY_USER_CREATEDtrueNotify on new user creation
NOTIFY_USER_UPDATEDtrueNotify on user modification
NOTIFY_USER_DELETEDtrueNotify on user deletion
NOTIFY_USER_DATA_USED_RESETtrueNotify on usage reset
NOTIFY_USER_SUB_REVOKEDtrueNotify on subscription revocation
NOTIFY_IF_DATA_USAGE_PERCENT_REACHEDtrueNotify when data threshold reached
NOTIFY_IF_DAYS_LEFT_REACHEDtrueNotify when expiry threshold reached
NOTIFY_LOGINtrueNotify on admin login
LOGIN_NOTIFY_WHITE_LISTIPs to exclude from login notifications
NOTIFY_DAYS_LEFT3,7Days-left thresholds for notifications
NOTIFY_REACHED_USAGE_PERCENT80,90Usage percent thresholds
RECURRENT_NOTIFICATIONS_TIMEOUT180Minutes between repeat notifications
NUMBER_OF_RECURRENT_NOTIFICATIONS3Max repeat notifications per event
DISCORD_WEBHOOK_URLDiscord webhook for Telegram-style notifications
WEBHOOK_ADDRESSLegacy: comma-separated static webhook URLs. Prefer the dashboard Webhooks UI for new setups.
WEBHOOK_SECRETLegacy: HMAC secret for WEBHOOK_ADDRESS delivery. Dashboard webhooks manage secrets per-endpoint.

Branding (White-Label)

VariableDefaultDescription
BRAND_NAMEPanelPanel name displayed in UI and emails
BRAND_LOGO_URLURL to custom logo image
BRAND_FAVICON_URLURL to custom favicon

Security

VariableDefaultDescription
CAPTCHA_PROVIDERdisabledCaptcha provider: disabled, turnstile, or builtin
TURNSTILE_SITE_KEYCloudflare Turnstile site key
TURNSTILE_SECRET_KEYCloudflare Turnstile secret key
LOGIN_RATE_LIMIT10/minuteMax login attempts per window
LOGIN_LOCKOUT_THRESHOLD10Failed attempts before lockout
LOGIN_LOCKOUT_DURATION_MINUTES30Lockout duration in minutes

Logging

VariableDefaultDescription
LOG_LEVELINFOLog level: DEBUG, INFO, WARNING, ERROR
LOG_FORMATtextLog format: text or json
LOG_FILE_PATHWrite logs to file (in addition to stdout)
LOG_MAX_SIZE_MB10Max log file size before rotation
LOG_BACKUP_COUNT5Number of rotated log files to keep

Metrics (Prometheus)

VariableDefaultDescription
METRICS_ENABLEDfalseEnable Prometheus /metrics endpoint
METRICS_TOKENBearer token required to scrape metrics

Additional Variables

VariableDefaultDescription
ACTIVE_STATUS_TEXTActiveCustom label for active status
EXPIRED_STATUS_TEXTExpiredCustom label for expired status
LIMITED_STATUS_TEXTLimitedCustom label for limited status
DISABLED_STATUS_TEXTDisabledCustom label for disabled status
ONHOLD_STATUS_TEXTOn-HoldCustom label for on-hold status
USERS_AUTODELETE_DAYS-1Auto-delete expired users after N days (-1 = disabled)
USER_AUTODELETE_INCLUDE_LIMITED_ACCOUNTSfalseInclude data-limited users in auto-delete
JOB_CORE_HEALTH_CHECK_INTERVAL10Health check interval (seconds)
JOB_RECORD_NODE_USAGES_INTERVAL30Node usage recording interval
JOB_RECORD_USER_USAGES_INTERVAL10User usage recording interval
JOB_REVIEW_USERS_INTERVAL10User review/expire check interval
JOB_SEND_NOTIFICATIONS_INTERVAL30Notification dispatch interval
DISABLE_RECORDING_NODE_USAGEfalseDisable node usage recording
DEBUGfalseEnable debug mode with hot-reload
DOCSfalseEnable Swagger UI at /docs
VITE_BASE_API/api/v1/Base API path for frontend build

Dashboard

The NexusPanel dashboard is a modern React-based web application accessible at /dashboard/. It provides a complete interface for managing your proxy infrastructure.

Overview Page

The dashboard home page displays real-time statistics at a glance:

  • Total users — active, expired, limited, disabled counts
  • Bandwidth usage — total upload/download with trend graphs
  • Node status — online/offline indicators with load percentages
  • Recent activity — latest user creations, connections, and admin actions
  • Protocol distribution — pie chart of protocols in use

Users Management

The Users page supports full lifecycle management:

  • Create user — set username, data limit, expiry date, protocols, device limit, IP limit
  • Edit user — modify all fields including status (active, disabled, on-hold)
  • Bulk operations — select multiple users for bulk update, reset usage, or delete
  • Search and filter — filter by status, admin, protocol, or search by username
  • Subscription links — copy subscription URL, QR code generation
  • Usage stats — per-user upload/download with historical data

Nodes

Manage remote Xray nodes connected to the panel:

  • Add node — provide address, port, and usage coefficient
  • Connection status — real-time online/offline with latency
  • Country flags — automatic flag display based on node location (60+ countries)
  • Reorder — drag or use arrow buttons to set display order
  • Certificate — view and copy the node SSL certificate for remote setup
  • Uptime tracking — historical uptime percentage per node

Hosts & Advanced TLS Settings

Each Xray inbound has one or more host rows that tell the subscription renderer what address, port, and TLS options to emit in client configs. The full field set per host:

FieldPurpose
RemarkDisplay name shown in client apps
AddressServer domain or IP the client connects to
PortOverride the inbound's listen port
SNI / HostTLS Server Name Indication and HTTP Host header
Security / ALPN / FingerprintTLS profile: none / tls / reality; h2/http1.1; Chrome/Firefox/Safari uTLS
Allow InsecureSkip TLS cert verification (use only behind CDN where cert isn't exposed)
Country codeISO 3166-1 alpha-2 — drives regional subscription reorder
Allowed / Denied AdminsRestrict host to specific sub-admins (leave blank = all admins)

ECH (Encrypted Client Hello)

ECH hides the SNI from passive observers — the TLS handshake extension is encrypted using a public key published in DNS. Enable per host: toggle ECH and paste the ECHConfig blob from your CDN/DNS provider. Requires a client that supports ECH (Happ, Chrome 117+).

TLS Fragmentation

Splits the TLS ClientHello into smaller TCP segments, bypassing DPI pattern matching on the first packet. Use when SNI-based blocking is active but a CDN isn't available.

  • Fragment size — bytes per fragment, e.g. 100-200 (random range)
  • Fragment delay — ms between fragments, e.g. 10-20

TLS Record Fragmentation

Fragments at the TLS record layer rather than TCP. More aggressive than ClientHello fragmentation; use when standard TLS fragmentation is still fingerprinted.

Noise Settings

Injects random noise packets before the real TLS handshake to defeat flow-based fingerprinting. JSON field:

json
[{"type": "rand", "packet": "10-50", "delay": "5-10"}]

Type rand sends random bytes; type str sends a literal hex string. Packet size and delay accept range notation.

Random User-Agent

Randomises the HTTP User-Agent on each request to avoid client fingerprinting on WS/HTTP transports.

Sessions

Monitor and manage active device connections:

  • Active sessions — view all currently connected devices
  • Per-user sessions — see which devices a specific user is using
  • Disconnect — forcibly terminate individual sessions
  • IP history — track user connection history by IP

Analytics

Comprehensive analytics dashboard with:

  • Summary — total users, active connections, bandwidth, revenue overview
  • Protocol distribution — usage breakdown by protocol (VMess, VLESS, etc.)
  • Node load — per-node connection counts and bandwidth usage
  • Node uptime — uptime percentages over 24h, 7d, 30d periods
  • Top users — highest bandwidth consumers
  • Expiring users — users expiring within configurable days

Admin Management

Role-based admin system with three tiers:

RoleCapabilities
OwnerFull access: manage admins, nodes, system settings, all users
AdminManage users (all), view nodes and analytics, limited settings
ResellerManage own users only, limited by max_users and max_traffic_bytes quotas

Each admin can have quotas:

  • max_users — maximum number of users the admin can create
  • max_traffic_bytes — total traffic quota across all their users

Settings

  • Two-Factor Authentication — enable/disable TOTP 2FA from the settings page
  • Xray Core Config — edit the raw Xray JSON in a 2-column layout (editor on left, live logs & status on right)
  • Env Editor — edit SMTP, tokens, feature flags inline with secret masking; Save & Restart self-restarts the panel
  • Hysteria2 — manage hy2 inbounds from the settings page (Standard+)
  • License info — tier, days remaining, current vs max users/nodes

User Groups

User Groups (called Squads in Remnawave) let you segment users for inbound-visibility control and subscription overrides. Pro license, sudo-only.

Each group can do any or all of:

  • Inbound filter (applies_to_inbounds) — CSV of inbound tags. Users in the group only get subscription entries for matching inbounds. Empty = all inbounds.
  • Template override (override_template_id) — use a different subscription template for members of this group.
  • Host override (override_hosts) — inject different host rows into member subscriptions (e.g., give a VIP group a direct-IP host that's hidden from everyone else).

Add users to a group from the User Detail page or via API. A user can be in at most one group.

bash — API
# List groups
curl /api/v1/user-groups -H "Authorization: Bearer TOKEN"

# Create a VIP group that only gets the hy2 + VLESS-Reality inbounds
curl -X POST /api/v1/user-groups \
  -H "Authorization: Bearer TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"name":"VIP","applies_to_inbounds":"hy2-main,vless-reality"}'

# Add user to group
curl -X POST /api/v1/user-groups/1/members \
  -d '{"username":"alice"}' -H "Authorization: Bearer TOKEN"

Inbound Sets

An Inbound Set is a named CSV of inbound tags that you assign to a node. When a node has an inbound set, only those inbounds are activated on it — the rest are suppressed. Use this to run different protocol mixes per node: e.g., node A gets VLESS+Trojan, node B gets VLESS+hy2.

Pro license, sudo-only.

bash — API
# Create an inbound set
curl -X POST /api/v1/inbound-sets \
  -H "Authorization: Bearer TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"name":"UDP nodes","tags":"hy2-main,vmess-ws"}'

# Assign to a node (set inbound_set_id on the node)
curl -X PUT /api/v1/node/1 \
  -d '{"inbound_set_id": 2}' -H "Authorization: Bearer TOKEN"

Subscription Response Rules

Sub Rules let you customise what a user's subscription response looks like based on their client. Rules match on request properties and apply an action.

Match fieldOperatorsActions
user_agentequals / contains / regextemplate / status / headers
client_osequals / contains / regextemplate / status / headers

Examples:

  • Match user_agent contains "Happ" → action template = happ-custom — serve a Happ-optimised template to Happ clients
  • Match client_os equals "iOS" → action headers = {"Content-Type": "text/plain"}
  • Global rules (sudo-only, admin_id = NULL) apply to all users regardless of which admin owns them

Rules are evaluated in ascending priority order. First match wins.

bash — API
# Create a rule: serve sing-box template to Karing clients
curl -X POST /api/v1/sub-rules \
  -H "Authorization: Bearer TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"name":"Karing","match_field":"user_agent","match_op":"contains",
     "match_value":"Karing","action":"template","action_arg":"singbox-default"}'

Webhooks

NexusPanel delivers signed HTTP POST events to any URL you register. Each delivery includes an X-Nexus-Signature header — HMAC-SHA256 of the body with your endpoint's secret.

Event scopes

ScopeEvents
user.*user.created, user.updated, user.deleted, user.expired, user.disabled, user.data_used_reset
node.*node.connected, node.disconnected, node.reconnecting
service.*service.started, service.stopped
billing.*billing.renewed, billing.expired
errors.*errors.cert_expired, errors.xray_crash
hwid.*hwid.mismatch, hwid.reset

Leave scopes empty to receive all events. Delivery retries with exponential backoff; after max attempts the event is marked failed and dropped.

bash — API
# Register an endpoint
curl -X POST /api/v1/webhooks \
  -H "Authorization: Bearer TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"url":"https://my-server/hook","scopes":"user.*,node.*"}'
# Response includes secret (shown once)

# Send a test delivery
curl -X POST /api/v1/webhooks/1/test -H "Authorization: Bearer TOKEN"

# Verify signature in your handler (Python example)
# expected = hmac.new(secret, body, sha256).hexdigest()
# assert expected == request.headers["X-Nexus-Signature"]
Legacy env-based approach
WEBHOOK_ADDRESS (comma-separated URLs) and WEBHOOK_SECRET still work as a static env-var alternative. Use the dashboard UI for new setups — it supports per-endpoint secrets, scopes, and delivery history.

Clients Page

Dashboard → Clients shows a curated list of recommended VPN clients with platform badges, download links, and usage notes. Operators share this page URL with end users.

ClientPlatformsNotes
HappiOS / macOS / Windows / AndroidRecommended — native sub URL, HWID binding, offline cache
v2RayTuniOS / macOS / AndroidPopular iOS client, VLESS-Reality support
KaringAll platformsSing-box-based, strong cross-platform story
ShadowrocketiOS$2.99 US App Store — rock-solid iOS
V2rayNGAndroidClassic Android client
FlClashXWindows / macOS / Linux / AndroidMihomo/Clash-compatible
StreisandiOS / macOSSupports custom JSON — set USE_CUSTOM_JSON_FOR_STREISAND=true

License System

NexusPanel uses a central license server (nexuspanel.store) to validate installations and push updates. This is how clients are tiered, billed, and kept up to date.

How heartbeat works

  • Every 6 hours the panel calls POST /api/validate on the license server with its license_id, client_id, and full telemetry (panel version, Xray version, hostname, OS, IP, total/active users, total/active nodes, total traffic, uptime).
  • The license server stores this and replies with {tier, expires_at, latest_version, update_available, docker_image}.
  • If update_available is true and AUTO_UPDATE is enabled (default), the panel runs docker compose pull && docker compose up -d --force-recreate in the background — you don't have to do anything.

Tiers

TierPriceUsersNodesDuration
Trial (Pro Trial)FreeUnlimitedUnlimited14 days
Standard$10/moUnlimited1030 days/mo
Pro$30/moUnlimitedUnlimited30 days/mo

Trial — Pro Trial

No credit card, no signup — open the Telegram bot and type /start. You get a free 14-day Pro trial: unlimited users and nodes, no card required. Full dashboard access: all protocols and analytics. Enough to evaluate on real traffic.

Standard — $10/month

For operators running a live service. Capped at 10 nodes, and unlocks:

  • Hysteria 2 protocol on all nodes
  • Bulk operations (enable/disable/reset/delete hundreds of users at once)
  • API access for automation and integrations
  • Multi-month billing (3/6/12 months at 5%/10%/15% discount)

Pro — $30/month

Everything in Standard, plus no node cap and the full feature set:

  • Unlimited nodes across any number of countries
  • ECH (Encrypted Client Hello) — hides SNI from DPI
  • Finalmask — anti-fingerprinting transport layer
  • White-label branding (custom panel domain + logo)
  • User Groups & Inbound Sets for reseller tier segmentation
  • Middle Server Relay with auto-generated iptables rules
  • Priority support

Feature comparison

FeatureTrialStandardPro
Max usersUnlimitedUnlimitedUnlimited
Max nodesUnlimited10Unlimited
Duration14 days30 days/mo30 days/mo
All protocols (VLESS, VMess, Trojan, SS)
Hysteria 2
Grafana-style analytics
Live session view
Audit log
Webhooks
Operator CLI
Bulk operations
API access
ECH + Finalmask
White-label branding
User Groups & Inbound Sets
Middle Server Relay

Buying a license

Open @nexuspanelpayment_bot on Telegram. Tap View Plans, pick a tier, choose duration (1/3/6/12 months at increasing discounts), pick a crypto currency (USDT TRC20, BTC, ETH, LTC, TRX, and 200+ others), and send the exact amount shown to the displayed wallet. Once NOWPayments confirms, the bot delivers your License Key and Client ID.

Grace period

If your license expires, the panel keeps running in grace mode for 72 hours so you can renew without an outage. After that the API switches to read-only until a valid license is restored.

IP & Device Limit Enforcement

NexusPanel enforces per-user IP and device limits in real time by parsing the Xray access log — not just at subscription import. This is what makes ip_limit and device_limit actually work.

How it works

  1. Xray writes one line to $XRAY_ACCESS_LOG for every accepted connection.
  2. The enforce_limits job runs every 60 seconds, tails the log (offset-tracked, rotation-aware), and extracts (user_id, client_ip) pairs from the last LIMIT_WINDOW_SECONDS (default 600 = 10 minutes).
  3. For each user, unique IPs are counted. If the count exceeds ip_limit (or device_limit if no ip_limit is set) and the user is currently active and ip_limit_mode == "limit", the user is flipped to status limited.
  4. All seen IPs are written to user_ip_history. View per-user IPs via GET /api/v1/user/{username}/ips.

Required xray config

The default install enables this automatically. For existing installs, the panel auto-patches your xray_config.json on startup to add the access log path. Manual config:

json
{
  "log": {
    "loglevel": "warning",
    "access": "/var/lib/panel/xray-access.log"
  }
}

Tunables

Env VarDefaultPurpose
XRAY_ACCESS_LOG/var/log/xray/access.logPath to the Xray access log file
LIMIT_WINDOW_SECONDS600Rolling window for unique-IP counting
LIMIT_ENFORCE_INTERVAL60How often (seconds) the enforcement job runs

Backups

NexusPanel runs an automatic database backup every day at 03:00 UTC via the backup APScheduler job.

Where backups go

  • Local files: /var/lib/panel/backups/backup_YYYYMMDD_HHMMSS.sqlite3 (or .sql for PostgreSQL)
  • Last 7 backups are kept; older ones are auto-pruned
  • If TELEGRAM_API_TOKEN and TELEGRAM_ADMIN_ID are configured, each backup is also pushed to your Telegram as a document so you have an off-server copy

Manual backup

bash
# SQLite
docker exec nexus-panel cp /var/lib/panel/db.sqlite3 /var/lib/panel/backups/manual.sqlite3

# Or grab the file directly from the host
cp /var/lib/panel/db.sqlite3 ~/panel-backup-$(date +%F).sqlite3

Restoring

  1. Stop the panel: cd /opt/panel && docker compose down
  2. Replace the DB file: cp /path/to/backup.sqlite3 /var/lib/panel/db.sqlite3
  3. Restart: docker compose up -d
Tunables
BACKUP_DIR — where backups are written (default /var/lib/panel/backups)
BACKUP_RETENTION — how many recent backups to keep (default 7)

Encrypted Happ Subscriptions

The dashboard's per-user "H" button generates a real happ://crypt4/<base64> deeplink using RSA-4096 PKCS1v15 with Happ's official public key. Once added to a Happ client, the user cannot view, edit, or share the underlying subscription URL.

Subscription URLs longer than 501 bytes (RSA-4096 + PKCS1v15 limit) automatically fall back to the plain happ://add/<base64> format.

Run Your Business

In plain words
Running the business day-to-day is mostly one loop: someone pays you, you create them a user, you send them the link. Everything below is that loop plus the handful of settings that make it scale past a few dozen customers.

Your First Customer

  1. Dashboard → UsersAdd User.
  2. Give them a username, pick an expiry date and a data limit (or leave both unlimited), and pick which protocols they get.
  3. Save — the panel generates their subscription link immediately.
  4. Send them the link. They paste it into a client app (see the Clients page for recommendations) and they're connected.

Everything here is also available over the API for automation — see the Users API if you want to script account creation from your own store or bot.

Device & IP Limits

Device and IP limits (see how enforcement works) aren't just an anti-abuse tool — they're a pricing lever. A common structure:

PlanDevice limitTypical use
Personal1–2One person, one or two devices
Family / Team4–6Shared across a household or small team, priced higher
Unlimited0 (off)Premium tier, no cap — price accordingly

Set device_limit (or ip_limit) per user when you create or edit them. Customers who exceed it get flipped to limited automatically — you don't have to police it by hand.

Admins & Resellers

If other people sell under you — friends, staff, or sub-resellers — give each one their own admin login instead of sharing yours. See the full breakdown in Admin Management; the short version:

  • Owner (you) — sees everything, manages other admins.
  • Admin — manages users, can't touch panel settings or other admins.
  • Reseller — manages only the users they create, capped by max_users and max_traffic_bytes you set.

This is how you scale past doing every sale yourself: a reseller logs in, creates and bills their own customers, and never sees or touches anyone else's.

Self-Service via Telegram

Once the Telegram bot is connected, customers can check their own usage (/usage), re-fetch their link (/sub), and see their connected devices (/devices) without messaging you. That alone removes most of the "is my VPN still working" support traffic a growing business gets.

Pricing Your Service

NexusPanel doesn't set your prices — what you charge is entirely up to you and your market. As a starting point, most operators think in terms of:

  • Your costs — the panel VPS, any nodes, your NexusPanel license (see tiers & pricing), and bandwidth if your provider charges for it.
  • Your differentiation — more device slots, more node locations, priority support, or just being reliable when competitors aren't.
  • Your market — what similar services charge where your customers are. Bundled multi-month plans (billed like the panel's own 3/6/12-month discounts) are a common way to improve retention.

Collect payment however suits you — manually over Telegram or messaging apps, a payment bot, or your own storefront that calls the Users API to provision accounts automatically once payment clears.

Nodes

What is a Node

In plain words
A node is just another server in another country that your panel controls. Add one in Germany and your customers can connect "through Germany." You manage them all from the same dashboard. You don't need any nodes to start — your first server already serves customers on its own.

A node is a remote server running the Xray core that connects back to your NexusPanel instance. Nodes allow you to distribute proxy endpoints across multiple servers and geographic locations while managing everything from a single dashboard.

The panel communicates with nodes over a secure gRPC connection using mutual TLS. User configurations and traffic data flow through this channel.

Node Install

Dashboard → NodesAdd New Node opens a modal with two tabs — pick whichever fits your access to the target VPS.

Auto install (recommended)

Paste a fresh VPS's IP and SSH login (root password or private key) and the panel does the rest: it SSHes in, installs Docker and the node agent with its own mTLS certificate already baked in, registers the node, and waits until it connects. No commands to copy or run yourself — the SSH password/key is used once and is never stored.

Manual (fallback — panel can't reach the node over SSH)

The Manual tab instead generates a ready-to-paste one-liner with the panel certificate baked in. No manual cert file writing.

  1. Dashboard → NodesAdd New NodeManual tab
  2. Click Copy Install Command — the command includes the cert, port, API port, and panel URL
  3. Paste and run on the node server
  4. Enter the node's IP and ports in the panel → Add Node
bash — example generated command
curl -sL https://nexuspanel.store/install-node | bash -s -- \
  --port 62060 \
  --api-port 62061 \
  --panel-url 'https://panel.example.com:8443' \
  --cert-b64 '<base64-cert>'

The installer waits for apt/dpkg locks automatically — safe to run on a freshly provisioned VPS. Don't drop --panel-url: without it Hysteria 2 auth stays disabled on that node until you set it later.

Certificate & Ports

NexusNode authenticates to the panel using the panel's signing certificate:

  • Panel connection port: 62060
  • Xray API port: 62061
  • Certificate CN: Panel — the node config's ssl_target_name must match
  • The cert is fetched once from GET /api/v1/node/settings (or baked in by the install command) and stored at /var/lib/nexus-panel-node/ssl_client_cert.pem
Firewall
Open TCP 62060 and TCP 62061 inbound on the node server to the panel's IP. Also open any proxy ports (443, 80, etc.) to end users.

Docker Compose for Node

This is the actual compose file the installer generates at /opt/nexus-panel-node/docker-compose.yml — for reference if you're customizing it by hand:

yaml — /opt/nexus-panel-node/docker-compose.yml
services:
  node:
    image: ghcr.io/haitovs/nexus-node:latest
    container_name: nexus-panel-node
    restart: always
    network_mode: host
    environment:
      SERVICE_PORT: 62060
      XRAY_API_PORT: 62061
      SSL_CERT_FILE: /var/lib/nexus-panel-node/ssl_cert.pem
      SSL_KEY_FILE: /var/lib/nexus-panel-node/ssl_key.pem
      SSL_CLIENT_CERT_FILE: /var/lib/nexus-panel-node/ssl_client_cert.pem
      # Hysteria2 sidecar — empty disables it until --panel-url is supplied
      PANEL_HY2_AUTH_URL: "https://panel.example.com:8443/api/v1/hy2-auth"
    volumes:
      - /var/lib/nexus-panel-node:/var/lib/nexus-panel-node
      - /etc/hysteria:/etc/hysteria

network_mode: host means there's no Docker ports: mapping — the node binds every port (service, API, and every Xray/Hysteria inbound end users connect to) directly on the host.

Multiple Nodes

To add nodes across different locations:

  1. Install the node service on each server using the generated one-liner
  2. In the panel, add each node with its public IP and ports
  3. Assign a country flag — drives both the visual grid and the regional subscription reorder
  4. Drag-and-drop to set display order in the grid
  5. Set a usage coefficient per node (e.g., 1.5 means traffic counts 1.5×)
Regional subscription reorder
Subscription links automatically sort by the subscriber's country: closest node first, then same continent, then others. Detected via CF-IPCountry (Cloudflare) or local MaxMind DB. Set country_code on every host row to activate.

Node Troubleshooting

IssueSolution
Node shows "Offline"Check firewall allows TCP 62060 from the panel; verify certificate in /var/lib/nexus-panel-node/ssl_client_cert.pem
Connection refusedEnsure Docker container is running: docker compose ps
Certificate errorRe-copy cert from panel (GET /api/v1/node/settings); verify ssl_target_name = Panel
High latencyCheck network route between panel and node; ensure BBR congestion control + 64 MB socket buffers are set
Users can't connect via nodeVerify proxy ports (443, 80, etc.) are open to end users on the node firewall

API Reference

All API endpoints are under /api/v1/. Enable the interactive Swagger UI by setting DOCS=true and visiting /docs.

Authentication

Obtain a JWT access token by posting credentials:

POST /api/v1/admin/token
bash
curl -X POST https://panel.example.com:8443/api/v1/admin/token \
  -d "username=admin&password=admin&grant_type=password"

# Response:
# {"access_token": "eyJ...", "token_type": "bearer"}

# Use the token in subsequent requests:
curl -H "Authorization: Bearer eyJ..." https://panel.example.com:8443/api/v1/system

If 2FA is enabled for the admin, include the TOTP code in the X-TOTP-Code header.

2FA Endpoints
POST /api/v1/admin/2fa/setup — generate TOTP secret + recovery codes
POST /api/v1/admin/2fa/enable — verify code and activate 2FA
POST /api/v1/admin/2fa/disable — deactivate 2FA

Users

POST /api/v1/user

Create a new user with protocols, data limit, expiry, device limit, and IP limit.

GET /api/v1/users

List all users. Automatically scoped by admin for non-sudo accounts.

GET /api/v1/user/{username}

Get detailed user info including usage stats and subscription links.

PUT /api/v1/user/{username}

Update user fields (data limit, expiry, status, protocols, etc.).

DELETE /api/v1/user/{username}

Permanently delete a user and all associated data.

Bulk Operations

POST /api/v1/users/bulk/update
POST /api/v1/users/bulk/delete
POST /api/v1/users/bulk/reset

Export

GET /api/v1/export/users

Download all users as a CSV file.

GET /api/v1/export/subscription-links

Export all subscription links as plain text.

Admins

POST /api/v1/admin

Create a new admin with role (owner, admin, reseller), max_users, and max_traffic_bytes.

GET /api/v1/admins

List all admin accounts.

Nodes

GET /api/v1/inbounds

List all protocol inbounds.

GET /api/v1/hosts

Get host configurations (sudo only).

Analytics

GET /api/v1/analytics/summary

Dashboard overview statistics.

GET /api/v1/analytics/protocols

Protocol distribution breakdown.

GET /api/v1/analytics/nodes/load

Per-node connection counts and bandwidth.

GET /api/v1/analytics/nodes/uptime

Node uptime percentages.

GET /api/v1/analytics/users/expiring?days=30

Users expiring within the specified number of days.

GET /api/v1/analytics/users/top?limit=10

Top users by bandwidth consumption.

Sessions

GET /api/v1/sessions/active?hours=24

Active device sessions in the last N hours.

GET /api/v1/sessions/user/{username}

Sessions for a specific user.

DELETE /api/v1/sessions/{session_id}

Forcibly disconnect a device session.

System

GET /api/v1/system

System stats including CPU, memory, and bandwidth. Non-sudo admins see zeroed values for sensitive metrics.

GET /api/v1/health

Health check endpoint returning database and Xray core status.

GET /metrics

Prometheus-compatible metrics endpoint. Requires METRICS_ENABLED=true and METRICS_TOKEN for authentication.

Full API Documentation
For complete request/response schemas, enable DOCS=true in your .env and visit http://your-panel/docs for the interactive Swagger UI.

Telegram Bot

Setup

  1. Open Telegram and message @BotFather
  2. Send /newbot and follow the prompts to create your bot
  3. Copy the bot token (e.g., 123456789:AAAA...)
  4. Get your Telegram user ID (message @userinfobot)
  5. Add to your .env:
env
TELEGRAM_API_TOKEN="123456789:AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"
TELEGRAM_ADMIN_ID="987654321"
TELEGRAM_LOGGER_CHANNEL_ID=-1001234567890

Restart the panel after adding the token. The bot will start automatically.

Bot Commands

CommandDescription
/usageCheck data usage and remaining quota
/subGet subscription link and QR code
/statsPanel statistics (admin only)
/devicesView connected devices
/helpList all available commands
/broadcastSend message to all users (admin only)

Notification Settings

The Telegram bot sends notifications for various events when configured. Control each notification type individually via .env variables (see Notifications configuration).

Notifications are sent to:

  • Admin IDs — direct messages to each admin in TELEGRAM_ADMIN_ID
  • Logger channel — all events to the TELEGRAM_LOGGER_CHANNEL_ID

Fields included in Created / Modified alerts

User Created and User Modified notifications include the following fields when set on the user:

FieldShown asWhen included
UsernameUsername: aliceAlways
Traffic limitTraffic Limit: 50 GBAlways (shows "Unlimited" if unset)
Expire dateExpire Date: 2026-06-01Always (shows "Never" if unset)
ProtocolsProxies: vless, trojanAlways
Data limit resetData Limit Reset Strategy: monthlyAlways
Device limitDevice Limit: 3Only when > 0
IP limitIP Limit: 5 (limit)Only when > 0; mode shown inline
HWID limitHWID Limit: 2Only when > 0
Has next planHas Next Plan: TrueAlways
NoteNote: Paid in advance 6moOnly when non-empty; truncated at 120 chars
Discord Integration
Set DISCORD_WEBHOOK_URL to receive the same notifications in a Discord channel. The Discord embeds include the same enriched fields.

Subscription Channels

Deliver sub URLs through infrastructure that censors can't block.

When your panel domain is blocked in Russia, Iran, China, or Turkmenistan, customers can't pull their subscription updates. Subscription channels solve this by publishing each user's config to a static file on Google / Cloudflare / GitHub / Telegram infrastructure — hostnames that censors can't blanket-block without breaking mainstream apps used by millions.

License requirement
Subscription channels require a Pro license tier. The dashboard shows a paywall banner on lower tiers.

How it works

  1. You configure one or more channels in Settings → Subscription channels.
  2. Each user gets a stable public URL on that channel (e.g. https://firebasestorage.googleapis.com/…?alt=media&token=…).
  3. The ⊞ (grid) icon on every user row in the Users table opens a popover with all available URLs — Direct, Happ-encrypted, and every configured channel. Copy or show QR in two clicks.
  4. When you edit Hosts or the Xray config, the panel automatically republishes all active users to Firebase (and other channels) within ~10–30 seconds via the background worker. No manual Backfill needed after routine config edits.

Available channels

ChannelProviderFree tierBest for
Firebase StorageGoogle~50K polls/day on Spark planPrimary anti-censorship channel
Firebase HostingGoogleSame Spark plan as StorageA second Firebase surface (*.web.app) — different SNI and edge CDN on the same project, so it stays reachable when Storage is blocked. Site-wide releases, not per-user, so it republishes in batches rather than on every edit.
Cloudflare R2Cloudflare10 GB/month, no egress feesSecondary; different vendor from Firebase
GitHub GistGitHub / MicrosoftUnlimited public gistsLo-fi fallback; extremely durable
GitLab SnippetGitLabUnlimited public snippetsConfirmed reachable in Turkmenistan even during heavy block windows — a second mirror alongside Firebase for clients who can't reach it
Telegram deliveryTelegramFreeEmergency delivery when everything else is down
Nginx-proxy poolYour VPSesCost of VPSFull operator control over the relay
Test from the panel side, not your laptop
The Test button (refresh icon on each channel row) uploads a real test blob and fetches it back from the panel's outbound network — not from your browser. This matters: your local DNS may be fine while your customers' region blocks the URL. What the test measures is whether the panel server can reach the URL, which is what determines whether the subscription update is delivered.

Firebase Storage

Recommended first channel. Free tier covers ~50K subscription polls/day. Hosted on Google's IP space — censors can't blanket-block without breaking Google Maps, Gmail, and countless other apps.

One-time setup at console.firebase.google.com

  1. Create project — Add project → name it (e.g. nexus-subs) → Spark plan (free) → Create.
  2. Enable Storage — Build → Storage → "Get started" → "Start in production mode" → choose location → Done.
  3. Set storage rules — Storage → Rules → replace with:
firebase rules
rules_version = '2';
service firebase.storage {
  match /b/{bucket}/o {
    match /sub_{file=**} {
      allow read: if true;
      allow write: if false;
    }
  }
}
  1. Generate service account key — Project Settings (⚙) → Service accounts → "Generate new private key" → Download. Treat as a password.
  2. Find bucket name — Storage → top shows gs://your-project.firebasestorage.app. Copy the part after gs://.

In the panel

  1. Settings → Subscription channels → Firebase Storage → ⚙
  2. Paste Bucket name and Service account JSON (entire file contents)
  3. Toggle Enabled, set Priority (lower = preferred; 10 is a good start)
  4. Save → click Test (refresh icon)

Reading the test result

A passing test looks like:

result
firebase: end-to-end OK in 1840ms
✓ creds   (180ms): bucket reachable
✓ upload  (650ms): published to https://firebasestorage.googleapis.com/…
✓ fetch   (820ms): GET 200 (87 bytes, attempt 1)
✓ match     (1ms): content matches
✓ cleanup (180ms): test blob deleted
Step that failedLikely causeFix
credsService account JSON wrong or expiredRegenerate the key in Firebase Console
uploadStorage not enabled or wrong rulesRe-check setup steps 2–3
fetchPublic read rule not appliedRe-paste the rules from step 3
matchEdge cache served stale blob (rare)Usually retries hide this; file a bug if persistent
cleanupService account read-onlyDelete nexus_test_* blobs manually

Apply to existing users

After the test passes, click Backfill at the bottom of the Subscription channels card. This immediately fans out Firebase uploads for all active users via the background worker. For 200 users expect 30–120 seconds to complete.

Stable URLs across plan changes
The Firebase URL never changes for a given user — only the blob content is overwritten. Customers import the URL once into their VPN client and it auto-updates on every plan change without any action on their part.

Cloudflare R2

S3-compatible object storage with no egress fees. Use as a secondary channel alongside Firebase — different vendor means a region-specific block on one doesn't take both down.

Setup at dash.cloudflare.com

  1. R2 (left sidebar) → Create bucket → name it (e.g. nexus-subs).
  2. Open bucket → Settings → Public access → enable. Copy the https://pub-<id>.r2.dev URL.
  3. Top-right of R2 page → Manage R2 API tokens → Create token → Object Read & Write (limit to your bucket) → save Access Key ID + Secret.
  4. Your Account ID is the 32-char hex in the bottom-right of the R2 dashboard page.

In the panel

Settings → Subscription channels → Cloudflare R2 → ⚙:

FieldWhere to find it
Cloudflare account ID32-char hex from step 4
Bucket namee.g. nexus-subs
Access key IDFrom step 3
Secret access keyFrom step 3 (shown once)
Public URL basehttps://pub-<id>.r2.dev from step 2
Don't connect a custom domain to R2
Custom domains get blocked at the DNS layer. The shared pub-<id>.r2.dev hostname benefits from the same anti-block leverage as Firebase — it's shared with thousands of other R2 buckets.

GitHub Gist

Free, GitHub-hosted (Microsoft IPs). Extremely durable — a good low-priority fallback that doesn't cost anything.

Setup

  1. github.com/settings/tokens → Personal access tokens → Tokens (classic) → Generate new token.
  2. Name: nexus-gists. Scope: check gist ONLY. Expiration: 1 year (calendar a renewal).
  3. Copy the ghp_… token — you can't see it again.

In the panel

Settings → Subscription channels → GitHub Gist → ⚙ → paste the PAT → Save → Test.

Telegram Delivery

Reachable in IR/RU/TM during exactly the windows when other channels aren't. This is the "panel is on fire and the user has nothing else" fallback channel.

This is delivery, not auto-update
The user gets a t.me/<bot>?start=sub_<token> deep-link, not a self-updating URL. They click it once, the bot DMs them a .txt file with their config. Set Priority very high (low priority number) only if you've wired the bot's /start handler — otherwise it goes to a bot that doesn't reply.

Setup — dedicated bot (recommended)

  1. Message @BotFather on Telegram → /newbot → pick a name and username (must end in bot).
  2. Copy the token BotFather gives you.
  3. Settings → Subscription channels → Telegram → ⚙:
    • Bot username: without the @
    • Bot token: paste from BotFather
  4. Save → Test. The test calls getMe and asserts the returned username matches what you pasted.

If you already have a TELEGRAM_API_TOKEN set in .env for notification delivery, you can leave the bot token field blank — the channel will fall back to that env var. Not recommended for security: a leaked notification-bot token would also expose subscription files.

Nginx-Proxy Pool

When static-storage channels all go down or get region-specifically blocked, fall back to your own fleet of cheap relay VPSes. Each host in the pool gets its own domain; the panel distributes users across hosts by weight.

Pre-provisioning

Spin up cheap VPSes (Hetzner CCX13 / Contabo / etc — €4–5/month each). On each:

bash
# Run as root on each fresh proxy host
curl -sSL https://your-panel.tld/setup_proxy.sh | bash

In the panel

Settings → Subscription channels → Nginx-proxy → ⚙. Config is JSON:

json
{
  "hosts": [
    { "host": "alpha.shop",  "subscription_path": "sub", "weight": 1 },
    { "host": "beta.shop",   "subscription_path": "sub", "weight": 1 },
    { "host": "gamma.shop",  "subscription_path": "sub", "weight": 5 }
  ]
}

weight: 5 host gets 5× the user share of weight: 1. Save → Test verifies TLS handshake for each pool member.

Subscription Channels — Dashboard UI

Subscription popover (per-user)

Every row in the Users table has a ⊞ (grid) icon. Clicking it opens a popover listing every URL the operator can hand to a customer:

RowWhat it isCopy + QR
DirectPlain /sub/<token> URL served by the panelCopy only
Happ (encrypted)AES-256-CBC encrypted form via /user/<u>/encrypt-subCopy only
Firebase / R2 / GistStatic-storage URLs from enabled channelsCopy + QR
TelegramDeep-link to bot deliveryCopy only
Nginx-proxyRelay URLCopy only

URLs are pre-fetched when the popover opens so Copy is gesture-safe — no async delay between click and clipboard write.

Channel health badges

Each channel row in Settings → Subscription channels shows a health badge from the last probe. The cron runs every 15 minutes. Force a fresh probe at any time with the Test button.

Auto-republish on config changes

Editing Hosts or the Xray core config triggers an automatic fan-out: all active users whose subscription content changed are re-published to every configured channel within ~10–30 seconds. The worker skips users whose rendered config didn't change (content-hash short-circuit), so a host edit that only affects 50 of 200 users causes only 50 Firebase writes.

Backfill

The Backfill button (at the bottom of the Subscription channels card) immediately uploads all active users to all enabled channels. Use it once after adding a new channel — after that, auto-republish keeps everything current.

Hysteria2

Hysteria2 is a QUIC/UDP-based protocol that delivers 3–5× the throughput of TCP on lossy last-mile networks (mobile, CIS 4G, Iran). It runs as a separate daemon alongside Xray — not as an Xray inbound — because Xray-core does not support the hysteria2 protocol natively.

License gate: Standard and above. Trial tier can see hy2 subscription entries but cannot create or manage inbounds.

UDP firewall required
Hysteria2 uses UDP. Before adding a host for a node, ensure the UDP port (e.g., 2053) is open in your hosting provider's control panel — Contabo, Aeza, PTR all gate UDP by default.

Add a Hysteria2 Inbound

Dashboard → SettingsHysteria2Add Inbound

Sudo admin required
Hysteria2 inbounds can only be created and managed by a sudo (super) admin. Non-sudo admins cannot see or edit hy2 inbounds.
FieldValueNotes
Taghy2-mainAny unique name
Listen port2053UDP — must be open in firewall
Obfs typesalamanderRecommended — hides UDP from DPI in CN/IR/RU
Obfs passwordstrong randomopenssl rand -hex 24
Masquerade URLhttps://www.bing.comHTTPS site hysteria impersonates for DPI probes
SNIbing.comTLS SNI presented to clients
TLS cert / keyleave blank for autoPanel auto-generates a 10-year self-signed cert if omitted

Creating the inbound syncs the config to every connected node and spawns the hysteria daemon on each. No SSH required.

Add Hosts per Node

Dashboard → Hosts → click the Hysteria2 inbound card → Add Host

Add one host row per node you want to expose hy2 on:

FieldExampleRequired
RemarkDE Frankfurt hy2Yes
Addressde.example.comYes — node's public domain or IP
Port2053Yes — the UDP port on that node
Country codeDERecommended — drives regional sub reorder

Subscription renderers automatically emit hy2:// entries for every enabled host alongside the existing VLESS/VMess links. Clients see it on the next sub refresh.

Via API (sudo admin credentials required):

bash
# 1. Get a token using your superadmin username and password
TOKEN=$(curl -s -X POST /api/v1/admin/token \
  -d "username=YOUR_ADMIN&password=YOUR_PASSWORD" \
  | jq -r .access_token)

# 2. List hy2 inbounds
curl /api/v1/hy2-inbounds -H "Authorization: Bearer $TOKEN"

# 3. Add a host to inbound id=1
curl -X POST /api/v1/hy2-inbounds/1/hosts \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"remark":"DE Frankfurt","address":"de.example.com","port":2053,"country_code":"DE"}'

Verify & Troubleshoot

After adding a host, fetch a subscription URL — you should see a hy2:// entry alongside the VLESS links. Import into Nekobox, sing-box, or Happ and connect.

IssueDiagnosis
No hy2:// in subCheck the host is enabled and country_code is set; verify license tier is Standard+
UDP connection refusedTest: nc -vu <node> 2053 from outside the DC. Port not open in provider firewall.
UDP timeoutISP or middlebox eating UDP — try obfs salamander or a different port
TLS errorSelf-signed cert: ensure client has allowinsecure: true or supply the cert's fingerprint
Daemon not starting on nodedocker logs nexus-node 2>&1 | grep hysteria on the node server

Middle Server (NAT Relay)

A middle server is a cheap VPS that sits between your users and your nodes. It relays traffic via iptables DNAT, so users connect to one stable IP regardless of which node handles them. Useful when a node IP gets blocked in a country — swap the node, regenerate NAT on the middle server, update one host entry.

Port conventions
TCP inbounds: middle port 50031–50036 → node port 31–36 (matches node id). Hysteria2 UDP inbounds: ports 50041+ assigned per-inbound. All are auto-generated from panel DB state — you never edit these by hand.

Initial Setup

Generate a one-time install command from the panel, then paste it on the fresh VPS as root:

  1. In the panel, go to Settings → Middle-servers and click Generate install command.
  2. Copy the command shown — it looks like:
bash
curl -fsSLk https://panel.example.com:8443/api/v1/middle-server/i/<token> | sudo bash

The token is single-use and expires in 30 minutes. No credentials appear in shell history.

Manual / scripted install (no panel UI access)
bash
read -p "Panel URL: " _P
read -p "Admin username: " _U
read -sp "Admin password: " _W; echo
curl -fsSL -k -u "$_U:$_W" "$_P/api/v1/middle-server/bootstrap.sh" | sudo bash
unset _P _U _W

The script is generated from your current node + Hysteria 2 inbound list, so re-run it any time you add or remove either.

The script will:

  • Install iptables-persistent
  • Apply kernel tuning (BBR, big buffers, conntrack)
  • Build all DNAT rules from current panel DB state
  • Print a table of host entries to add in the panel

After the script completes, go to Hosts page and add one host entry per row the script printed, using the middle server IP and the printed port.

Re-running is safe
The script flushes existing rules before applying new ones. Run it any time you add a node, add a Hysteria2 inbound, or change a node IP.

Swapping to a New Middle Server

When the current middle server is blocked or you want to move to a different VPS:

  1. SSH into the new VPS and run the same single command above
  2. In the panel → Hosts page, edit every host whose address points to the old middle server IP and change it to the new IP. Ports stay the same.
  3. Done — no node changes, no user reconfiguration needed
Don't forget Hysteria2 hosts
If you have Hysteria2 hosts that route through the middle server, update those addresses too.

Fronting & Censorship Resistance

In plain words
Fronting means your customers' traffic looks like it's going to Cloudflare, Amazon, Google, Fastly, or Bunny — not to your VPS. A censor that wants to block you would have to block that entire cloud provider, breaking millions of unrelated sites. That's a cost most networks won't pay, so your traffic slips through.

NexusPanel's IP-Defender manages this for you: it watches every fronted host, detects when one gets blocked, and swaps it for a healthy one — automatically, or with one click. You reach it from Dashboard → Defender, organized as a pipeline:

GroupWhat's there
Start hereOverview — the health map, your default landing page
PipelineSources (rival subscription URLs, for research), ② IP Pool (candidate addresses being validated), ③ Hosts (your live fronted entries — add a Cloudflare-proxied host here directly)
InfrastructureShield (a status board with one card per provider — click a card to drill into that provider's page) and Setup (where you add provider API credentials)

You don't need every provider. Start with one — Cloudflare is the easiest and free — then add more later if you want redundancy or you serve regions with different blocking behavior.

The IP-Defender

The IP-Defender is a background job that continuously checks whether each fronted host still works. It doesn't wait for a customer to complain.

  1. Healthy — the host is probed and responds normally.
  2. Suspect — probes start failing. A single bad probe doesn't trigger anything; a sustained run of failures is required before the defender takes it seriously, so a brief network blip doesn't cause an unnecessary swap.
  3. Blocked — failures persist. The defender picks a healthy replacement address of the same kind (a Cloudflare host only swaps to another Cloudflare address, an AWS host only to another AWS address — mixing kinds breaks the certificate the client expects) and updates it automatically.
  4. Cooldown — after a swap, the old address is left alone for a while before it's eligible for reuse, so a host can't flap back and forth.
You can also rotate manually
Every provider page under Shield has a rotate/swap action of its own (labels vary: Swap front, Rotate account, Rotate IP…). If you already know a host is blocked — a customer reports it, or you tested it yourself — you don't have to wait for the automatic detector.
Fastly and Bunny don't fast-rotate
Cloudflare, AWS, and Google can swap to a fresh address in seconds. Fastly and Bunny can't — if one of their edges gets blocked, you re-create the CDN/wiring with new edge IPs instead. Keep that in mind when picking providers for a market with frequent blocks.

Because per-host traffic isn't something Xray records on its own, the defender relies on active probing rather than bandwidth graphs. That's normal — you'll see "last checked" timestamps on each host, not live traffic counters.

Cloudflare

The easiest provider to start with — the free tier is enough for fronting. Cloudflare edges are shared by an enormous number of ordinary websites, so blocking them collaterally breaks a lot of legitimate traffic, which is why networks are reluctant to do it. Cloudflare does two separate jobs in NexusPanel, reached two different ways:

  1. In Cloudflare, create an API token with DNS-edit permission for the domain you'll front through.
  2. Dashboard → DefenderSetup → add a credential, provider cloudflare, paste the API token.
  3. Fronting actual VPN traffic: Dashboard → DefenderHosts (Pipeline, step 3) → add a host pointed at your Cloudflare-proxied domain (orange-cloud DNS record). No separate distribution to provision — Cloudflare is just DNS + proxy, so the IP-Defender tracks that host like any other.
  4. Keeping the subscription link reachable: Dashboard → DefenderShieldCloudflare card opens a separate page that rotates the domain your subscription links are served from (PRIMARY/STANDBY, with automatic escape if that domain gets blocked). This is a different concern from fronting VPN traffic — it's about keeping customers able to fetch config updates.

AWS CloudFront

Amazon's CDN — a different vendor from Cloudflare, which matters: if you offer both, a network that has blocked one still has to deal with the other separately.

  1. In AWS, create an IAM user with CloudFront + Route 53 permissions and generate an access key.
  2. Dashboard → DefenderSetup → add a credential, provider aws, paste the access key ID and secret.
  3. Dashboard → DefenderShieldAWS CloudFront card to provision a distribution and see its current entry IP and health.

AWS CloudFront billing is usage-based (mostly data-transfer-out) — keep an eye on it if you're pushing serious bandwidth through it.

Google (Cloud Run)

Fronts through Google's run.app infrastructure — the same hostnames used by countless ordinary Cloud Run apps.

  1. In Google Cloud, create a service account with Cloud Run + DNS permissions and download its JSON key.
  2. Dashboard → DefenderSetup → add a credential, provider google, paste the service account key.
  3. Dashboard → DefenderShieldGoogle Cloud Run card to deploy and manage the fronting service.

Cloud Run services must be reachable publicly to work as a front — the panel takes care of that when it provisions the service for you.

Fastly

Fastly is a one-click CDN provider, just like Cloudflare, AWS, and Google — you supply the edge IPs, the panel builds the rest.

  1. In Fastly, generate an API token with global scope (required to create CDN services).
  2. Dashboard → DefenderSetup → add a credential, provider fastly, paste the token.
  3. Dashboard → DefenderShieldFastly card → paste your Fastly edge IPs (one per line or comma-separated) and click Create CDN. The panel stands up a Fastly service configured like your existing setup, then wires the matching inbound and one host per edge IP for you.

Fastly has no fast rotation: if an edge IP gets blocked, re-run Create CDN with new edge IPs rather than waiting for an automatic swap.

Bunny CDN

Bunny works differently from the other providers: instead of the panel creating the CDN for you, you create it on bunny.net and the panel adopts it.

  1. On bunny.net, create one pull zone on the Standard tier with WebSockets enabled.
  2. Add a per-node Edge Rule on that pull zone: Host == bunny-<node>.<your-domain> overriding the Origin URL to http://<node-ip>:2009 (that node's VLESS BUNNY WS inbound). Repeat one rule per node.
  3. Dashboard → DefenderSetup → add a credential, provider bunny, paste your Bunny account API key.
  4. Dashboard → DefenderShieldBunny CDN card → click Auto-wire per node. The panel reads your pull zone's edge rules, matches each origin IP to a panel node, and creates one managed Bunny host per matched node. Re-run any time you add a node or fix a rule.
  5. Click Refresh edge IPs to seed Bunny's published edge server IPs into the rotation pool.

Like Fastly, Bunny has no fast rotation — a blocked edge means fixing the pull zone and re-running auto-wire, not an instant swap.

Azure

Azure isn't a CDN front like the others — it provisions a rotatable relay VM (the same idea as a Middle Server, but Azure spins the VM up and swaps its IP for you) so a blocked node can get a fresh entry IP without touching the node itself.

  1. Create a Service Principal: az ad sp create-for-rbac --role Contributor --scopes /subscriptions/<id>.
  2. Dashboard → DefenderShieldAzure card → paste the tenant ID, client ID, client secret, and subscription ID, then click Verify & connect. Azure has its own credential form here — it doesn't go through Setup like the other providers.
  3. Pick a region (the panel suggests one detected from your subscriptions) and click Create relay — this provisions a small Ubuntu VM with a static IP, cheapest size chosen automatically.
  4. Install the middle agent on the returned IP and promote it, same as any middle server (see Middle Server → Initial Setup).

If the relay's IP gets blocked later, click Rotate IP on the Azure page, then re-run the middle-server install on the new IP and re-test from the censored network.

Migration from Marzban

In plain words
Already running Marzban? This moves everything over to NexusPanel — your customers, settings, and even their existing links — with one command. Your customers do nothing and notice nothing. It previews every change first and can be undone right up until the final step, so it's safe to try.

The nexus cli migrate tool moves a live Marzban installation to NexusPanel with zero end-user reconfiguration. It runs on the same host as Nexus, reads Marzban's data directory directly, and uses a 9-stage atomic state machine with full rollback until the finalize command.

JWT compatibility
The migrate tool extracts Marzban's JWT_SECRET_KEY and stores it as MARZBAN_LEGACY_JWT_SECRET in Nexus's env. Every existing Marzban subscription URL keeps working on day one — users never re-import anything.

Prerequisites

  • Marzban version 0.6.0–0.8.4 (official install script, marzban or marzban_cli)
  • NexusPanel installed on the same host, or able to read /var/lib/marzban/
  • Free disk for a snapshot of Marzban's SQLite DB

Step 1: Dry Run

Always dry-run first. It snapshots Marzban's DB, replays every import into a scratch copy, and finishes in seconds. Nothing is written to Nexus or Marzban.

bash
# Inspect what was found
nexus cli migrate discover

# Rehearse: snapshot + import + verify on scratch DB, no side effects
nexus cli migrate run --dry-run

Read the dry-run report at /var/lib/nexus/migration/dryrun-<ts>.json. Confirm user count, admin list, and that MARZBAN_LEGACY_JWT_SECRET was extracted. Fix any flagged errors before proceeding.

Step 2: Live Cutover

bash
# Live run — stops Marzban, imports, restarts Nexus
nexus cli migrate run --yes

The critical path (MARZBAN_STOP → NEXUS_RESTART) takes ~15–30 seconds. Node VPN traffic continues uninterrupted — nodes run independently of the panel. Only the subscription URL endpoint is briefly unavailable.

If VERIFY fails, auto-rollback fires: Nexus configs are restored and Marzban is restarted. Check docker logs nexus-panel --tail 200 for the root cause, then re-run.

bash
# After watching prod for a few hours:
nexus cli migrate finalize    # frees snapshot, closes the run

# If you need to undo (pre-finalize only):
nexus cli migrate rollback

What Migrates

DataMigratedNotes
Users (username, data, expiry)YesAll profiles, quotas, UUIDs preserved
User proxies / protocolsYesVMess, VLESS, Trojan, Shadowsocks
Admin accountsYesPasswords carried over
Hosts (proxy endpoints)YesAll host rows copied, Nexus-only fields default to off
Xray inboundsYesCopied from Marzban's xray_config.json
Telegram bot token, NOTIFY_* flagsYesWritten to Nexus .env
JWT secret (sub URL compat)YesStored as MARZBAN_LEGACY_JWT_SECRET — existing sub URLs keep working
Notification reminder historyYesPrevents re-firing "expires in 3 days" alerts
Node configurationsNoNexus uses ports 62060/62061; re-add nodes via dashboard with a fresh cert
Xray routing/dns/outboundsNoInbounds only; paste custom blocks into Settings → Core editor after migration
Hysteria2 hostsNoMarzban has no hy2 — add via Dashboard → Hosts after migration

Post-Migration Checklist

After nexus cli migrate run --yes completes, the CLI prints a table of migrated hosts. Verify them and then:

  1. Test a legacy Marzban subscription URL — it must return a valid config (JWT compat check)
  2. Check user count: docker exec nexus-panel sqlite3 /var/lib/panel/db.sqlite3 'SELECT COUNT(*) FROM users;'
  3. Run nexus cli migrate post-cutover to scan for stale Marzban daemons (marzguard, certbot cron hooks)
  4. If you use Hysteria2: add inbound + one host per node (see the Hysteria2 section)
  5. Re-add nodes via Dashboard → Nodes (new cert, ports 62060/62061)
  6. Run nexus cli migrate finalize to free the snapshot once stable
bash — quick verify
# Health
curl -sk https://<your-domain>/api/v1/health

# Legacy sub URL must return 200 with config content
curl -sk "https://<your-domain>/sub/<marzban-token>" | head -c 200

# Scan for Marzban leftovers
nexus cli migrate post-cutover

Migration from Remnawave

In plain words
Already running Remnawave? One command imports your users into NexusPanel — traffic limits, expiry, and their protocol credentials all carry over, and their existing subscription links keep working. It's a dry run by default, so nothing is written until you explicitly tell it to run.

The nexus cli migrate remnawave tool connects to a live Remnawave panel over its REST API (no direct database access needed) and imports its users into NexusPanel.

Prerequisites

  • The Remnawave panel URL
  • An admin username and password on that panel
bash — dry run (default)
# Connects, extracts, and reports what would be imported — writes nothing
nexus cli migrate remnawave run --url https://your-remnawave-panel --username ADMIN --password ...

The report lists exactly what would be imported, plus any username collisions with existing NexusPanel accounts. Review it, then add --run to actually apply the migration:

bash — live run
nexus cli migrate remnawave run --url https://your-remnawave-panel --username ADMIN --password ... --run

Useful flags:

  • --yes — only required if a username already exists in NexusPanel; without it, a live run refuses rather than overwrite anything.
  • --insecure — skip TLS verification, for Remnawave panels on self-signed certificates.
  • --page-size — tunes how many users are fetched per API page (default 250).
Subscription links keep working
Remnawave subscription links use an opaque short ID. NexusPanel stores that same ID as an alias during import, so a customer's existing /sub/ link resolves after migration with zero client reconfiguration.

Changed your mind? nexus cli migrate remnawave rollback deletes exactly the users and aliases that run created, leaving any pre-existing NexusPanel data untouched.

What Migrates

DataMigratedNotes
Users, traffic limits, used traffic, expiry, statusYes 
Per-protocol credentialsYesVLESS UUID, Trojan password, Shadowsocks password preserved
Subscription URLYesOpaque short ID stored as an alias — existing links keep resolving
Telegram IDNoNexusPanel users have no Telegram field; reported as a warning during import
Hosts / nodesNoWire up NexusPanel's own inbounds — migrated users' preserved credentials work against them

Security

Two-Factor Authentication (2FA)

NexusPanel supports TOTP-based 2FA (compatible with Google Authenticator, Authy, etc.):

  1. Navigate to Settings in the dashboard
  2. Click Enable 2FA
  3. Scan the QR code with your authenticator app
  4. Enter the 6-digit code to confirm
  5. Save recovery codes in a secure location

Via API:

bash
# Generate TOTP secret and recovery codes
curl -X POST /api/v1/admin/2fa/setup -H "Authorization: Bearer TOKEN"

# Activate 2FA (provide TOTP code to verify)
curl -X POST /api/v1/admin/2fa/enable \
  -H "Authorization: Bearer TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"code": "123456"}'

# Login with 2FA
curl -X POST /api/v1/admin/token \
  -H "X-TOTP-Code: 123456" \
  -d "username=admin&password=admin&grant_type=password"

Captcha Protection

Protect the login page against brute-force attacks with captcha:

Cloudflare Turnstile

env
CAPTCHA_PROVIDER="turnstile"
TURNSTILE_SITE_KEY="0x4AAAAAAA..."
TURNSTILE_SECRET_KEY="0x4AAAAAAA..."

Built-in Captcha

env
CAPTCHA_PROVIDER="builtin"

The built-in captcha requires no external services and generates simple math challenges.

Rate Limiting

Login endpoint rate limiting is enabled by default:

env
LOGIN_RATE_LIMIT="10/minute"
LOGIN_LOCKOUT_THRESHOLD=10
LOGIN_LOCKOUT_DURATION_MINUTES=30

After 10 failed attempts, the IP is locked out for 30 minutes. The rate limiter is in-memory (per-process) and resets on server restart.

SSL / TLS

For production deployments, always use HTTPS. Options include:

  • Direct SSL — set UVICORN_SSL_CERTFILE and UVICORN_SSL_KEYFILE
  • Reverse proxy — use Nginx or Caddy in front with SSL termination
  • Cloudflare — proxy through Cloudflare with Full (Strict) SSL mode

Nginx Reverse Proxy Example

nginx
server {
    listen 443 ssl http2;
    server_name panel.example.com;

    ssl_certificate     /etc/letsencrypt/live/panel.example.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/panel.example.com/privkey.pem;

    location / {
        proxy_pass http://127.0.0.1:8000;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
    }
}

FAQ

How to Change Admin Password

Option 1: Update the SUDO_PASSWORD environment variable and restart the panel.

Option 2: Use the API:

bash
curl -X PUT /api/v1/admin/admin \
  -H "Authorization: Bearer TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"password": "newSecurePassword123"}'

How to Backup

SQLite

bash
# Stop the panel first for a clean backup
docker compose stop panel
cp /var/lib/nexuspanel/db.sqlite3 /backups/db-$(date +%Y%m%d).sqlite3
docker compose start panel

# Or use SQLite online backup (no downtime)
sqlite3 /var/lib/nexuspanel/db.sqlite3 ".backup /backups/db-$(date +%Y%m%d).sqlite3"

PostgreSQL

bash
docker compose exec db pg_dump -U nexus nexuspanel > /backups/db-$(date +%Y%m%d).sql
Tip
Also backup your .env, xray_config.json, and any custom templates.

How to Update

bash
cd /opt/nexuspanel

# Pull latest images
docker compose pull

# Restart with new version
docker compose up -d

# Check logs for migration status
docker compose logs -f panel

Database migrations run automatically on startup. Always backup your database before updating.

How to Add Custom Templates

Custom templates let you control subscription output for various clients:

  1. Create your template files in the templates directory:
bash
mkdir -p /var/lib/nexuspanel/templates/clash
nano /var/lib/nexuspanel/templates/clash/custom.yml
  1. Reference the template in .env:
env
CUSTOM_TEMPLATES_DIRECTORY="/var/lib/panel/templates/"
CLASH_SUBSCRIPTION_TEMPLATE="clash/custom.yml"

Templates support Jinja2 syntax with access to user data, proxy configs, and panel settings.

Subscription Page Customization

The user-facing subscription page (shown when visiting a subscription link in a browser) is fully customizable:

  1. Copy the default template as a starting point:
bash
cp -r /opt/nexuspanel/app/templates/subscription \
  /var/lib/nexuspanel/templates/subscription
  1. Edit /var/lib/nexuspanel/templates/subscription/index.html
  2. Set in .env:
env
SUBSCRIPTION_PAGE_TEMPLATE="subscription/index.html"

Available template variables include: user, sub_url, clash_url, singbox_url, usage, expire_date, and brand_name.


NexusPanel Documentation — Built with care.