Deploy a Docker proxy stack with VLESS + WebSocket + CDN, VLESS + XTLS-Reality, and/or Hysteria 2.
The stack is built around one .env file. WebSocket mode gets a Let's Encrypt certificate through nginx/acme, and Hysteria reuses that same certificate from ./certs/<domain>.crt and ./certs/<domain>.key.
| Mode | Transport | Public port | Cloudflare/CDN | Domain | Notes |
|---|---|---|---|---|---|
| VLESS + WS + CDN | WebSocket over TLS | TCP 443 | Yes | Required | Nginx terminates TLS and proxies to Xray |
| VLESS + Reality | TCP + Reality TLS camouflage | TCP 443 or 2083 | No | Not required | Direct to Xray; use server IP |
| Hysteria 2 | QUIC/UDP | UDP 443 | No | Required for cert/SNI | Direct UDP; reuses the nginx/acme cert |
You can install one mode, WS + Reality together, or all three together.
Before running the installer for WS/Both/All modes, point your domain's DNS A record to the server IP and keep it DNS only (grey cloud) in Cloudflare — the installer verifies this and waits for the Let's Encrypt certificate. You enable the orange-cloud proxy after the certificate is issued.
Run the current branch installer:
sudo bash <(curl -fsSL https://raw.githubusercontent.com/samrand96/v2ray-nginx-cloudflare/backup/install.sh)The installer asks you to choose:
VLESS + WebSocket + CDNVLESS + XTLS-RealityHysteria 2only, using an existing cert in./certsWS + RealityAll three
For Hysteria-only, run WS/Both/All first or copy an existing certificate into ./certs before starting Hysteria.
Client -> Cloudflare/CDN -> Nginx TLS -> Xray VLESS-WS on 1310
Use this when you want CDN-fronted HTTPS traffic. During certificate issuance, keep the DNS record pointed to the server and disable Cloudflare proxy until Let's Encrypt succeeds.
Client -> Xray Reality TCP port
Use this direct to the server IP. Do not put Reality behind nginx or Cloudflare.
Client -> Hysteria UDP port -> direct IPv4 outbound
Hysteria uses the same certificate files that nginx/acme created for your domain. It can share numeric port 443 with nginx because nginx listens on TCP and Hysteria listens on UDP. Cloudflare proxy does not carry this UDP traffic, so connect to the server IP or a DNS-only hostname and keep SNI set to the certificate domain.
.env # Generated runtime configuration
.env.example # Example configuration
install.sh # Main interactive installer
install-hysteria.sh # Standalone Hysteria installer
tiny-vps-mode.sh # Stop helper containers after cert issuance
generate-config.sh # Regenerate Xray config from .env
generate-hysteria-config.sh # Regenerate Hysteria config from .env
docker-compose.yml # WS-only stack
docker-compose.reality.yml # Reality-only stack
docker-compose.modular.yml # WS + Reality stack
docker-compose.hysteria.yml # Hysteria-only stack
hysteria/config.template.yaml # Hysteria template
hysteria/config.yaml # Generated Hysteria config, ignored by git
vhost/default # Server-level nginx include
vhost/default_location # Location-level nginx include
v2ray/config/*.template.json # Xray templates
v2ray/config/config.json # Generated Xray config, ignored by git
vless://<UUID>@<CLOUDFLARE-IP>:443?type=ws&security=tls&path=%2F&host=<DOMAIN>&sni=<DOMAIN>&encryption=none#VLESS-WS
Settings: network ws, path /, TLS enabled, Host/SNI set to your domain.
vless://<UUID>@<SERVER-IP>:<REALITY-PORT>?type=tcp&security=reality&pbk=<PUBLIC-KEY>&fp=chrome&sni=<REALITY-SNI>&sid=<SHORT-ID>&flow=xtls-rprx-vision#VLESS-Reality
Settings: address is the server IP, not the CDN domain. Use the generated public key, short ID, and Reality SNI from .env.
hysteria2://<PASSWORD>@<SERVER-IP>:<UDP-PORT>/?sni=<DOMAIN>#Hysteria2
Settings: address is the server IP or a DNS-only hostname, SNI is the domain whose cert exists in ./certs, and the firewall must allow UDP on the chosen port.
Regenerate Xray:
./generate-config.sh
docker compose -f docker-compose.modular.yml restart v2rayRegenerate Hysteria:
./generate-hysteria-config.sh
docker compose -f docker-compose.hysteria.yml restart hysteriaInstall Hysteria separately after certs exist:
sudo bash install-hysteria.shView logs:
docker compose -f docker-compose.modular.yml logs -f
docker compose -f docker-compose.hysteria.yml logs -f hysteriaThe default configs are tuned for small VPS boxes:
- Docker logs are capped at
2mx2files per container. - Xray uses plain IPv4 DNS instead of loading geosite/geoip databases.
- Xray log level is
warningand nginx access logs are disabled. - Hysteria uses direct IPv4 outbound and a short sniff timeout.
- Compose applies conservative memory guardrails: nginx
96m, Xray128m, Hysteria128m, docker-gen64m, ACME96m.
After certificates are issued, you can stop the nginx helper containers to save RAM:
./tiny-vps-mode.sh docker-compose.modular.ymlThat keeps nginx and Xray running, but stops dockergen and nginx-proxy-acme. Restart those helpers before changing proxy config or renewing certificates.
docker compose -f docker-compose.modular.yml ps
docker compose -f docker-compose.modular.yml logs nginx
docker compose -f docker-compose.modular.yml logs nginx-proxy-acme
docker compose -f docker-compose.modular.yml exec nginx nginx -tIf ACME fails, confirm port 80/tcp is open, DNS points to the server IP, and Cloudflare proxy is disabled during issuance.
To test port 80 from another machine: curl -I http://<domain>/ — any HTTP response (even 403/503) means the port is open; a timeout means it is blocked. Open it with ufw allow 80/tcp or firewall-cmd --permanent --add-port=80/tcp && firewall-cmd --reload, and remember that provider firewalls (AWS security groups, Oracle, Hetzner, GCP, ...) must be opened in the provider's panel as well.
The installer uses ACME_FORCE_IPV4=auto by default: it leaves the ACME companion dual-stack when IPv6 connectivity works, and only forces outbound Let's Encrypt API calls over IPv4 when IPv6 routing is broken. Set ACME_FORCE_IPV4=1 to always force IPv4, or ACME_FORCE_IPV4=0 to never force it.
If the certificate is missing after a failed attempt, the ACME companion only retries once per hour on its own. Trigger an immediate retry with:
docker exec nginx-proxy-acme /app/signal_le_serviceThe installer waits up to 300 seconds for the certificate; on slow networks you can extend this with CERT_WAIT_TIMEOUT=600 sudo -E bash install.sh.
grep REALITY .env
docker compose -f docker-compose.modular.yml logs -f v2ray
ss -lntp | grep -E ':(443|2083)\b'Reality must be tested against the server IP and the exact generated REALITY_PUBLIC_KEY, REALITY_SHORT_ID, and REALITY_SERVER_NAME.
grep HYSTERIA .env
ls -l certs/${DOMAIN}.crt certs/${DOMAIN}.key
docker compose -f docker-compose.hysteria.yml ps
docker compose -f docker-compose.hysteria.yml logs -f hysteria
ss -lunp | grep ":${HYSTERIA_PORT:-443}"If Hysteria does not connect, check that UDP is open on the server firewall/provider firewall. If the domain is orange-cloud proxied in Cloudflare, still use the server IP as the client address and set sni=<DOMAIN>.
docker compose -f docker-compose.modular.yml down
docker compose -f docker-compose.reality.yml down
docker compose -f docker-compose.hysteria.yml down