Setup X-Ray Server in Debian 12
Before we begin, when we come to any "nano" command, it refers to a text editor in linux. It is recommended to copy them to notepad first, then copy from notepad to putty. As I found that some php code will also be copied to putty if we copy the code directly from here. I will always remind you to copy to notepad first when we come to those instance. When you finished editing the file, press "Ctrl"+"X" together, then press "Y" to confirm edit, follow with "Enter" to quit editor. I will always call that command before I explain what I'm going to do with that file, so you won't be confused for what should copy
1. Setting up environment of Debian
Get super user right
sudo -i
Change timezone to HK
timedatectl set-timezone Asia/Hong_Kong
Edit source list for updating image to Debian 11 (in case there you want to update from old version or there is no Bullseye image in VPS)
nano /etc/apt/sources.list
Change the file to following (Please copy it to notepad first)
deb https://deb.debian.org/debian bookworm main contrib non-free non-free-firmware
deb-src https://deb.debian.org/debian bookworm main contrib non-free non-free-firmware
deb https://deb.debian.org/debian-security/ bookworm-security main contrib non-free non-free-firmware
deb-src https://deb.debian.org/debian-security/ bookworm-security main contrib non-free non-free-firmware
deb https://deb.debian.org/debian bookworm-updates main contrib non-free non-free-firmware
deb-src https://deb.debian.org/debian bookworm-updates main contrib non-free non-free-firmware
deb https://deb.debian.org/debian bookworm-backports main contrib non-free non-free-firmware
deb-src https://deb.debian.org/debian bookworm-backports main contrib non-free non-free-firmware
deb https://debian.mirror.constant.com bookworm main contrib non-free non-free-firmware
deb-src https://debian.mirror.constant.com bookworm main contrib non-free non-free-firmware
Update installed packages
apt-get update && apt-get upgrade -y
Perform full upgrade (From 10 to 11)
apt full-upgrade -y
In the middle, there is license term confirmation, press q
Then, reboot
reboot
Remove old packages that will not use anymore after upgrade
apt autoremove -y
Prepare the folder for xray
mkdir /usr/local/etc
mkdir /usr/local/etc/xray
set VPS to use Google BBR as TCP congestion control
echo "net.core.default_qdisc=fq" >> /etc/sysctl.conf
echo "net.ipv4.tcp_congestion_control=bbr" >> /etc/sysctl.conf
sysctl -p
reboot
2. Get a Free DDNS (for example, freemyip)
Please remember the token you received (if you use freemyip), the following is an example:
The above example, token is 7f5b79f4f0aaab7c33f04d8a, domain is testbunch.freemyip.com
Then, Install ddclient
apt-get -y install ddclient libdata-validate-ip-perl
Since some items are not shown, anything prompt, press enter We will reconfig ddelient below
dpkg-reconfigure ddclient
Option A. For freemyip (assume using example domain name)
Dynamic DNS service provider: select “other”.
Dynamic DNS update protocol: select "freemyip".
Dynamic DNS server: leave blank.
Optional HTTP proxy: leave blank.
Username: 7f5b79f4f0aaab7c33f04d8a
Password: 7f5b79f4f0aaab7c33f04d8a
Re-enter password: 7f5b79f4f0aaab7c33f04d8a
IP address discovery method: Web-based IP discovery service.
IP discovery services: ipify-ipv4 https://api.ipify.org/
Time between address checks: 5m
Hosts to update: testbunch.freemyip.com
Option B. For cloudflare (assume the full name is a.bname.com)
Dynamic DNS service provider: select “other”.
Dynamic DNS update protocol: select "cloudflare".
Dynamic DNS server: leave blank.
Optional HTTP proxy: leave blank.
Username: email address of cloudflare account.
Password: Cloudflare Global API key.
Re-enter password: Cloudflare Global API key.
IP address discovery method: Web-based IP discovery service.
IP discovery services: ipify-ipv4 https://api.ipify.org/
Time between address checks: 5m
Hosts to update: a.bname.com
Option B(cont.). Edit ddclient config (as zone should be missing)
nano /etc/ddclient.conf
Option B(cont.). Add the following line before the line of a.bname.com, then save
zone='bname.com' \
End of Option B.
Restart ddclient
systemctl restart ddclient
Try to ping the DDNS to check the IP, you might need to restart the ddclient a few times if the DDNS was just created seconds ago.

