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:
The owner
You run the business, set prices, and add customers. You do not need to be a programmer.
Your shop building
A computer you rent in a data center (≈ $5/month). Your panel lives here. Providers: Hetzner, Contabo, DigitalOcean…
Your till + shelves
The control panel you log into in a browser. Add customers, watch traffic, get paid — all from here.
A branch abroad
An extra server in, say, Germany or Finland so customers can choose where to connect. Optional — start with none.
The membership card
One web link you hand each customer. It carries all their connection settings — that's all they ever need.
The customer's door
A free app (Happ, v2rayNG, Streisand…). They paste the link once, tap connect, done. You never touch their phone.
How the money actually flows
- Someone wants private or unblocked internet and pays you (collect crypto automatically through the built-in Telegram bot, or take payment however you like).
- You open NexusPanel and create a user for them — set how long it lasts and how much data they get. Takes about 10 seconds.
- You send them their subscription link.
- They paste it into a free app and tap connect. They're online through your server.
- Next month they pay again to stay active. Repeat with as many customers as you want.
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.comthat 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.
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.
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.
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.
See install command ↓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.
Migration guide ↓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.
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:
- Check the panel logs:
cd /opt/panel && docker compose logs --tail 100 - Read the relevant section of these docs (sidebar on the left)
- Message us on Telegram — link in the bot, response time is hours not days
What is NexusPanel
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
.sobinaries - In-app actionable notifications — expiring users, data caps, offline nodes, license expiry
Requirements
| Component | Minimum | Recommended |
|---|---|---|
| OS | Ubuntu 20.04+ / Debian 11+ | Ubuntu 22.04 LTS |
| RAM | 1 GB | 2 GB+ |
| CPU | 1 vCPU | 2 vCPU |
| Disk | 10 GB | 20 GB+ (SSD) |
| Docker | 20.10+ | Latest stable |
| Domain | Optional | Recommended (for SSL) |
Quick Install
Run this single command on a fresh VPS to install NexusPanel with default settings:
curl -sL https://nexuspanel.store/install | bash
The script will prompt you for:
- 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)
- Domain — for SSL via Let's Encrypt (skip for IP-only)
- Admin username & password — for the dashboard
- Panel port — defaults to 8443
Then it will:
- Install Docker & Docker Compose if missing
- Pull
ghcr.io/haitovs/nexus:latest(Cython-protected production image) - Create
/opt/panel/with.envanddocker-compose.yml(container name:nexus-panel) - Seed
xray_config.jsonwith access log enabled (required for IP/device limit enforcement) - Start the panel and print dashboard URL + credentials
nexus update on the host.
One-Command Install (Detailed)
The install script accepts optional flags to customize the setup:
# 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).
First Steps in the Panel
Once you're logged in, here's the fastest path to your first working connection:
- Create a user — Dashboard → Users → Add User. Set an expiry date and data limit, then copy their subscription link and hand it to a client app (Happ, v2rayNG, Streisand…).
- Add a node (optional) — Dashboard → Nodes → Add 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.
- Set your subscription domain — if you're serving subscription links from a different host/domain than the admin panel itself, set
XRAY_SUBSCRIPTION_URL_PREFIXin 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:
# 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:
# 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:
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:
# 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:
# From your install dir (e.g. /opt/panel)
cp docker-compose.modern.yml docker-compose.yml
docker compose up -d
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
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.
.env.example to .env and uncomment the variables you need. All variables have sensible defaults.
Server
| Variable | Default | Description |
|---|---|---|
UVICORN_HOST | 0.0.0.0 | Bind address for the server |
UVICORN_PORT | 8000 | HTTP port |
UVICORN_UDS | — | Unix domain socket path (overrides host/port) |
UVICORN_SSL_CERTFILE | — | Path to SSL certificate (fullchain.pem) |
UVICORN_SSL_KEYFILE | — | Path to SSL private key |
UVICORN_SSL_CA_TYPE | public | CA type: public or private |
DASHBOARD_PATH | /dashboard/ | URL path for the web dashboard |
ALLOWED_ORIGINS | — | Comma-separated CORS origins |
SUDO_USERNAME | — | Initial super admin username |
SUDO_PASSWORD | — | Initial super admin password |
JWT_ACCESS_TOKEN_EXPIRE_MINUTES | 1440 | Token expiration in minutes (default 24h) |
Database
| Variable | Default | Description |
|---|---|---|
SQLALCHEMY_DATABASE_URL | sqlite:///db.sqlite3 | Database connection string |
SQLALCHEMY_POOL_SIZE | 10 | Connection pool size |
SQLIALCHEMY_MAX_OVERFLOW | 30 | Max connections above pool size |
BACKEND_MODE | classic | classic (SQLite/Postgres, default) or modern (adds Redis-backed event queue) |
REDIS_URL | — | Redis connection string; required when BACKEND_MODE=modern |
postgresql+asyncpg://user:pass@host:5432/dbname for async PostgreSQL.
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
| Variable | Default | Description |
|---|---|---|
XRAY_JSON | xray_config.json | Path to Xray core configuration |
XRAY_EXECUTABLE_PATH | /usr/local/bin/xray | Path to Xray binary |
XRAY_ASSETS_PATH | /usr/local/share/xray | Path to geoip.dat and geosite.dat |
XRAY_SUBSCRIPTION_URL_PREFIX | — | Public 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_PATH | sub | URL path segment for subscriptions |
XRAY_EXCLUDE_INBOUND_TAGS | — | Space-separated inbound tags to exclude |
XRAY_FALLBACKS_INBOUND_TAG | — | Inbound tag used for fallback routing |
Subscription
| Variable | Default | Description |
|---|---|---|
SUB_PROFILE_TITLE | Subscription | Display name shown in client apps |
SUB_SUPPORT_URL | — | Support link included in subscription info |
SUB_UPDATE_INTERVAL | 12 | Client auto-update interval (hours) |
EXTERNAL_CONFIG | — | External config URL for client integration |
USE_CUSTOM_JSON_DEFAULT | false | Enable custom JSON config for default client |
USE_CUSTOM_JSON_FOR_V2RAYN | false | Enable custom JSON for V2RayN |
USE_CUSTOM_JSON_FOR_V2RAYNG | false | Enable custom JSON for V2RayNG |
USE_CUSTOM_JSON_FOR_STREISAND | false | Enable custom JSON for Streisand |
USE_CUSTOM_JSON_FOR_HAPP | false | Enable custom JSON for Happ |
SUB_RATE_LIMIT_PER_MINUTE | 60 | Max subscription fetches per IP per minute (in-process, resets on restart) |
SUB_ENABLE_ETAG | true | Return ETag / honour If-None-Match to save bandwidth on unchanged subs |
SUB_GZIP_MIN_SIZE | 512 | Gzip-compress subscription responses larger than this many bytes |
Templates
| Variable | Default | Description |
|---|---|---|
CUSTOM_TEMPLATES_DIRECTORY | /var/lib/panel/templates/ | Base directory for custom templates |
SUBSCRIPTION_PAGE_TEMPLATE | subscription/index.html | Template for the user subscription page |
HOME_PAGE_TEMPLATE | home/index.html | Template for the panel home page |
CLASH_SUBSCRIPTION_TEMPLATE | clash/default.yml | Clash subscription template |
CLASH_SETTINGS_TEMPLATE | clash/settings.yml | Clash settings template |
V2RAY_SUBSCRIPTION_TEMPLATE | v2ray/default.json | V2Ray subscription template |
V2RAY_SETTINGS_TEMPLATE | v2ray/settings.json | V2Ray settings template |
SINGBOX_SUBSCRIPTION_TEMPLATE | singbox/default.json | Sing-box subscription template |
SINGBOX_SETTINGS_TEMPLATE | singbox/settings.json | Sing-box settings template |
MUX_TEMPLATE | mux/default.json | Multiplex config template |
USER_AGENT_TEMPLATE | user_agent/default.json | User-agent parsing template |
GRPC_USER_AGENT_TEMPLATE | user_agent/grpc.json | gRPC user-agent template |
Telegram
| Variable | Default | Description |
|---|---|---|
TELEGRAM_API_TOKEN | — | Bot token from @BotFather |
TELEGRAM_ADMIN_ID | — | Comma-separated Telegram user IDs for admins |
TELEGRAM_LOGGER_CHANNEL_ID | — | Channel ID for log messages |
TELEGRAM_DEFAULT_VLESS_FLOW | xtls-rprx-vision | Default VLESS flow for bot-created users |
TELEGRAM_PROXY_URL | — | Proxy URL for Telegram API connections |
Notifications
| Variable | Default | Description |
|---|---|---|
NOTIFY_STATUS_CHANGE | true | Notify when user status changes |
NOTIFY_USER_CREATED | true | Notify on new user creation |
NOTIFY_USER_UPDATED | true | Notify on user modification |
NOTIFY_USER_DELETED | true | Notify on user deletion |
NOTIFY_USER_DATA_USED_RESET | true | Notify on usage reset |
NOTIFY_USER_SUB_REVOKED | true | Notify on subscription revocation |
NOTIFY_IF_DATA_USAGE_PERCENT_REACHED | true | Notify when data threshold reached |
NOTIFY_IF_DAYS_LEFT_REACHED | true | Notify when expiry threshold reached |
NOTIFY_LOGIN | true | Notify on admin login |
LOGIN_NOTIFY_WHITE_LIST | — | IPs to exclude from login notifications |
NOTIFY_DAYS_LEFT | 3,7 | Days-left thresholds for notifications |
NOTIFY_REACHED_USAGE_PERCENT | 80,90 | Usage percent thresholds |
RECURRENT_NOTIFICATIONS_TIMEOUT | 180 | Minutes between repeat notifications |
NUMBER_OF_RECURRENT_NOTIFICATIONS | 3 | Max repeat notifications per event |
DISCORD_WEBHOOK_URL | — | Discord webhook for Telegram-style notifications |
WEBHOOK_ADDRESS | — | Legacy: comma-separated static webhook URLs. Prefer the dashboard Webhooks UI for new setups. |
WEBHOOK_SECRET | — | Legacy: HMAC secret for WEBHOOK_ADDRESS delivery. Dashboard webhooks manage secrets per-endpoint. |
Branding (White-Label)
| Variable | Default | Description |
|---|---|---|
BRAND_NAME | Panel | Panel name displayed in UI and emails |
BRAND_LOGO_URL | — | URL to custom logo image |
BRAND_FAVICON_URL | — | URL to custom favicon |
Security
| Variable | Default | Description |
|---|---|---|
CAPTCHA_PROVIDER | disabled | Captcha provider: disabled, turnstile, or builtin |
TURNSTILE_SITE_KEY | — | Cloudflare Turnstile site key |
TURNSTILE_SECRET_KEY | — | Cloudflare Turnstile secret key |
LOGIN_RATE_LIMIT | 10/minute | Max login attempts per window |
LOGIN_LOCKOUT_THRESHOLD | 10 | Failed attempts before lockout |
LOGIN_LOCKOUT_DURATION_MINUTES | 30 | Lockout duration in minutes |
Logging
| Variable | Default | Description |
|---|---|---|
LOG_LEVEL | INFO | Log level: DEBUG, INFO, WARNING, ERROR |
LOG_FORMAT | text | Log format: text or json |
LOG_FILE_PATH | — | Write logs to file (in addition to stdout) |
LOG_MAX_SIZE_MB | 10 | Max log file size before rotation |
LOG_BACKUP_COUNT | 5 | Number of rotated log files to keep |
Metrics (Prometheus)
| Variable | Default | Description |
|---|---|---|
METRICS_ENABLED | false | Enable Prometheus /metrics endpoint |
METRICS_TOKEN | — | Bearer token required to scrape metrics |
Additional Variables
| Variable | Default | Description |
|---|---|---|
ACTIVE_STATUS_TEXT | Active | Custom label for active status |
EXPIRED_STATUS_TEXT | Expired | Custom label for expired status |
LIMITED_STATUS_TEXT | Limited | Custom label for limited status |
DISABLED_STATUS_TEXT | Disabled | Custom label for disabled status |
ONHOLD_STATUS_TEXT | On-Hold | Custom label for on-hold status |
USERS_AUTODELETE_DAYS | -1 | Auto-delete expired users after N days (-1 = disabled) |
USER_AUTODELETE_INCLUDE_LIMITED_ACCOUNTS | false | Include data-limited users in auto-delete |
JOB_CORE_HEALTH_CHECK_INTERVAL | 10 | Health check interval (seconds) |
JOB_RECORD_NODE_USAGES_INTERVAL | 30 | Node usage recording interval |
JOB_RECORD_USER_USAGES_INTERVAL | 10 | User usage recording interval |
JOB_REVIEW_USERS_INTERVAL | 10 | User review/expire check interval |
JOB_SEND_NOTIFICATIONS_INTERVAL | 30 | Notification dispatch interval |
DISABLE_RECORDING_NODE_USAGE | false | Disable node usage recording |
DEBUG | false | Enable debug mode with hot-reload |
DOCS | false | Enable 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:
| Field | Purpose |
|---|---|
| Remark | Display name shown in client apps |
| Address | Server domain or IP the client connects to |
| Port | Override the inbound's listen port |
| SNI / Host | TLS Server Name Indication and HTTP Host header |
| Security / ALPN / Fingerprint | TLS profile: none / tls / reality; h2/http1.1; Chrome/Firefox/Safari uTLS |
| Allow Insecure | Skip TLS cert verification (use only behind CDN where cert isn't exposed) |
| Country code | ISO 3166-1 alpha-2 — drives regional subscription reorder |
| Allowed / Denied Admins | Restrict 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:
[{"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:
| Role | Capabilities |
|---|---|
| Owner | Full access: manage admins, nodes, system settings, all users |
| Admin | Manage users (all), view nodes and analytics, limited settings |
| Reseller | Manage 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 createmax_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.
# 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.
# 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 field | Operators | Actions |
|---|---|---|
user_agent | equals / contains / regex | template / status / headers |
client_os | equals / contains / regex | template / status / headers |
Examples:
- Match
user_agent contains "Happ"→ actiontemplate = happ-custom— serve a Happ-optimised template to Happ clients - Match
client_os equals "iOS"→ actionheaders = {"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.
# 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
| Scope | Events |
|---|---|
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.
# 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"]
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.
| Client | Platforms | Notes |
|---|---|---|
| Happ | iOS / macOS / Windows / Android | Recommended — native sub URL, HWID binding, offline cache |
| v2RayTun | iOS / macOS / Android | Popular iOS client, VLESS-Reality support |
| Karing | All platforms | Sing-box-based, strong cross-platform story |
| Shadowrocket | iOS | $2.99 US App Store — rock-solid iOS |
| V2rayNG | Android | Classic Android client |
| FlClashX | Windows / macOS / Linux / Android | Mihomo/Clash-compatible |
| Streisand | iOS / macOS | Supports 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/validateon the license server with itslicense_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_availableis true andAUTO_UPDATEis enabled (default), the panel runsdocker compose pull && docker compose up -d --force-recreatein the background — you don't have to do anything.
Tiers
| Tier | Price | Users | Nodes | Duration |
|---|---|---|---|---|
| Trial (Pro Trial) | Free | Unlimited | Unlimited | 14 days |
| Standard | $10/mo | Unlimited | 10 | 30 days/mo |
| Pro | $30/mo | Unlimited | Unlimited | 30 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
| Feature | Trial | Standard | Pro |
|---|---|---|---|
| Max users | Unlimited | Unlimited | Unlimited |
| Max nodes | Unlimited | 10 | Unlimited |
| Duration | 14 days | 30 days/mo | 30 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
- Xray writes one line to
$XRAY_ACCESS_LOGfor every accepted connection. - The
enforce_limitsjob runs every 60 seconds, tails the log (offset-tracked, rotation-aware), and extracts(user_id, client_ip)pairs from the lastLIMIT_WINDOW_SECONDS(default 600 = 10 minutes). - For each user, unique IPs are counted. If the count exceeds
ip_limit(ordevice_limitif noip_limitis set) and the user is currentlyactiveandip_limit_mode == "limit", the user is flipped to statuslimited. - All seen IPs are written to
user_ip_history. View per-user IPs viaGET /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:
{
"log": {
"loglevel": "warning",
"access": "/var/lib/panel/xray-access.log"
}
}
Tunables
| Env Var | Default | Purpose |
|---|---|---|
XRAY_ACCESS_LOG | /var/log/xray/access.log | Path to the Xray access log file |
LIMIT_WINDOW_SECONDS | 600 | Rolling window for unique-IP counting |
LIMIT_ENFORCE_INTERVAL | 60 | How 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.sqlfor PostgreSQL) - Last 7 backups are kept; older ones are auto-pruned
- If
TELEGRAM_API_TOKENandTELEGRAM_ADMIN_IDare configured, each backup is also pushed to your Telegram as a document so you have an off-server copy
Manual backup
# 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
- Stop the panel:
cd /opt/panel && docker compose down - Replace the DB file:
cp /path/to/backup.sqlite3 /var/lib/panel/db.sqlite3 - Restart:
docker compose up -d
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
Your First Customer
- Dashboard → Users → Add User.
- Give them a username, pick an expiry date and a data limit (or leave both unlimited), and pick which protocols they get.
- Save — the panel generates their subscription link immediately.
- 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:
| Plan | Device limit | Typical use |
|---|---|---|
| Personal | 1–2 | One person, one or two devices |
| Family / Team | 4–6 | Shared across a household or small team, priced higher |
| Unlimited | 0 (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_usersandmax_traffic_bytesyou 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
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 → Nodes → Add 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.
- Dashboard → Nodes → Add New Node → Manual tab
- Click Copy Install Command — the command includes the cert, port, API port, and panel URL
- Paste and run on the node server
- Enter the node's IP and ports in the panel → Add Node
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'sssl_target_namemust 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
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:
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:
- Install the node service on each server using the generated one-liner
- In the panel, add each node with its public IP and ports
- Assign a country flag — drives both the visual grid and the regional subscription reorder
- Drag-and-drop to set display order in the grid
- Set a usage coefficient per node (e.g.,
1.5means traffic counts 1.5×)
CF-IPCountry (Cloudflare) or local MaxMind DB. Set country_code on every host row to activate.
Node Troubleshooting
| Issue | Solution |
|---|---|
| Node shows "Offline" | Check firewall allows TCP 62060 from the panel; verify certificate in /var/lib/nexus-panel-node/ssl_client_cert.pem |
| Connection refused | Ensure Docker container is running: docker compose ps |
| Certificate error | Re-copy cert from panel (GET /api/v1/node/settings); verify ssl_target_name = Panel |
| High latency | Check network route between panel and node; ensure BBR congestion control + 64 MB socket buffers are set |
| Users can't connect via node | Verify 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:
/api/v1/admin/tokencurl -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.
POST /api/v1/admin/2fa/setup — generate TOTP secret + recovery codesPOST /api/v1/admin/2fa/enable — verify code and activate 2FAPOST /api/v1/admin/2fa/disable — deactivate 2FA
Users
/api/v1/userCreate a new user with protocols, data limit, expiry, device limit, and IP limit.
/api/v1/usersList all users. Automatically scoped by admin for non-sudo accounts.
/api/v1/user/{username}Get detailed user info including usage stats and subscription links.
/api/v1/user/{username}Update user fields (data limit, expiry, status, protocols, etc.).
/api/v1/user/{username}Permanently delete a user and all associated data.
Bulk Operations
/api/v1/users/bulk/update/api/v1/users/bulk/delete/api/v1/users/bulk/resetExport
/api/v1/export/usersDownload all users as a CSV file.
/api/v1/export/subscription-linksExport all subscription links as plain text.
Admins
/api/v1/adminCreate a new admin with role (owner, admin, reseller), max_users, and max_traffic_bytes.
/api/v1/adminsList all admin accounts.
Nodes
/api/v1/inboundsList all protocol inbounds.
/api/v1/hostsGet host configurations (sudo only).
Analytics
/api/v1/analytics/summaryDashboard overview statistics.
/api/v1/analytics/protocolsProtocol distribution breakdown.
/api/v1/analytics/nodes/loadPer-node connection counts and bandwidth.
/api/v1/analytics/nodes/uptimeNode uptime percentages.
/api/v1/analytics/users/expiring?days=30Users expiring within the specified number of days.
/api/v1/analytics/users/top?limit=10Top users by bandwidth consumption.
Sessions
/api/v1/sessions/active?hours=24Active device sessions in the last N hours.
/api/v1/sessions/user/{username}Sessions for a specific user.
/api/v1/sessions/{session_id}Forcibly disconnect a device session.
System
/api/v1/systemSystem stats including CPU, memory, and bandwidth. Non-sudo admins see zeroed values for sensitive metrics.
/api/v1/healthHealth check endpoint returning database and Xray core status.
/metricsPrometheus-compatible metrics endpoint. Requires METRICS_ENABLED=true and METRICS_TOKEN for authentication.
DOCS=true in your .env and visit http://your-panel/docs for the interactive Swagger UI.
Telegram Bot
Setup
- Open Telegram and message @BotFather
- Send
/newbotand follow the prompts to create your bot - Copy the bot token (e.g.,
123456789:AAAA...) - Get your Telegram user ID (message @userinfobot)
- Add to your
.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
| Command | Description |
|---|---|
/usage | Check data usage and remaining quota |
/sub | Get subscription link and QR code |
/stats | Panel statistics (admin only) |
/devices | View connected devices |
/help | List all available commands |
/broadcast | Send 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:
| Field | Shown as | When included |
|---|---|---|
| Username | Username: alice | Always |
| Traffic limit | Traffic Limit: 50 GB | Always (shows "Unlimited" if unset) |
| Expire date | Expire Date: 2026-06-01 | Always (shows "Never" if unset) |
| Protocols | Proxies: vless, trojan | Always |
| Data limit reset | Data Limit Reset Strategy: monthly | Always |
| Device limit | Device Limit: 3 | Only when > 0 |
| IP limit | IP Limit: 5 (limit) | Only when > 0; mode shown inline |
| HWID limit | HWID Limit: 2 | Only when > 0 |
| Has next plan | Has Next Plan: True | Always |
| Note | Note: Paid in advance 6mo | Only when non-empty; truncated at 120 chars |
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.
How it works
- You configure one or more channels in Settings → Subscription channels.
- Each user gets a stable public URL on that channel (e.g.
https://firebasestorage.googleapis.com/…?alt=media&token=…). - 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.
- 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
| Channel | Provider | Free tier | Best for |
|---|---|---|---|
| Firebase Storage | ~50K polls/day on Spark plan | Primary anti-censorship channel | |
| Firebase Hosting | Same Spark plan as Storage | A 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 R2 | Cloudflare | 10 GB/month, no egress fees | Secondary; different vendor from Firebase |
| GitHub Gist | GitHub / Microsoft | Unlimited public gists | Lo-fi fallback; extremely durable |
| GitLab Snippet | GitLab | Unlimited public snippets | Confirmed reachable in Turkmenistan even during heavy block windows — a second mirror alongside Firebase for clients who can't reach it |
| Telegram delivery | Telegram | Free | Emergency delivery when everything else is down |
| Nginx-proxy pool | Your VPSes | Cost of VPS | Full operator control over the relay |
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
- Create project — Add project → name it (e.g.
nexus-subs) → Spark plan (free) → Create. - Enable Storage — Build → Storage → "Get started" → "Start in production mode" → choose location → Done.
- Set storage rules — Storage → Rules → replace with:
rules_version = '2';
service firebase.storage {
match /b/{bucket}/o {
match /sub_{file=**} {
allow read: if true;
allow write: if false;
}
}
}
- Generate service account key — Project Settings (⚙) → Service accounts → "Generate new private key" → Download. Treat as a password.
- Find bucket name — Storage → top shows
gs://your-project.firebasestorage.app. Copy the part aftergs://.
In the panel
- Settings → Subscription channels → Firebase Storage → ⚙
- Paste Bucket name and Service account JSON (entire file contents)
- Toggle Enabled, set Priority (lower = preferred;
10is a good start) - Save → click Test (refresh icon)
Reading the test result
A passing test looks like:
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 failed | Likely cause | Fix |
|---|---|---|
creds | Service account JSON wrong or expired | Regenerate the key in Firebase Console |
upload | Storage not enabled or wrong rules | Re-check setup steps 2–3 |
fetch | Public read rule not applied | Re-paste the rules from step 3 |
match | Edge cache served stale blob (rare) | Usually retries hide this; file a bug if persistent |
cleanup | Service account read-only | Delete 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.
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
- R2 (left sidebar) → Create bucket → name it (e.g.
nexus-subs). - Open bucket → Settings → Public access → enable. Copy the
https://pub-<id>.r2.devURL. - Top-right of R2 page → Manage R2 API tokens → Create token → Object Read & Write (limit to your bucket) → save Access Key ID + Secret.
- 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 → ⚙:
| Field | Where to find it |
|---|---|
| Cloudflare account ID | 32-char hex from step 4 |
| Bucket name | e.g. nexus-subs |
| Access key ID | From step 3 |
| Secret access key | From step 3 (shown once) |
| Public URL base | https://pub-<id>.r2.dev from step 2 |
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
- github.com/settings/tokens → Personal access tokens → Tokens (classic) → Generate new token.
- Name:
nexus-gists. Scope: check gist ONLY. Expiration: 1 year (calendar a renewal). - 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.
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)
- Message
@BotFatheron Telegram →/newbot→ pick a name and username (must end inbot). - Copy the token BotFather gives you.
- Settings → Subscription channels → Telegram → ⚙:
- Bot username: without the @
- Bot token: paste from BotFather
- Save → Test. The test calls
getMeand 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:
# 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:
{
"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:
| Row | What it is | Copy + QR |
|---|---|---|
| Direct | Plain /sub/<token> URL served by the panel | Copy only |
| Happ (encrypted) | AES-256-CBC encrypted form via /user/<u>/encrypt-sub | Copy only |
| Firebase / R2 / Gist | Static-storage URLs from enabled channels | Copy + QR |
| Telegram | Deep-link to bot delivery | Copy only |
| Nginx-proxy | Relay URL | Copy 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.
2053) is open in your hosting provider's control panel — Contabo, Aeza, PTR all gate UDP by default.
Add a Hysteria2 Inbound
Dashboard → Settings → Hysteria2 → Add Inbound
| Field | Value | Notes |
|---|---|---|
| Tag | hy2-main | Any unique name |
| Listen port | 2053 | UDP — must be open in firewall |
| Obfs type | salamander | Recommended — hides UDP from DPI in CN/IR/RU |
| Obfs password | strong random | openssl rand -hex 24 |
| Masquerade URL | https://www.bing.com | HTTPS site hysteria impersonates for DPI probes |
| SNI | bing.com | TLS SNI presented to clients |
| TLS cert / key | leave blank for auto | Panel 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:
| Field | Example | Required |
|---|---|---|
| Remark | DE Frankfurt hy2 | Yes |
| Address | de.example.com | Yes — node's public domain or IP |
| Port | 2053 | Yes — the UDP port on that node |
| Country code | DE | Recommended — 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):
# 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.
| Issue | Diagnosis |
|---|---|
No hy2:// in sub | Check the host is enabled and country_code is set; verify license tier is Standard+ |
| UDP connection refused | Test: nc -vu <node> 2053 from outside the DC. Port not open in provider firewall. |
| UDP timeout | ISP or middlebox eating UDP — try obfs salamander or a different port |
| TLS error | Self-signed cert: ensure client has allowinsecure: true or supply the cert's fingerprint |
| Daemon not starting on node | docker 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.
Initial Setup
Generate a one-time install command from the panel, then paste it on the fresh VPS as root:
- In the panel, go to Settings → Middle-servers and click Generate install command.
- Copy the command shown — it looks like:
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)
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.
Swapping to a New Middle Server
When the current middle server is blocked or you want to move to a different VPS:
- SSH into the new VPS and run the same single command above
- 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.
- Done — no node changes, no user reconfiguration needed
Fronting & Censorship Resistance
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:
| Group | What's there |
|---|---|
| Start here | Overview — the health map, your default landing page |
| Pipeline | ① Sources (rival subscription URLs, for research), ② IP Pool (candidate addresses being validated), ③ Hosts (your live fronted entries — add a Cloudflare-proxied host here directly) |
| Infrastructure | Shield (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.
- Healthy — the host is probed and responds normally.
- 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.
- 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.
- 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.
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:
- In Cloudflare, create an API token with DNS-edit permission for the domain you'll front through.
- Dashboard → Defender → Setup → add a credential, provider
cloudflare, paste the API token. - Fronting actual VPN traffic: Dashboard → Defender → Hosts (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.
- Keeping the subscription link reachable: Dashboard → Defender → Shield → Cloudflare 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.
- In AWS, create an IAM user with CloudFront + Route 53 permissions and generate an access key.
- Dashboard → Defender → Setup → add a credential, provider
aws, paste the access key ID and secret. - Dashboard → Defender → Shield → AWS 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.
- In Google Cloud, create a service account with Cloud Run + DNS permissions and download its JSON key.
- Dashboard → Defender → Setup → add a credential, provider
google, paste the service account key. - Dashboard → Defender → Shield → Google 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.
- In Fastly, generate an API token with global scope (required to create CDN services).
- Dashboard → Defender → Setup → add a credential, provider
fastly, paste the token. - Dashboard → Defender → Shield → Fastly 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.
- On bunny.net, create one pull zone on the Standard tier with WebSockets enabled.
- Add a per-node Edge Rule on that pull zone:
Host == bunny-<node>.<your-domain>overriding the Origin URL tohttp://<node-ip>:2009(that node's VLESS BUNNY WS inbound). Repeat one rule per node. - Dashboard → Defender → Setup → add a credential, provider
bunny, paste your Bunny account API key. - Dashboard → Defender → Shield → Bunny 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.
- 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.
- Create a Service Principal:
az ad sp create-for-rbac --role Contributor --scopes /subscriptions/<id>. - Dashboard → Defender → Shield → Azure 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.
- 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.
- 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
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_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,
marzbanormarzban_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.
# 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
# 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.
# 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
| Data | Migrated | Notes |
|---|---|---|
| Users (username, data, expiry) | Yes | All profiles, quotas, UUIDs preserved |
| User proxies / protocols | Yes | VMess, VLESS, Trojan, Shadowsocks |
| Admin accounts | Yes | Passwords carried over |
| Hosts (proxy endpoints) | Yes | All host rows copied, Nexus-only fields default to off |
| Xray inbounds | Yes | Copied from Marzban's xray_config.json |
| Telegram bot token, NOTIFY_* flags | Yes | Written to Nexus .env |
| JWT secret (sub URL compat) | Yes | Stored as MARZBAN_LEGACY_JWT_SECRET — existing sub URLs keep working |
| Notification reminder history | Yes | Prevents re-firing "expires in 3 days" alerts |
| Node configurations | No | Nexus uses ports 62060/62061; re-add nodes via dashboard with a fresh cert |
| Xray routing/dns/outbounds | No | Inbounds only; paste custom blocks into Settings → Core editor after migration |
| Hysteria2 hosts | No | Marzban 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:
- Test a legacy Marzban subscription URL — it must return a valid config (JWT compat check)
- Check user count:
docker exec nexus-panel sqlite3 /var/lib/panel/db.sqlite3 'SELECT COUNT(*) FROM users;' - Run
nexus cli migrate post-cutoverto scan for stale Marzban daemons (marzguard, certbot cron hooks) - If you use Hysteria2: add inbound + one host per node (see the Hysteria2 section)
- Re-add nodes via Dashboard → Nodes (new cert, ports 62060/62061)
- Run
nexus cli migrate finalizeto free the snapshot once stable
# 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
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
# 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:
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).
/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
| Data | Migrated | Notes |
|---|---|---|
| Users, traffic limits, used traffic, expiry, status | Yes | |
| Per-protocol credentials | Yes | VLESS UUID, Trojan password, Shadowsocks password preserved |
| Subscription URL | Yes | Opaque short ID stored as an alias — existing links keep resolving |
| Telegram ID | No | NexusPanel users have no Telegram field; reported as a warning during import |
| Hosts / nodes | No | Wire 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.):
- Navigate to Settings in the dashboard
- Click Enable 2FA
- Scan the QR code with your authenticator app
- Enter the 6-digit code to confirm
- Save recovery codes in a secure location
Via API:
# 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
CAPTCHA_PROVIDER="turnstile" TURNSTILE_SITE_KEY="0x4AAAAAAA..." TURNSTILE_SECRET_KEY="0x4AAAAAAA..."
Built-in Captcha
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:
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_CERTFILEandUVICORN_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
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:
curl -X PUT /api/v1/admin/admin \ -H "Authorization: Bearer TOKEN" \ -H "Content-Type: application/json" \ -d '{"password": "newSecurePassword123"}'
How to Backup
SQLite
# 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
docker compose exec db pg_dump -U nexus nexuspanel > /backups/db-$(date +%Y%m%d).sql
.env, xray_config.json, and any custom templates.
How to Update
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:
- Create your template files in the templates directory:
mkdir -p /var/lib/nexuspanel/templates/clash nano /var/lib/nexuspanel/templates/clash/custom.yml
- Reference the template in
.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:
- Copy the default template as a starting point:
cp -r /opt/nexuspanel/app/templates/subscription \ /var/lib/nexuspanel/templates/subscription
- Edit
/var/lib/nexuspanel/templates/subscription/index.html - Set in
.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.