VPN on Raspberry PI
31 Aug 2026
Configure Xray in Docker
- Generate UUID and Reality keypair:
docker run --rm ghcr.io/xtls/xray-core:latest uuid docker run --rm ghcr.io/xtls/xray-core:latest x25519PrivateKeygoes intoconfig.json,Password (PublicKey)goes into the client link.Hash32is not used. - Create
docker-compose.yml: ```yml services: xray: image: ghcr.io/xtls/xray-core:26.3.27 container_name: xray restart: always network_mode: host user: root # <— important cap_add:- NET_BIND_SERVICE # <— to take ports lower 1024 volumes:
- ./config.json:/usr/local/etc/xray/config.json ```
- Create
config.json— insert UUID and PrivateKey from step 1{ "log": { "loglevel": "warning" }, "policy": { "levels": { "0": { "handshake": 4, "connIdle": 120, "uplinkOnly": 2, "downlinkOnly": 5 } } }, "inbounds": [{ "port": 443, "listen": "0.0.0.0", "protocol": "vless", "settings": { "clients": [{ "id": "... UUID ...", "flow": "xtls-rprx-vision", "email": "user1" }], "decryption": "none" }, "streamSettings": { "network": "tcp", "security": "reality", "realitySettings": { "show": false, "dest": "google.com:443", "serverNames": ["google.com"], "privateKey": "... private key ...", "shortIds": [""] }, "sockopt": { "tcpKeepAliveIdle": 100, "tcpKeepAliveInterval": 30 } }, "sniffing": { "enabled": true, "destOverride": ["http", "tls", "quic"] } }], "outbounds": [{ "protocol": "freedom", "settings": { "domainStrategy": "UseIPv4" } }] }
dest and serverNames define the site Reality masquerades as. google.com
works, but it is the most obvious choice and is used to fingerprint Reality
servers. Pick a less popular site that supports TLS 1.3 and HTTP/2, ideally
hosted near your server.
- Start Docker:
docker compose up -d docker compose logs -f xrayLook for
Xray ... started. - Create VLESS link (one line, you may need to change SNI):
vless://[YOUR_UUID]@[YOUR_DDNS_DOMAIN_OR_IP]:443?type=tcp&security=reality&encryption=none&sni=google.com&fp=chrome&pbk=[YOUR_PUBLIC_KEY]&flow=xtls-rprx-vision#TestVPN
snimust matchserverNamesfrom the configpbkis the public key — safe to share, unlikePrivateKeyfp=chromeis required; Reality will not work without a fingerprint
- Import this link into V2BOX or another client.
Multiple users
The config above has a single client, so everyone shares one UUID and you can only revoke access for all of them at once. To revoke access per user, give each one their own entry:
"clients": [
{ "id": "... UUID 1 ...", "flow": "xtls-rprx-vision", "email": "alice" },
{ "id": "... UUID 2 ...", "flow": "xtls-rprx-vision", "email": "bob" }
]
The pbk in the link stays the same for everyone — only the UUID differs.
Removing one entry disconnects that user and leaves the rest untouched.
Rotating keys / revoking access
- Generate a new UUID and update
config.json:docker compose exec xray xray uuidChanging the UUID is enough to revoke access — clients keep the same
pbk.
Regenerate the Reality keypair only if the private key leaked. This forces every client to update their link, since the public key changes too:
docker compose exec xray xray x25519
- Apply the changes:
docker compose up -d --force-recreateUse
--force-recreaterather thanrestart:config.jsonis bind-mounted as a file, so an editor that rewrites it (vim,sed -i) creates a new inode and the running container keeps reading the old one.
Verify the container picked up the new values:
docker exec xray grep -E '"id"|privateKey' /usr/local/etc/xray/config.json