3. Install NGINX as HTTPS page for fallback
The meaning of fallback is to fake GFW, if GFW tried to access the server without correct setup, a real https page will be shown You can host a your webpage if you want, folder location is /var/www/html/
Install NGINX
apt-get -y install nginx
Edit NGINX config to recognize acme folder and deny invalid access to folder view
nano /etc/nginx/sites-enabled/default

Better copy it to notepad, then copy it from notepad again (as it might copy the background code in putty) You may ignore the 3 lines for acme-challenge if you use cloudflare, but you must add the deny all lines
*If you planning to install haproxy later, please change port 80 too.
listen 81 http2;
location ^~ /.well-known/acme-challenge/ {
# the usual settings
}
location ~ /\. {
deny all;
}
restart NGINX, create folder to store acme-challenge file Again, you can ignore the 2 lines which mkdir if you use cloudflare
systemctl restart nginx
mkdir /var/www/html/.well-known
mkdir /var/www/html/.well-known/acme-challenge
4. Install acme.sh and get SSL cert
apt-get -y install curl && curl https://get.acme.sh | sh && source ~/.bashrc
Register an account in ZeroSSL with email Change [email protected] to your email address
acme.sh --register-account -m [email protected]
Option A. for user that use freemyip only
Issue SSL cert to your system and install it to xray folder (Remember to change testbunch.freemyip.com to your ddns name) This method requires opening port 80 or port forward of port 80
acme.sh --issue -d testbunch.freemyip.com -w /var/www/html -k ec-384 --force
acme.sh --installcert -d testbunch.freemyip.com --fullchainpath /usr/local/etc/xray/fullchain.crt --keypath /usr/local/etc/xray/privkey.key --ecc --force
Option B. for user using cloudflare (non Proxied) only
You can generate cert with port 80 closed Issue SSL cert to your system and install it to xray folder, you can add more domain with -d when issue cert Never put your proxied CNAME record here. (Remember to change a.bname.co to your ddns name)
CF_Token=Generate a new Cloudflare API token with 2 Permission: Zone.Zone.Read, Zone.DNS.Edit You can set it can access all Zone, which you don't need to export zoneID here. Or you can set it access target zone and get the zone ID in Cloudflare webpage
export CF_Token="V8**************************"
export CF_Account_ID="61**************************"
export CF_Zone_ID="09**********************"
acme.sh --issue --dns dns_cf -d a.bname.co -k ec-384 --force
acme.sh --installcert -d a.bname.co --fullchainpath /usr/local/etc/xray/fullchain.crt --keypath /usr/local/etc/xray/privkey.key --ecc --force
Option C. for user using cloudflare (with CDN) only
Since there will be 2 domain name pointing to the same server, you need a cert that accept 2 names
b.bname.co is the proxied cname in Cloudflare
*Haproxy case, add \ after -force, next line type --reloadcmd "cat /root/.acme.sh/a.bname.co_ecc/fullchain.cer /root/.acme.sh/a.bname.co_ecc/a.bname.com.key | tee /etc/ssl/private/a.bname.co.pem"
export CF_Token="V8**************************"
export CF_Account_ID="61**************************"
export CF_Zone_ID="09**********************"
acme.sh --issue --dns dns_cf -d a.bname.co -d b.bname.co -k ec-384 --force
acme.sh --installcert -d a.bname.co --fullchainpath /usr/local/etc/xray/fullchain.crt --keypath /usr/local/etc/xray/privkey.key --ecc --force
End of Option parts
Schedule should be auto created, check with following Choose 1 (nano) when asked for choosing editor
crontab -e
There should be a line with command similar to following, if none, add it
48 0 * * * "/root/.acme.sh"/acme.sh --cron --home "/root/.acme.sh" > /dev/null
5. Generate Cloudflare warp setting
Since Xray 1.7.0, xray support wireguard outbound connections. We can use Cloudflare warp to protect connection to CN instead of blocking all CN traffic
Create a folder to store Cloudflare warp config and download wgcf for generating config (Current version is 2.2.22 on 4 Jun 2024)
mkdir /usr/local/wgcf
cd /usr/local/wgcf
wget https://github.com/ViRb3/wgcf/releases/download/v2.2.22/wgcf_2.2.22_linux_amd64 -O wgcf
Modify permission of wgcf to be able to execute Then, register Cloudflare warp free account
chmod +x wgcf
./wgcf register
Generate Cloudflare warp config file
./wgcf generate
Show the config of Cloudflare warp, you need to save the following 2 items: PrivateKey, PublicKey
cat wgcf-profile.conf

