This post documents a simple, stable setup I’m currently using on a VPS. No protocol comparison, no theory — just a configuration that works reliably on mobile networks (iOS) using a single public port.
Client (VLESS TCP+TLS)
↓
V2Ray
↓
Shadowsocks (internal)
Only port 443/tcp is exposed to the internet.
Prerequisites
- A domain pointing to the VPS public IPv4 (DNS only, no CDN proxy)
- Port
443/tcp open on the VPS
Generate VLESS UUID
cat /proc/sys/kernel/random/uuid
Use this UUID in:
- V2Ray config
- Client import URL
TLS Certificate (Certbot)
sudo apt update
sudo apt install -y certbot
sudo certbot certonly --standalone -d your.domain.com
Certificates are stored under:
/opt/ssl/live/your.domain.com/
├─ fullchain.pem
└─ privkey.pem
If certbot used /etc/letsencrypt, copy once and keep paths consistent:
sudo mkdir -p /opt/ssl
sudo cp -a /etc/letsencrypt /opt/ssl/
Permissions:
sudo chmod 755 /opt/ssl
sudo chmod -R 755 /opt/ssl/live /opt/ssl/archive
sudo chmod 644 /opt/ssl/live/your.domain.com/fullchain.pem
sudo chmod 600 /opt/ssl/live/your.domain.com/privkey.pem
Docker Compose
~/shadowsocks/docker-compose.yml
services:
shadowsocks:
image: shadowsocks/shadowsocks-libev
restart: unless-stopped
expose:
- "8388"
command: >
ss-server
-s 0.0.0.0
-p 8388
-m chacha20-ietf-poly1305
-k 'PASSWORD'
Notes:
- Shadowsocks is internal only
- V2Ray connects via Docker DNS name
shadowsocks - Public surface =
443/tcp only
V2Ray Configuration
/opt/v2ray/config.json
{
"log": { "loglevel": "warning" },
"inbounds": [{
"port": 443,
"protocol": "vless",
"settings": {
"clients": [{ "id": "UUID-HERE" }],
"decryption": "none"
},
"streamSettings": {
"network": "tcp",
"security": "tls",
"tlsSettings": {
"alpn": ["http/1.1", "h2"],
"certificates": [{
"certificateFile": "/opt/ssl/live/your.domain.com/fullchain.pem",
"keyFile": "/opt/ssl/live/your.domain.com/privkey.pem"
}]
}
}
}],
"outbounds": [{
"protocol": "shadowsocks",
"settings": {
"servers": [{
"address": "shadowsocks",
"port": 8388,
"method": "chacha20-ietf-poly1305",
"password": "PASSWORD"
}]
}
}]
}
sudo chmod 755 /opt/v2ray
sudo chmod 644 /opt/v2ray/config.json
Start Containers
cd ~/shadowsocks
docker compose up -d
Verify:
docker ps
docker logs v2ray --tail=30
Client Import (iOS / v2Box)
vless://[email protected]:443?encryption=none&security=tls&type=tcp&sni=your.domain.com&alpn=http/1.1
Import → Connect.
Certificate Renewal
Certbot enables automatic renewal via a systemd timer. After renewal, Docker containers must be restarted to load new certificates.
Deploy hook:
sudo mkdir -p /etc/letsencrypt/renewal-hooks/deploy
sudo nano /etc/letsencrypt/renewal-hooks/deploy/restart-v2ray.sh
#!/bin/sh
docker restart v2ray >/dev/null 2>&1 || true
sudo chmod +x /etc/letsencrypt/renewal-hooks/deploy/restart-v2ray.sh
Notes
- VLESS over TCP+TLS behaves closer to normal HTTPS
- More reliable than WS on mobile networks
- Simple structure, easy to maintain
This setup is stable enough that I no longer touch it — this post is just a record.
This post documents a simple, stable setup I’m currently using on a VPS. No protocol comparison, no theory — just a configuration that works reliably on mobile networks (iOS) using a single public port.
Only port
443/tcpis exposed to the internet.Prerequisites
443/tcpopen on the VPSGenerate VLESS UUID
Use this UUID in:
TLS Certificate (Certbot)
Certificates are stored under:
If certbot used
/etc/letsencrypt, copy once and keep paths consistent:Permissions:
Docker Compose
~/shadowsocks/docker-compose.ymlNotes:
shadowsocks443/tcponlyV2Ray Configuration
/opt/v2ray/config.jsonStart Containers
Verify:
Client Import (iOS / v2Box)
Import → Connect.
Certificate Renewal
Certbot enables automatic renewal via a systemd timer. After renewal, Docker containers must be restarted to load new certificates.
Deploy hook:
Notes
This setup is stable enough that I no longer touch it — this post is just a record.
Read Next
Caddy as the Front Door of My Homelab
Homelab: Self-Hosting GitLab
Homelab: WireGuard VPN (Docker)
Homelab: OpenVPN AS (Docker)
Launch your OpenVPN Access Server instantly with this ready-to-use Docker Compose file. Skip the complex setup - just copy, paste, and get a fully functional VPN server with local network access.