Where to Start

Three 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.

Path A · No install

Try the Free Trial

Open the Telegram bot, type /start, get a 7-day trial license. 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 ↓
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 but strongly recommended — without it you get http only)
  • A NexusPanel license (grab one from the Telegram bot, free 7-day 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

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.

Key capabilities include:

  • 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 & auto-update of 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 (skip to install in trial mode)
  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
Auto-updates
Once installed, the panel heartbeats your license server every 6 hours. When a new version is published, the panel pulls and restarts itself automatically — no manual maintenance.

One-Command Install (Detailed)

The install script accepts optional flags to customize the setup:

bash
# Install with custom port and admin credentials
curl -sL https://nexuspanel.store/install | bash -s -- \
  --port 8443 \
  --username myadmin \
  --password securepass123

# Install with SSL certificate (requires domain pointed to server)
curl -sL https://nexuspanel.store/install | bash -s -- \
  --domain panel.example.com \
  --ssl

After installation, the panel is accessible at http://YOUR_IP:8000/dashboard/ (or the configured port).

Manual Install

If you prefer manual control over the installation process:

bash
# Clone the repository
git clone https://github.com/your-org/nexuspanel.git /opt/nexuspanel
cd /opt/nexuspanel

# Copy and edit the environment file
cp .env.example .env
nano .env

# Start with Docker Compose
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:

bash
# Set in .env
SQLALCHEMY_DATABASE_URL="postgresql+asyncpg://nexus:secretpass@db:5432/nexuspanel"

Add the PostgreSQL service to your docker-compose.yml:

yaml — docker-compose.yml
services:
  db:
    image: postgres:16-alpine
    environment:
      POSTGRES_USER: nexus
      POSTGRES_PASSWORD: secretpass
      POSTGRES_DB: nexuspanel
    volumes:
      - pgdata:/var/lib/postgresql/data
    restart: always

  panel:
    image: ghcr.io/haitovs/nexus:latest
    container_name: nexus-panel
    depends_on:
      - db
    env_file: .env
    ports:
      - "8000:8000"
    volumes:
      - /var/lib/nexuspanel:/var/lib/panel
    restart: always

volumes:
  pgdata:

Docker Compose Examples

Minimal (SQLite)

yaml
services:
  panel:
    image: ghcr.io/haitovs/nexus:latest
    container_name: nexus-panel
    env_file: .env
    ports:
      - "8000:8000"
    volumes:
      - /var/lib/nexuspanel:/var/lib/panel
    restart: always

Full Stack (PostgreSQL + SSL + Telegram)

yaml
services:
  db:
    image: postgres:16-alpine
    environment:
      POSTGRES_USER: nexus
      POSTGRES_PASSWORD: ${DB_PASSWORD}
      POSTGRES_DB: nexuspanel
    volumes:
      - pgdata:/var/lib/postgresql/data
    restart: always

  panel:
    image: ghcr.io/haitovs/nexus:latest
    container_name: nexus-panel
    depends_on:
      - db
    env_file: .env
    ports:
      - "443:8000"
    volumes:
      - /var/lib/nexuspanel:/var/lib/panel
      - /etc/letsencrypt:/etc/letsencrypt:ro
    restart: always

volumes:
  pgdata:

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
TrialFree5017 days
Standard$4/moUnlimited1030 days/mo
Pro$10/moUnlimitedUnlimited30 days/mo

Trial

No credit card, no signup — open the Telegram bot and type /start. You get a 7-day trial license with 50 users and 1 node. Full dashboard access: all protocols, analytics, Hysteria 2, webhooks, everything. Enough to evaluate on real traffic.

Standard — $4/month

For operators running a live service. Removes the 50-user cap, adds 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 — $10/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 users50UnlimitedUnlimited
Max nodes110Unlimited
Duration7 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.

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

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

  1. Dashboard → NodesAdd New Node
  2. Click Copy Install Command — the command includes the cert, port, and API port
  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://your-domain.com/node-install.sh | bash -s -- \
  --cert-b64 <base64-cert> \
  --port 62060 \
  --api-port 62061

The installer waits for apt/dpkg locks automatically — safe to run on a freshly provisioned VPS.

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 and stored at /var/lib/nexus-node/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

yaml — /opt/nexus-node/docker-compose.yml
services:
  nexus-node:
    image: ghcr.io/haitovs/nexus-node:latest
    environment:
      SERVICE_PORT: 62060
      XRAY_API_PORT: 62061
      SSL_CLIENT_CERT_FILE: "/var/lib/nexus-node/cert.pem"
    ports:
      - "62060:62060"
      - "62061:62061"
      - "443:443"
      - "80:80"
    volumes:
      - /var/lib/nexus-node:/var/lib/nexus-node
    restart: always

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-node/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
Discord Integration
Set DISCORD_WEBHOOK_URL to receive the same notifications in a Discord channel.

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.

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 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

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.