With the above example PrivateKey: UI.......u/Wk= PublicKey: bm......yo=
6. Install and config Xray
Install Xray
bash -c "$(curl -L https://github.com/XTLS/Xray-install/raw/main/install-release.sh)" @ install -u root
Generate UUID use for XRAY (Please remember the uuid, you will reuse it)
xray uuid

Remove stock X-Ray config and create new one
rm /usr/local/etc/xray/config.json
nano /usr/local/etc/xray/config.json
Current config applies to Xray 1.8.13
Option A. For someone who will only use direct connection (No CDN setting)
Config description: Default using vless, xtls-rprx-vision, fallback to webpage if invalid If destination is CN domain or CN IP, use Cloudflare warp for outbound
Better copy it to notepad, then copy it from notepad again (as it might copy the background code in putty) Replace the following items: 90000000-0000-0000-0000-00000000000a : UUID you generated UI0000000000000000000000000000000000000u/Wk= : PrivateKey in 5 bm000000000000000000000000000000000000000yo= : PublicKey in 5
{
"log": {
"loglevel": "none"
},
"dns":{
"servers":[
"localhost"
],
"queryStrategy":"UseIPv4"
},
"routing": {
"domainStrategy": "IPIfNonMatch",
"rules": [
{
"type": "field",
"domain": [
"geosite:category-ads-all"
],
"outboundTag": "block"
},
{
"type": "field",
"domain": [
"geosite:cn"
],
"outboundTag": "wireguard-1"
},
{
"type": "field",
"ip": [
"geoip:cn"
],
"outboundTag": "wireguard-1"
},
{
"type": "field",
"ip": [
"geoip:private"
],
"outboundTag": "block"
}
]
},
"inbounds": [
{
"port": 443,
"protocol": "vless",
"settings": {
"clients": [
{
"id": "90000000-0000-0000-0000-00000000000a",
"flow": "xtls-rprx-vision"
}
],
"decryption": "none",
"fallbacks": [
{
"alpn": "http/1.1",
"dest":80
},
{
"alpn": "h2",
"dest": 81
}
]
},
"streamSettings": {
"network": "tcp",
"security": "tls",
"tlsSettings": {
"minVersion":"1.3",
"alpn": [
"h2",
"http/1.1"
],
"certificates": [
{
"certificateFile": "/usr/local/etc/xray/fullchain.crt",
"keyFile": "/usr/local/etc/xray/privkey.key",
"ocspStapling": 3600
}
],
"rejectUnknownSni": true
}
},
"sniffing": {
"enabled": true,
"destOverride": [
"http",
"tls"
]
}
}
],
"outbounds": [
{
"protocol": "freedom",
"settings": {
"domainStrategy": "UseIPv4"
},
"tag": "direct"
},
{
"protocol": "wireguard",
"settings": {
"secretKey" : "UI0000000000000000000000000000000000000u/Wk=",
"peers": [
{
"publicKey": "bm000000000000000000000000000000000000000yo=",
"endpoint": "engage.cloudflareclient.com:2408"
}
]
},
"tag": "wireguard-1"
},
{
"protocol": "blackhole",
"tag": "block"
}
]
}
Option B. For someone who will use CDN for backup connection (Cloudflare Proxied)
Config description: Default using vless, xtls-rprx-vision, fallback to webpage if invalid If path /fallws is provided, fallback to use ws, and it only accept proxy protocol connection (for example Cloudflare CDN) If destination is CN domain or CN IP, use Cloudflare warp for outbound
Better copy it to notepad, then copy it from notepad again (as it might copy the background code in putty) Replace the following items: 90000000-0000-0000-0000-00000000000a : UUID you generated (2 instance) /fallws : the folder you name to tell xray fallback using ws/CDN (2 instance) UI0000000000000000000000000000000000000u/Wk= : PrivateKey in 5 bm000000000000000000000000000000000000000yo= : PublicKey in 5
Note: there are 6 lines with "#" in front, if you use reverse proxy (like HAProxy) to handle all traffic in TCP443, and you enabled "Proxy Protocol" in that config. Remove those "#" in this config. If you don't know what I'm talking about, please ignore this
{
"log": {
"loglevel": "none"
},
"dns":{
"servers":[
"localhost"
],
"queryStrategy":"UseIPv4"
},
"routing": {
"domainStrategy": "IPIfNonMatch",
"rules": [
{
"type": "field",
"domain": [
"geosite:category-ads-all"
],
"outboundTag": "block"
},
{
"type": "field",
"domain": [
"geosite:cn"
],
"outboundTag": "wireguard-1"
},
{
"type": "field",
"ip": [
"geoip:cn"
],
"outboundTag": "wireguard-1"
},
{
"type": "field",
"ip": [
"geoip:private"
],
"outboundTag": "block"
}
]
},
"inbounds": [
{
"port": 443,
"protocol": "vless",
"settings": {
"clients": [
{
"id": "90000000-0000-0000-0000-00000000000a",
"flow": "xtls-rprx-vision"
}
],
"decryption": "none",
"fallbacks": [
{
"alpn": "http/1.1",
"dest":80
},
{
"path": "/fallws",
"dest": 1234,
"xver": 1
},
{
"alpn": "h2",
"dest": 81
}
]
},
"streamSettings": {
"network": "tcp",
"security": "tls",
# "tcpSettings":{
# "acceptProxyProtocol":true
# },
"tlsSettings": {
"minVersion":"1.3",
"alpn": [
"h2",
"http/1.1"
],
"certificates": [
{
"certificateFile": "/usr/local/etc/xray/fullchain.crt",
"keyFile": "/usr/local/etc/xray/privkey.key",
"ocspStapling": 3600
}
],
"rejectUnknownSni": true
}
},
"sniffing": {
"enabled": true,
"destOverride": [
"http",
"tls"
]
}
},
{
"port": 1234,
"listen": "127.0.0.1",
"protocol": "vless",
"settings": {
"clients": [
{
"id": "90000000-0000-0000-0000-00000000000a",
"level": 0
}
],
"decryption": "none"
},
"streamSettings": {
"network": "ws",
# "tcpSettings": {
# "acceptProxyProtocol": true
# },
"security": "none",
"wsSettings": {
"acceptProxyProtocol": true,
"path": "/fallws"
}
},
"sniffing": {
"enabled": true,
"destOverride": [
"http",
"tls"
]
}
}
],
"outbounds": [
{
"protocol": "freedom",
"settings": {
"domainStrategy": "UseIPv4"
},
"tag": "direct"
},
{
"protocol": "wireguard",
"settings": {
"secretKey" : "UI0000000000000000000000000000000000000u/Wk=",
"peers": [
{
"publicKey": "bm000000000000000000000000000000000000000yo=",
"endpoint": "engage.cloudflareclient.com:2408"
}
]
},
"tag": "wireguard-1"
},
{
"protocol": "blackhole",
"tag": "block"
}
]
}
End of Options part
Since XRay will not load new cert automatically if cert updated, you need to restart xray after cert updated.
crontab -e
Add the following to the bottom line to restart xray everyday 4 AM
0 4 * * * /bin/systemctl restart xray
Then, restart XRay
systemctl restart xray
You can check xray status with following command
systemctl status xray
The result will be something like below, which shows it is running and the version of xray

Last updated
Was this helpful?