A Dockerized V2Ray/Xray proxy client that manages the entire proxy lifecycle β from subscription parsing to automatic failover. Feed it a subscription URL, and it handles the rest: parses protocols, health-checks servers, and provides stable SOCKS5 + HTTP proxies for your other apps and containers.
Caution
Disclaimer: V2ProDock operates no proxy servers and provides no bandwidth of its own. You supply the subscription URL(s) β self-hosted or public β and the app routes your traffic through those third-party servers as a free alternative to paid proxies. Public configs are run by strangers, so treat exit nodes as untrusted: prefer TLS end-to-end, and expect no uptime, speed, or anonymity guarantees.
Failover changes your exit IP whenever the active upstream switches, and the VPN gateway is experimental (alpha) β do not use it in production or for anything sensitive to IP changes (IP-locked accounts, banking, anti-fraud systems). You are responsible for complying with your ISP's terms and the terms of every service you access through this project.
- Features Β· Quick Start Β· Multi-Instance Setup Β· Usage Β· VPN for legacy devices Β· Commands Β· How It Works Β· Supported Protocols Β· Configuration Β· License
flowchart TB
subgraph LAN["Your Local Network"]
TV["Smart TVs / IoT<br/>IKEv2 / L2TP-IPsec"]
SAT["Satellite Receivers<br/>PPTP + MPPE-128"]
end
subgraph GW["v2prodock-vpn Gateway"]
DAEMONS["charon (IKEv2) Β· xl2tpd (L2TP) Β· pptpd (PPTP)"]
IFACES["ppp / XFRM client interfaces"]
FW{{"iptables fail-closed<br/>direct egress DROPPED"}}
TUN(["tun0 β hev-socks5-tunnel"])
DAEMONS --> IFACES --> FW --> TUN
end
subgraph CORE["v2prodock Core Proxy Engine"]
XRAY[("Live Xray instances<br/>VLESS / VMess / Trojan / Shadowsocks")]
API["auto-healthcheck + latency sort<br/>GET /proxies"]
end
NET["The Internet"]
TV -- "UDP 500 / 4500 / 1701" --> DAEMONS
SAT -- "TCP 1723 + GRE (proto 47)" --> DAEMONS
TUN -- "SOCKS5, pinned to fastest alive" --> XRAY
XRAY --> NET
XRAY -.-> API
style TUN fill:#2ea043,color:#fff
style FW fill:#da3633,color:#fff
Pushed DNS is resolved through the same tunnel β clients cannot leak around the working proxy.
- Subscription-based β paste a v2ray subscription URL, it parses vless/vmess/trojan/shadowsocks configs automatically
- Multi-instance β run N independent xray processes, each with its own proxy pair and failover
- Auto-failover β health-checks every 60s, switches to the next working server on failure
- Auto-refresh β re-fetches subscription every 120s for updated server lists
- HTTP API β query live proxies sorted by latency at
GET /proxies - Dynamic ports β all ports are auto-assigned, no hardcoded ranges
- Docker bridge β other containers connect through the Docker network without port mapping
- Multi-platform β auto-detects OS/arch and downloads the right xray-core binary
- Low-end friendly β tunable memory limits, connection caps, and GC tuning for constrained devices
curl -fsSL https://raw.githubusercontent.com/411A/V2ProDock/main/install.sh | bashOr clone manually:
git clone https://github.com/411A/V2ProDock.git && cd V2ProDock
[ -f .env ] || cp .env.example .env
# Edit .env with your subscription URL(s)
sudo bash install.shRun multiple independent proxy instances in a single container:
# .env
SUBSCRIPTION_URLS=https://sub1.example,https://sub2.example,https://sub3.example
PROXY_INSTANCES=3Port layout: all SOCKS5 ports come first, then all HTTP ports. Instance i of N gets SOCKS5=PORT_BASE+i and HTTP=PORT_BASE+N+i (e.g. with N=3: SOCKS 27019-27021, HTTP 27022-27024). Query them via the API:
# Returns alive proxies sorted by lowest latency
curl http://localhost:27018/proxies[
{"index":1, "socks5":"0.0.0.0:27020", "http":"0.0.0.0:27023", "status":"ok", "latency_ms":85, "name":"server-1"},
{"index":0, "socks5":"0.0.0.0:27019", "http":"0.0.0.0:27022", "status":"ok", "latency_ms":120, "name":"server-2"}
]| Endpoint | Method | Description |
|---|---|---|
/proxies |
GET | Alive proxies sorted by latency (lowest first) |
/all |
GET | All instances including down ones |
/health |
GET | {"status":"ok","instances":3,"alive":2,"starting":1} |
/vpn |
GET | VPN pinning proof: upstream SOCKS + egress IP + verified |
/refresh |
POST | Force subscription re-fetch |
import requests
proxies = requests.get("http://localhost:27018/proxies").json()
proxy = proxies[0] # Fastest proxy
r = requests.get("https://api.ipify.org", proxies={
"http": f"http://{proxy['http']}",
"https": f"socks5://{proxy['socks5']}",
})
print(r.text)services:
your-app:
image: your-app
environment:
- HTTP_PROXY=http://v2prodock:27020
- HTTPS_PROXY=socks5://v2prodock:27019
- NO_PROXY=localhost,127.0.0.1,192.168.1.0/24
networks:
- proxy-net
networks:
proxy-net:
external: true
name: v2prodock_v2prodock-proxy-netTVs, IoT, consoles connect to the v2prodock-vpn sidecar. Its traffic is
forced via tun0 into the fastest alive Xray SOCKS β fail-closed, never
direct. Proofs land in the logs and in GET /vpn:
# .env
VPN_ENABLED=1
VPN_DOMAIN=192.168.1.10 # or public hostname/IP
VPN_USER=vpnuser
VPN_PASSWORD=change-me-8-chars-min
VPN_IPSEC_PSK=change-me-too-8-chars-mincurl http://localhost:27018/vpn
docker logs -f v2prodock-vpn # look for: VPN egress VERIFIED via <name>: <ip>| Device | Protocol | Settings | Notes |
|---|---|---|---|
| MediaStar / StarSat / Ali-chipset receivers | PPTP | Server = your VPN_DOMAIN IP; user/pass from .env; encryption (ΩΎΩΩΨ§ΩΨ³Ψ§Ψ²Ϋ) ON |
Set VPN_ENABLE_PPTP=1. Bridge mode works; switch to host networking only if reconnects stall. |
| Smart TVs, legacy routers | L2TP/IPsec with pre-shared key | Server = VPN_DOMAIN; IPsec PSK = VPN_IPSEC_PSK; user/pass from .env |
No files to import. |
| Apple TV / iPhone / Mac | IKEv2 | Install config/vpn/apple.mobileconfig |
Zero-config, certificate pinned. |
- Set
VPN_ENABLE_PPTP=1in.envanddocker compose up -d. - On the box, server = the exact
VPN_DOMAINvalue (e.g. your VM's LAN IP). - Encryption (
ΩΎΩΩΨ§ΩΨ³Ψ§Ψ²Ϋ) MUST be ON β the server strictly enforces MPPE-128. - Reboot the box once after changing VPN settings; wait 30s+ before redialing.
- If redials still stall, switch to host networking (command below).
Linux host + PPTP reconnects stalling? Bridge mode is proven and stays the default β but if redials stall behind Docker NAT, host networking removes NAT from the GRE path entirely (recommended fallback, not a requirement):
docker compose -f docker-compose.yml -f docker-compose.host.yml up -d --build
Type L2TP/IPsec with pre-shared key, server = VPN_DOMAIN, then enter the
IPsec PSK (VPN_IPSEC_PSK) + PPP username/password. No files to import β
works on old Windows/Android/routers natively.
Some old routers (e.g. stock Asus with bare-L2TP client, no IPsec option)
cannot do IPsec at all. Set VPN_ALLOW_PLAIN_L2TP=1 to accept them: server =
VPN_DOMAIN, no PSK, just PPP username/password. Everything β login and
all traffic β crosses the internet in cleartext. Default 0 refuses bare
L2TP at packet level. Use a unique strong password and only for devices that
cannot do IPsec.
Some devices speak nothing but PPTP β e.g. MediaStar / Ali-chipset satellite
receivers whose VPN menu is just User + Password + an encryption toggle
(ΩΎΩΩΨ§ΩΨ³Ψ§Ψ²Ϋ = MPPE). Symptom: the box sends TCP SYN to port 1723 and times
out because nothing listens there. Set VPN_ENABLE_PPTP=1 to serve PPTP on
TCP 1723 (+ GRE protocol 47) with the same username/password as
IKEv2/L2TP β enable the box's encryption option (MPPE-128 is mandatory
server-side, no opt-out).
PPTP/MPPE is cryptographically broken β LAN-only, never expose TCP 1723
to the internet. The PPTP leg stays inside your trusted LAN; internet
egress still goes via the working Xray proxy (fail-closed, same tun0
machinery, same VERIFIED proofs). How to tell it's PPTP: packet capture
shows IP <box>.x > <server>.1723: Flags [S] (L2TP would be UDP 1701).
GRE note: GRE (IP proto 47) is not TCP/UDP, so Docker ports: cannot publish
it. On a Linux host (e.g. Ubuntu 24.04 VM) the kernel helper
nf_conntrack_pptp (loaded automatically by install.sh) forwards GRE to
the container alongside the TCP 1723 DNAT. If PPTP control connects but data
stalls, your kernel/cloud lacks GRE passthrough β run the vpn service with
network_mode: host (Linux only) as the fallback. Diagnostics:
docker exec v2prodock-vpn cat /var/log/ppp-pptp.log.
Symptom: first connect works, then after an (abrupt/manual) disconnect the
box redials into silence (LCP: timeout sending Config-Requests). Prime
suspect is a stale GRE Call-ID mapping in the host's conntrack table:
same Call ID reused while the old entry still lives, so return GRE goes to
the dead session. The gateway already fights this three ways β dead sessions
are reaped after ~30s (lcp-echo-interval 10, failure 3), every session
teardown is logged ([pptp-ip-down] lines in /var/log/ppp-pptp.log), and
teardown best-effort flushes the peer's GRE entries. Two honest limits: a
container-side flush cannot reach the host NAT table in bridge mode (it is a
no-op there, effective only under host networking), and no idle timeout is
set on purpose β it would hang up your legitimately-idle receiver.
Fix ladder:
- Wait 30s+ before redialing (lets zombies die and conntrack entries age out), and reboot the box once after changing VPN settings β consumer firmware often keeps its own stale call state.
- If it recurs, switch the gateway to host networking (no NAT, no helper,
no stale mappings involved at all):
docker compose -f docker-compose.yml -f docker-compose.host.yml up -d --build - Diagnose with: on the VM host
sudo conntrack -L -p gre(stale entries for your box's IP during a failed redial confirm it) and in the containerdocker exec v2prodock-vpn tail -50 /var/log/ppp-pptp.log(a redial that never logs[pptp-ip-up]died before PPP β network/conntrack, not auth).
Server = VPN_DOMAIN (UDP 500/4500), EAP-MSCHAPv2 with the same
username/password, after trusting config/vpn/ca.crt. Apple devices can use
config/vpn/apple.mobileconfig instead. Connect using exactly VPN_DOMAIN
(or a name/IP listed in VPN_EXTRA_SANS) β anything else fails server-identity
validation.
The gateway needs NET_ADMIN + MKNOD + SYS_ADMIN, /dev/net/tun, and
UDP 500/4500/1701 (+ TCP 1723 when PPTP is on, see compose) β least
privilege verified, no --privileged. The host kernel needs PPP support
(modprobe ppp_generic, plus ppp_mppe for PPTP and nf_conntrack_pptp
for GRE passthrough β install.sh loads all of these).
PPP diagnostics: docker exec v2prodock-vpn cat /var/log/ppp.log
(L2TP) / /var/log/ppp-pptp.log (PPTP).
Single client stalled while others work: toggle VPN off/on on that device
(child-SA desync is DPD-blind; the server also recycles DATA SAs every
30 min regardless).
Client-side kill switch (essential): our gateway is fail-closed, but a
device whose own VPN SA dies will happily send traffic direct. On Android
enable βBlock connections without VPNβ, on Windows bind sensitive apps
with firewall rules to the VPN interface, on iOS use OnDemand mode. Without
this, no VPN provider can promise no-leak on the device itself.
Docker publishes the ports, but a default-deny host firewall still blocks
them first. install.sh opens them automatically on ufw/firewalld
when VPN_ENABLED=1; otherwise do it by hand:
sudo ufw allow 500,4500,1701/udp
# or: sudo firewall-cmd --permanent --add-port={500,4500,1701}/udp && sudo firewall-cmd --reload
# PPTP only (LAN-only!): sudo ufw allow from 192.168.0.0/16 to any port 1723 proto tcpCloud VMs need the same three UDP ports in the provider's security group.
PPTP needs TCP 1723 restricted to your LAN in the host firewall β
never open it to 0.0.0.0/0 (PPTP crypto is broken); GRE (proto 47) must
also pass where the VM firewall filters by protocol.
IP protocol ESP (50) is not required anywhere β all IPsec is forced
through UDP/4500 encapsulation, which is also what makes same-LAN clients
work (raw ESP cannot cross Docker's port NAT into the container).
Example: project runs in a VM at 192.168.1.100, legacy box on the same
LAN connects L2TP to server 192.168.1.100 with your user/pass (+ PSK for
L2TP/IPsec, none for opt-in bare L2TP). Set VPN_DOMAIN=192.168.1.100 so
the server identity matches what clients dial. No host sysctls or forwarding
setup needed β the container handles its own networking.
WSL2 note: stock WSL2 is NAT mode (172.x private IP), so physical LAN
devices cannot reach services inside a WSL2 distro directly. This project
runs on Docker Desktop (ports published on the Windows host's own
interfaces, LAN-reachable), which we verified end-to-end from inside WSL2
(IKE handshake + L2TP control + generic UDP all answer). If you instead run
dockerd inside WSL2 itself, LAN devices need netsh interface portproxy
relays for UDP 500/4500/1701 from the Windows host into WSL2.
sudo bash install.sh # Install & start
sudo bash install.sh start # Start
sudo bash install.sh stop # Stop
sudo bash install.sh status # Show status
sudo bash install.sh logs # Follow logs
sudo bash install.sh uninstall # Remove everything- Fetches subscription URL(s) and parses vless/vmess/trojan/shadowsocks links
- Converts each to an xray-core JSON outbound config
- Distributes configs across N instances (round-robin)
- Each instance: starts xray, tests configs, keeps the first working one
- Health checks run every 60s per instance β on failure, switches to next config
- API returns alive proxies sorted by latency β dead ones excluded
- Subscriptions re-fetched every 120s for updated server lists
| Protocol | Transport |
|---|---|
| VLESS | TCP, WebSocket, gRPC, Reality |
| VMess | TCP, WebSocket, gRPC |
| Trojan | TCP, WebSocket |
| Shadowsocks | TCP |
Environment variables (set in .env or via docker-compose):
| Variable | Default | Description |
|---|---|---|
SUBSCRIPTION_URL |
β | Single v2ray subscription URL (see V2RayDAR to self-host one) |
SUBSCRIPTION_URLS |
β | Comma-separated URLs (one per instance, overrides SUBSCRIPTION_URL) |
PROXY_INSTANCES |
1 |
Number of xray instances to run |
PORT_BASE |
27019 |
Base port: N SOCKS5 ports, then N HTTP ports (SOCKS=base+i, HTTP=base+N+i) |
API_PORT |
27018 |
Port for the HTTP API |
HEALTH_CHECK_URL |
http://api.ipify.org |
URL used to test proxy connectivity |
XRAY_DIR |
/root/xray |
Path to xray binary directory |
GOGC |
100 |
Go GC target percentage (lower = more frequent GC, less memory) |
GOMEMLIMIT |
128MiB |
Go soft memory limit (prevents OOM by triggering aggressive GC) |
MAX_CONNS |
128 |
Max concurrent HTTP CONNECT relay connections |
VPN_ENABLED |
0 |
1 = serve VPN via working proxy |
VPN_DOMAIN |
vpn.local |
Hostname/IP clients connect to (server cert SAN) |
VPN_SUBNET |
10.10.10.0/24 |
IKEv2 client pool (forwarded to tun0 only) |
VPN_L2TP_NET |
10.10.11.0/24 |
L2TP client pool (forwarded to tun0 only) |
VPN_ENABLE_PPTP |
0 |
1 = serve PPTP on TCP 1723+GRE (ancient LAN devices only, MPPE-128 mandatory) |
VPN_PPTP_NET |
10.10.12.0/24 |
PPTP client pool (forwarded to tun0 only) |
VPN_DNS |
1.1.1.1,8.8.8.8 |
DNS pushed to clients (also via proxy) |
VPN_USER / VPN_PASSWORD |
β | EAP-MSCHAPv2 + PPP creds (or VPN_USERS=u1:p1,u2:p2) |
VPN_IPSEC_PSK |
β | IPsec pre-shared key for L2TP clients |
You need a v2ray subscription URL to get started. V2RayDAR fetches configs from public repos and serves them as a single subscription endpoint β run it on your server and point to:
http://192.168.x.x:27141/subscription
Docker note: Inside a container,
127.0.0.1refers to the container itself, not your host. Usehost.docker.internal(Docker 20.10+) or your host's LAN IP instead. The compose file already includesextra_hostsforhost.docker.internalsupport on Linux.
WSL2 note: If V2RayDAR runs on Windows and V2ProDock runs in WSL2/Docker,
host.docker.internalwon't reach the Windows host. The install script auto-detects WSL2 and replaces it with the correct Windows IP. To fix manually:WIN_HOST=$(ip route show default | awk '/default/ {print $3}') sed -i "s|host.docker.internal|$WIN_HOST|g" .env bash install.sh restart
You can also add more public subscription URLs directly via SUBSCRIPTION_URLS in .env β comma-separated, one per instance.
For devices with limited RAM (256MB-512MB):
# .env β conservative defaults that won't OOM
GOGC=100
GOMEMLIMIT=128MiB
MAX_CONNS=64
PROXY_INSTANCES=1# .env β aggressive for VPS with 1GB+ RAM
GOGC=50
GOMEMLIMIT=256MiB
MAX_CONNS=256
PROXY_INSTANCES=3MIT
