Jun 22, 2026
This guide comes from a recent end-to-end deployment session where I set up an Xboard panel, Caddy for HTTPS, Hysteria2 nodes, a dedicated subscription domain, SMTP mail, and the final production hardening steps from scratch.
This setup fits a fairly common pattern: one public Linux server runs the Xboard panel, and one or more Linux VPS instances serve as nodes. You can host Xboard locally if you really want to, but you still need at least one overseas server for the node side.
Before you start, make sure you already have the following:
80/tcp and 443/tcp available.8443/udp open, or later 20000-29999/udp if you plan to use port hopping.curl, wget, git, ufw, and later Docker, Caddy, and certbot.The example domains in this guide are:
Panel domain: v.shishishi3.com
Subscription domain: 474769754.xyz
Hong Kong node: hk.474769754.xyz
US node: us.474769754.xyz
Replace them with your own domains in practice.
This post uses a direct Linux deployment model:
User -> HTTPS domain -> Linux server with Caddy -> Xboard container
User -> HY2 node domain over UDP -> Linux node server running xboard-node
I strongly recommend separating the responsibilities of your domains:
v.shishishi3.com
Purpose: the main Xboard panel, admin login, and management operations
474769754.xyz
Purpose: subscription distribution entry
hk.474769754.xyz
Purpose: Hong Kong node domain
us.474769754.xyz
Purpose: US node domain
Do not use the same domain for the admin panel, subscriptions, and nodes. Splitting them makes troubleshooting and risk control much cleaner, and it also reduces the blast radius if one part gets blocked or abused.
If you want a separate subscription entry, a cheap numeric domain is usually enough. In practice it often costs less than ¥10.
Recommended specs for the panel server:
OS: Ubuntu 22.04 / 24.04 or Debian 12
Memory: 1 GB minimum, 2 GB preferred
Ports: 80/tcp, 443/tcp
Recommended specs for node servers:
OS: Ubuntu 22.04 / 24.04 or Debian 12
Protocol: Hysteria2
Ports: 8443/udp, or 20000-29999/udp for port hopping
Start with a system update:
sudo apt-get update
sudo apt-get install -y curl wget ca-certificates git ufw
If this gets stuck because of an old Ookla speedtest repository, jump to Broken apt source.
Example Cloudflare DNS records:
A v panel server IP DNS only
A @ panel server IP DNS only
A hk Hong Kong node IP DNS only
A us US node IP DNS only
Important details:
474769754.xyz, the hostname should be @, not the literal domain string.DNS only.Verify with:
nslookup 474769754.xyz 1.1.1.1
nslookup hk.474769754.xyz 1.1.1.1
nslookup us.474769754.xyz 1.1.1.1
If your local machine is using a proxy or TUN and you see 198.18.x.x fake IP results, jump to Fake-IP interference.
If Docker is not installed yet:
curl -fsSL https://get.docker.com | sudo sh
sudo systemctl enable --now docker
docker --version
docker compose version
The official compose branch of Xboard ships with a Docker Compose template that exposes container port 7001 to host port 7001.
cd /opt
sudo git clone -b compose --depth 1 https://github.com/cedar2025/Xboard Xboard
cd /opt/Xboard
Make sure compose.yaml contains something like:
services:
xboard:
image: ghcr.io/cedar2025/xboard:latest
restart: unless-stopped
ports:
- "7001:7001"
volumes:
- ./.env:/www/.env
- ./.docker/.data/:/www/.docker/.data
- ./storage/logs:/www/storage/logs
- ./storage/theme:/www/storage/theme
- ./plugins:/www/plugins
- redis-data:/data
For a simple setup, initialize it with SQLite and the built-in Redis:
sudo docker compose run --rm \
-e ENABLE_SQLITE=true \
-e ENABLE_REDIS=true \
-e ADMIN_ACCOUNT=admin@example.com \
xboard php artisan xboard:install
The installer will print:
Admin email
Admin password
Admin path
Save those values, then bring the service up:
sudo docker compose up -d
sudo docker compose ps
sudo docker compose logs -f
Test local access:
curl -I http://127.0.0.1:7001
The expected result is:
HTTP/1.1 200 OK
Install Caddy:
sudo apt-get install -y debian-keyring debian-archive-keyring apt-transport-https curl
curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/gpg.key' \
| sudo gpg --dearmor -o /usr/share/keyrings/caddy-stable-archive-keyring.gpg
curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/debian.deb.txt' \
| sudo tee /etc/apt/sources.list.d/caddy-stable.list
sudo apt-get update
sudo apt-get install -y caddy
Write this into /etc/caddy/Caddyfile:
v.shishishi3.com {
reverse_proxy 127.0.0.1:7001
}
474769754.xyz {
handle /s/* {
reverse_proxy 127.0.0.1:7001
}
handle /api/v1/client/subscribe* {
reverse_proxy 127.0.0.1:7001
}
respond "Not Found" 404
}
What this means:
v.shishishi3.com
Reverse-proxy the full Xboard panel.
474769754.xyz
Expose only subscription paths and keep the admin panel hidden.
The /s/* rule is important. A real Xboard subscription link often looks like:
https://474769754.xyz/s/xxxxxxxxxxxxxxxx
If you only allow /api/v1/client/subscribe*, the client import request can still get blocked by Caddy. If that happens, jump to Subscription 404.
Validate and reload Caddy:
sudo caddy validate --config /etc/caddy/Caddyfile
sudo systemctl reload caddy
sudo journalctl -u caddy -f
Test both domains:
curl -I https://v.shishishi3.com
curl -I https://474769754.xyz
Expected results:
https://v.shishishi3.com -> 200
https://474769754.xyz -> 404
A 404 on the root of the subscription domain is normal, because we intentionally do not expose the panel there.
If you get unexpected eof while reading here, jump to Caddy TLS EOF.
After logging into the panel, start by changing:
Site name: your own name
Site URL: your panel domain or subdomain, such as https://v.shishishi3.com
Subscription URL: your subscription domain or subdomain, such as https://474769754.xyz
Recommended security settings:
Admin path: change it to a random path
Open registration: enable only when ready, or keep it closed for private use
Email verification: enabled
Login captcha: enabled
Registration captcha: enabled
Subscription anti-leech: enabled
If the panel only offers one URL field, use:
https://v.shishishi3.com
If it provides a separate subscription URL field, use:
https://474769754.xyz
If the panel is for personal use only, you may not need this section. But if you want email verification or password reset, set up SMTP properly.
Cloudflare Email Routing can receive and forward mail, but it is not an SMTP sending server.
This guide uses Brevo’s free SMTP:
SMTP host: smtp-relay.brevo.com
SMTP port: 587
Encryption: TLS
SMTP username: the SMTP Login provided by Brevo
SMTP password: your Brevo SMTP key
From address: noreply@474769754.xyz
A few details matter:
587 means TLS or STARTTLS, not SSL/TLS.ac96b9001@smtp-brevo.com are SMTP login usernames, not the visible sender address.noreply@474769754.xyz, but it must be verified in Brevo.noreply@474769754.xyz.Cloudflare DNS allows only one SPF record. If Cloudflare already gives you:
v=spf1 include:_spf.mx.cloudflare.net ~all
and Brevo asks for:
v=spf1 include:spf.brevo.com ~all
merge them into:
v=spf1 include:_spf.mx.cloudflare.net include:spf.brevo.com ~all
If verification mail is not arriving, inspect the Xboard logs:
cd /opt/Xboard
sudo docker compose logs --tail=160 xboard
sudo grep -R "SMTP\|Brevo\|SendEmailJob\|Unauthorized" storage/logs/
If you see 525 5.7.1 Unauthorized IP address in the logs, jump to SMTP unauthorized IP.
Adjust your plans to fit your actual node machines and bandwidth. The examples below are simply the way I structured mine.
If the US node has plenty of traffic while the Hong Kong node is relatively limited, a reasonable split is:
US San Jose: main node, 1x
Hong Kong: backup node, 5x / 8x / 10x
Example low-cost plan:
Plan name: Vela Basic
Monthly: ¥3.9
Yearly: ¥36
Traffic: 50 GB / month
Speed cap: 300 Mbps
Devices: 3
US: 1x
HK: 5x or higher
If the pricing is closer to:
Monthly: ¥3
Yearly: ¥30
then a more conservative package may be:
Traffic: 40 GB / month
Speed cap: 200 Mbps
Devices: 2
HK: 8x
Example plan description template:
## Plan Details
- Traffic: {{transfer}} GB
- Speed limit: {{speed}} Mbps
- Concurrent devices: {{devices}}
- Primary node: San Jose, US West Coast
- Backup node: high-multiplier Hong Kong node
## Service Notes
1. Traffic resets {{reset_method}}
2. Supports common iOS, Android, Windows, and macOS clients
3. The San Jose node is the primary route, with a 10 Gbps uplink and a real-world peak around 1.8 Gbps to 6 Gbps
4. The Hong Kong node is a backup route and uses a higher multiplier because monthly traffic is limited
5. Best suited for browsing, social media, AI tools, and light streaming
6. BT/PT, mining, scanning, spam, and long-term bandwidth abuse are not allowed
7. 24/7 technical support
In the Xboard backend, choose:
Hysteria

Example Hong Kong node:
Node name: HK-HY2-01
Base multiplier: 5
Traffic limit: 0
Tags: HK, HY2
Permission group: Default
Node address: hk.474769754.xyz
Connect port: 8443
Service port: 8443
Protocol version: V2
Obfuscation: enabled
Obfuscation implementation: Salamander
Obfuscation password: random
SNI: hk.474769754.xyz
Allow insecure: disabled
ECH: disabled
Upload bandwidth: set to the real line capacity
Download bandwidth: set to the real line capacity
Bind server: independent deployment or bind to the matching server
Example US node:
Node name: US-HY2-01
Base multiplier: 1
Node address: us.474769754.xyz
Connect port: 8443
Service port: 8443
SNI: us.474769754.xyz
Things to keep straight:
reverse_proxy ordinary HY2 UDP traffic.This next screenshot corresponds to the transport-layer settings, including connect port, service port, obfuscation, SNI, and ECH:

If you want to configure port hopping and server binding, this is the relevant part of the interface:

You can go straight to the Xboard backend, open Node Management -> Add Server, copy the generated xboard-node installation command, and run it on the node server.

Once installation is done, the backend should show the server online with resource and load information.

HY2 requires TLS certificates.
If you are already seeing protocol "hysteria" requires TLS certificate files, jump to Server online but node not running.
If the node server has nothing listening on port 80, you can issue a certificate directly:
sudo apt-get install -y certbot
sudo certbot certonly --standalone \
-d hk.474769754.xyz \
--agree-tos \
-m admin@474769754.xyz \
--non-interactive
If Caddy or nginx is already bound to port 80 on that machine, stop it first, issue the certificate, then start it again:
sudo systemctl stop caddy
sudo certbot certonly --standalone -d hk.474769754.xyz --agree-tos -m admin@474769754.xyz --non-interactive
sudo systemctl start caddy
The resulting certificate files are:
/etc/letsencrypt/live/hk.474769754.xyz/fullchain.pem
/etc/letsencrypt/live/hk.474769754.xyz/privkey.pem
Copy them into the Xboard-Node directory:
sudo mkdir -p /etc/xboard-node/certs
sudo install -m 0644 /etc/letsencrypt/live/hk.474769754.xyz/fullchain.pem \
/etc/xboard-node/certs/hk.fullchain.pem
sudo install -m 0600 /etc/letsencrypt/live/hk.474769754.xyz/privkey.pem \
/etc/xboard-node/certs/hk.privkey.pem
Edit the config:
sudo nano /etc/xboard-node/config.yml
Add this at the top level:
cert:
cert_mode: file
domain: hk.474769754.xyz
cert_file: /etc/xboard-node/certs/hk.fullchain.pem
key_file: /etc/xboard-node/certs/hk.privkey.pem
cert: must stay at the top level. Do not indent it under panel: or machine:.
Restart the service:
sudo systemctl restart xboard-node
sudo journalctl -u xboard-node --since "2 minutes ago" --no-pager
Check the UDP listener:
sudo ss -lunp | grep 8443
You should see 8443/udp.
The US node is the same idea. Just replace the domain and file paths with:
us.474769754.xyz
/etc/xboard-node/certs/us.fullchain.pem
/etc/xboard-node/certs/us.privkey.pem
Open these ports on the panel server:
80/tcp
443/tcp
22/tcp
Open these on the node server:
8443/udp
22/tcp
If you are using port hopping:
20000-29999/udp
Example ufw rules:
sudo ufw allow 22/tcp
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
sudo ufw allow 8443/udp
sudo ufw enable
sudo ufw status
If your cloud provider has security groups, open the same UDP ports there too. Opening only the system firewall is not enough.
Hysteria2 supports port hopping. A practical range is:
20000-29999/udp
The Xboard node settings would look like:
Connect port: 20000-29999
Service port: 8443
Hop interval: 30
On the node server, redirect the port range to the real listening port:
sudo iptables -t nat -A PREROUTING -p udp --dport 20000:29999 -j REDIRECT --to-ports 8443
Persist the rule:
sudo apt-get install -y iptables-persistent
sudo netfilter-persistent save
Check both iptables and the UDP listener:
sudo iptables -t nat -S PREROUTING | grep 8443
sudo ss -lunp | grep 8443
During rollout, it is safer to keep both open:
8443/udp
20000-29999/udp
Once everything is stable, you can keep only the hopping range.
If you are worried that ss does not show the full port range, jump to UDP port range not visible.
The usual flow is:
Create a plan -> create a user -> attach the plan -> copy the subscription link -> import into the client
If the imported client shows only DIRECT and no nodes, the common reasons are:
Inspect the subscription payload directly:
curl -L "https://474769754.xyz/s/your-token"
If the output does not contain:
hysteria
hy2
hk.474769754.xyz
US-HY2-01
then Xboard is not distributing the node yet, and the first things to check are plans and permission groups.
If the payload does contain nodes but the client still does not show them, switch to a client or template that supports HY2, such as sing-box, Clash Meta, or Mihomo.
Before enabling public registration, I recommend confirming all of the following:
SMTP test mail works
Email verification enabled
Registration captcha enabled
Login captcha enabled
Admin path changed to a random one
Subscription domain only exposes /s/*
Plan permission groups are correct
Node multipliers are correct
HK node uses a higher multiplier
Cloud security groups expose only necessary ports
Suggested trial policy:
Free signup plan: disabled
or 1 GB / 1 day
Do not allocate too much Hong Kong traffic to cheap plans
The final port exposure I recommend is:
Panel server:
80/tcp
443/tcp
22/tcp
Node server:
22/tcp
20000-29999/udp
If you are not using port hopping:
22/tcp
8443/udp
Caddy should expose only:
Full reverse proxy for the panel domain
Only /s/* and required API paths on the subscription domain
Backend settings:
Open registration: only when needed
Email verification: enabled
Captcha: enabled
Admin path: random
Trial quota: extremely low or disabled
Node strategy:
US as the primary route, 1x
HK as the backup route, 5x / 8x / 10x
At that point you are no longer in a test setup. You have something you can reasonably expose and operate.
If you run into:
E: The repository 'https://packagecloud.io/ookla/speedtest-cli/ubuntu noble Release' does not have a Release file.
that means the Ookla speedtest apt source does not support the current system release. Disable it first:
grep -R "packagecloud.io/ookla" /etc/apt/sources.list /etc/apt/sources.list.d/
Suppose the output is:
/etc/apt/sources.list.d/ookla_speedtest-cli.list
Disable it like this:
sudo mv /etc/apt/sources.list.d/ookla_speedtest-cli.list \
/etc/apt/sources.list.d/ookla_speedtest-cli.list.disabled
sudo apt-get update
This was a real issue in my own deployment. Any installer that calls apt-get update can get stuck behind that broken source.
If your machine is running a proxy or TUN, 198.18.x.x fake IP results are normal. Use a public DNS resolver directly, or temporarily disable the proxy while checking records.
Typical symptom:
curl: (35) TLS connect error: unexpected eof while reading
Check with:
nslookup domain 1.1.1.1
sudo caddy validate --config /etc/caddy/Caddyfile
sudo journalctl -u caddy -f
The common causes are:
80 and 443 are not actually open.If the response headers look like:
HTTP/2 404
server: Caddy
content-length: 9
then Caddy is blocking the request. Make sure the Caddyfile includes:
handle /s/* {
reverse_proxy 127.0.0.1:7001
}
Check the logs:
sudo grep -R "SendEmailJob\|SMTP\|Unauthorized" /opt/Xboard/storage/logs/
If you find:
525 5.7.1 Unauthorized IP address
then Brevo is rejecting the outbound IP address. Check the IP used by the Xboard container:
cd /opt/Xboard
sudo docker compose exec -T xboard curl -s https://ifconfig.me
Add that IP to Brevo’s Authorized IP addresses.
Inspect the node logs:
sudo journalctl -u xboard-node -n 120 --no-pager
If you see:
requires TLS certificate files
then HY2 is missing certificate configuration. Go back to Section 11 and add the cert: block correctly.
This is normal. It simply means the node process is up, but no client is actively using it at the moment.
Check:
sudo ss -lunp | grep 8443
If you are using port hopping, it is normal that ss shows only 8443, because the process is actually listening on that single backend port. Use iptables to inspect the forwarding rule:
sudo iptables -t nat -S PREROUTING | grep 20000
Also remember to open 20000-29999/udp in your cloud security group.
If the backend says that the plan cannot be deleted because orders still reference it, do not force-delete it.
A safer pattern is:
Disable the plan
Hide the plan
Close purchases
Rename it to Test-Deprecated
Keeping historical order references intact is normal in production systems.
ghcr.io/cedar2025/xboard:latest: https://github.com/cedar2025/Xboard/tree/compose7001 mapped to container 7001: https://raw.githubusercontent.com/cedar2025/Xboard/compose/compose.yamlhttps://github.com/cedar2025/Xboard-Nodehttps://v2.hysteria.network/docs/advanced/Port-Hopping/https://developers.cloudflare.com/email-service/get-started/https://help.brevo.com/hc/en-us/articles/7924908994450-Send-transactional-emails-using-Brevo-SMTP