Complete guide for deploying a V2Ray VLESS-H2-TLS server on AWS EC2 using Systems Manager (SSM)
- Prerequisites
- Variables to Customize
- Step-by-Step Deployment
- DNS Configuration
- SSM Communication Guide
- Clash Client Configuration
- Management Commands
- Troubleshooting
- Cost Information
Before starting the deployment, ensure you have:
- AWS CLI installed and configured with appropriate credentials
- Domain name with ability to create DNS A records
- jq (JSON processor) for parsing AWS CLI outputs (optional but recommended)
- SSH client (optional, we use SSM instead)
- Active AWS account with billing enabled
- IAM permissions to:
- Create IAM roles and policies
- Launch EC2 instances
- Create security groups
- Use Systems Manager (SSM)
- Basic understanding of:
- AWS EC2 and IAM
- DNS configuration
- Command-line operations
- VPN/proxy concepts
IMPORTANT: Replace these variables with your own values before running commands.
| Variable | Example Value | Description |
|---|---|---|
DOMAIN_NAME |
my.server.com |
Your fully qualified domain name |
INSTANCE_NAME |
V2Ray-Server |
Name tag for your EC2 instance |
REGION |
us-east-1 |
AWS region for deployment |
KEY_PAIR |
your-key-name |
EC2 key pair name (optional with SSM) |
AMI_ID |
ami-0e2c8caa4b6378d8c |
Ubuntu 22.04 LTS AMI for your region |
ROLE_NAME |
V2Ray-SSM-Role |
IAM role name for SSM access |
SECURITY_GROUP_NAME |
V2Ray-Security-Group |
Security group name |
This role allows the EC2 instance to communicate with AWS Systems Manager without requiring SSH access.
Create ec2-trust-policy.json:
cat > ec2-trust-policy.json << 'EOF'
{
"Version": "2012-10-17",
"Statement": [{
"Effect": "Allow",
"Principal": {"Service": "ec2.amazonaws.com"},
"Action": "sts:AssumeRole"
}]
}
EOFaws iam create-role \
--role-name V2Ray-SSM-Role \
--assume-role-policy-document file://ec2-trust-policy.json \
--description "Role for V2Ray EC2 instance to use SSM"aws iam attach-role-policy \
--role-name V2Ray-SSM-Role \
--policy-arn arn:aws:iam::aws:policy/AmazonSSMManagedInstanceCoreaws iam create-instance-profile \
--instance-profile-name V2Ray-SSM-InstanceProfileaws iam add-role-to-instance-profile \
--instance-profile-name V2Ray-SSM-InstanceProfile \
--role-name V2Ray-SSM-Role⏱️ Wait time: 10-15 seconds for IAM propagation
sleep 15Configure firewall rules to allow HTTP (80), HTTPS (443), and optionally SSH (22).
VPC_ID=$(aws ec2 describe-vpcs \
--filters "Name=isDefault,Values=true" \
--query "Vpcs[0].VpcId" \
--output text)
echo "VPC ID: $VPC_ID"SG_ID=$(aws ec2 create-security-group \
--group-name V2Ray-Security-Group \
--description "Security group for V2Ray server - HTTPS and HTTP" \
--vpc-id $VPC_ID \
--query 'GroupId' \
--output text)
echo "Security Group ID: $SG_ID"# Allow HTTPS (443) - Required for V2Ray
aws ec2 authorize-security-group-ingress \
--group-id $SG_ID \
--protocol tcp \
--port 443 \
--cidr 0.0.0.0/0
# Allow HTTP (80) - Required for Let's Encrypt certificate validation
aws ec2 authorize-security-group-ingress \
--group-id $SG_ID \
--protocol tcp \
--port 80 \
--cidr 0.0.0.0/0
# Optional: Allow SSH (22) - Only if you need direct SSH access
# aws ec2 authorize-security-group-ingress \
# --group-id $SG_ID \
# --protocol tcp \
# --port 22 \
# --cidr 0.0.0.0/0This script runs automatically when the instance boots, installing necessary packages and configuring the SSM agent.
Create user-data.sh:
cat > user-data.sh << 'EOF'
#!/bin/bash
set -x
exec > >(tee /var/log/user-data.log) 2>&1
apt-get update
apt-get upgrade -y
apt-get install -y curl wget unzip expect
snap start amazon-ssm-agent || systemctl start snap.amazon-ssm-agent.amazon-ssm-agent.service
systemctl enable snap.amazon-ssm-agent.amazon-ssm-agent.service
timedatectl set-timezone UTC
apt-get install -y chrony
systemctl enable chrony
systemctl start chrony
echo "User data completed at $(date)" >> /var/log/user-data.log
EOFWhat this script does:
- Updates system packages
- Installs
curl,wget,unzip, andexpect(needed for V2Ray automation) - Starts and enables SSM agent
- Configures UTC timezone
- Installs and enables chrony for time synchronization (critical for TLS)
aws ec2 describe-images \
--owners 099720109477 \
--filters "Name=name,Values=ubuntu/images/hvm-ssd/ubuntu-jammy-22.04-amd64-server-*" \
--query "Images | sort_by(@, &CreationDate) | [-1].[ImageId,Name,CreationDate]" \
--output tableINSTANCE_ID=$(aws ec2 run-instances \
--image-id ami-0e2c8caa4b6378d8c \
--instance-type t3.micro \
--security-group-ids $SG_ID \
--iam-instance-profile Name=V2Ray-SSM-InstanceProfile \
--user-data file://user-data.sh \
--tag-specifications "ResourceType=instance,Tags=[{Key=Name,Value=V2Ray-Server}]" \
--block-device-mappings '[{"DeviceName":"/dev/sda1","Ebs":{"VolumeSize":8,"VolumeType":"gp3","DeleteOnTermination":true}}]' \
--query 'Instances[0].InstanceId' \
--output text)
echo "Instance ID: $INSTANCE_ID"Instance specifications:
- Type:
t3.micro(2 vCPUs, 1 GB RAM) - recommended for reliable VPN operation - Storage: 8 GB GP3 (cost-effective)
- OS: Ubuntu 22.04 LTS
t3.nano (0.5 GB RAM) can technically run V2Ray, it may experience memory pressure and potential OOM (Out Of Memory) issues under moderate load. The t3.micro with 1 GB RAM is the recommended minimum for stable operation and only costs an additional ~$3.80/month.
aws ec2 wait instance-running --instance-ids $INSTANCE_ID
echo "Instance is running!"PUBLIC_IP=$(aws ec2 describe-instances \
--instance-ids $INSTANCE_ID \
--query 'Reservations[0].Instances[0].PublicIpAddress' \
--output text)
echo "Public IP: $PUBLIC_IP"An Elastic IP provides a permanent, static public IP address that won't change when you stop/start your instance. This is critical for maintaining consistent DNS configuration.
Benefits:
- Persistent IP: IP address remains constant even after instance stop/start
- No DNS updates needed: Your DNS configuration never needs to change
- Cost-effective: FREE while associated with a running instance
- Professional setup: Standard practice for production deployments
Without Elastic IP:
- IP changes every time you stop/start the instance
- Must update DNS records after each IP change
- DNS propagation delays (5-60 minutes)
- Service interruptions during DNS updates
If you want to manage Elastic IPs programmatically, attach the Elastic IP policy to your IAM role.
Create elastic-ip-policy.json:
cat > elastic-ip-policy.json << 'EOF'
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"ec2:AllocateAddress",
"ec2:AssociateAddress",
"ec2:DescribeAddresses",
"ec2:DisassociateAddress",
"ec2:ReleaseAddress"
],
"Resource": "*"
}
]
}
EOFAttach to your IAM role:
# Create the policy
POLICY_ARN=$(aws iam create-policy \
--policy-name V2Ray-ElasticIP-Policy \
--policy-document file://elastic-ip-policy.json \
--query 'Policy.Arn' \
--output text)
# Attach to role
aws iam attach-role-policy \
--role-name V2Ray-SSM-Role \
--policy-arn $POLICY_ARN# Allocate a new Elastic IP
ELASTIC_IP_ALLOCATION=$(aws ec2 allocate-address \
--domain vpc \
--query 'AllocationId' \
--output text)
echo "Elastic IP Allocation ID: $ELASTIC_IP_ALLOCATION"
# Get the actual IP address
ELASTIC_IP=$(aws ec2 describe-addresses \
--allocation-ids $ELASTIC_IP_ALLOCATION \
--query 'Addresses[0].PublicIp' \
--output text)
echo "Elastic IP Address: $ELASTIC_IP"# Associate the Elastic IP with your instance
ASSOCIATION_ID=$(aws ec2 associate-address \
--instance-id $INSTANCE_ID \
--allocation-id $ELASTIC_IP_ALLOCATION \
--query 'AssociationId' \
--output text)
echo "Association ID: $ASSOCIATION_ID"
echo "✅ Elastic IP $ELASTIC_IP is now associated with instance $INSTANCE_ID"# Verify the Elastic IP is associated
aws ec2 describe-addresses \
--allocation-ids $ELASTIC_IP_ALLOCATION \
--query 'Addresses[0].[PublicIp,InstanceId,AssociationId]' \
--output tableExpected output: Should show your Elastic IP, instance ID, and association ID.
echo "🎉 Your permanent static IP is: $ELASTIC_IP"
echo "Use this IP address for your DNS A record in the next step."💡 Important Notes:
- Cost: $0.00/month while associated with a running instance
- Cost if unassociated: ~$3.60/month if you stop the instance but keep the IP
- Best practice: Always associate Elastic IPs with running instances or release them when not needed
- This IP is permanent: Use this IP (not the auto-assigned one) for all DNS configuration
Configure your domain to point to the EC2 instance using the Elastic IP.
You must manually configure an A record in your DNS provider to point your domain to the Elastic IP address.
Create the following DNS record:
Type: A
Name: my.server.com (or your subdomain)
Value: [ELASTIC_IP from Step 5.6]
TTL: 300 (5 minutes) or Auto
- Use the Elastic IP (not the auto-assigned IP from Step 4.4)
- The Elastic IP will NEVER change, even if you stop/start the instance
- If your DNS provider offers proxy/CDN features (like Cloudflare), ensure they are DISABLED
- V2Ray uses its own TLS encryption and will not work through a proxy
- Set to "DNS only" mode (gray cloud in Cloudflare, not orange)
# Check DNS resolution
dig +short my.server.com
# Or use nslookup
nslookup my.server.com
# Should return your PUBLIC_IP⏱️ Wait time: 1-5 minutes for DNS propagation (depends on TTL)
The instance needs time to boot, run user-data script, and register with Systems Manager.
aws ssm describe-instance-information \
--filters "Key=InstanceIds,Values=$INSTANCE_ID" \
--query "InstanceInformationList[0].PingStatus" \
--output textExpected output: Online
echo "Waiting for SSM agent to come online..."
while true; do
STATUS=$(aws ssm describe-instance-information \
--filters "Key=InstanceIds,Values=$INSTANCE_ID" \
--query "InstanceInformationList[0].PingStatus" \
--output text 2>/dev/null)
if [ "$STATUS" = "Online" ]; then
echo "✅ SSM agent is online!"
break
fi
echo "Status: $STATUS - waiting 10 seconds..."
sleep 10
done⏱️ Wait time: 2-5 minutes typically
Install V2Ray using the popular one-click installation script.
COMMAND_ID=$(aws ssm send-command \
--instance-ids $INSTANCE_ID \
--document-name "AWS-RunShellScript" \
--parameters 'commands=["bash <(wget -qO- -o- https://github.com/233boy/v2ray/raw/master/install.sh)"]' \
--query "Command.CommandId" \
--output text)
echo "Command ID: $COMMAND_ID"aws ssm wait command-executed \
--command-id $COMMAND_ID \
--instance-id $INSTANCE_ID
echo "✅ Installation complete!"aws ssm get-command-invocation \
--command-id $COMMAND_ID \
--instance-id $INSTANCE_ID \
--query "StandardOutputContent" \
--output text⏱️ Wait time: 1-2 minutes
Use the expect script to automate the interactive V2Ray configuration.
Create v2ray-ws-tls-v2.exp:
cat > v2ray-ws-tls-v2.exp << 'EOF'
#!/usr/bin/expect -f
set timeout 300
spawn v2ray change
expect "请选择" {
send "1\r"
}
expect "请选择协议" {
send "7\r"
}
expect "请输入域名" {
send "my.server.com\r"
}
expect eof
EOFWhat this does:
- Option 1: Modify transport/protocol
- Option 7: Select VLESS-H2-TLS protocol
- Input domain name for TLS certificate
my.server.com with your actual domain name!
cat v2ray-ws-tls-v2.exp | base64 > v2ray-ws-tls-v2.exp.b64COMMAND_ID=$(aws ssm send-command \
--instance-ids $INSTANCE_ID \
--document-name "AWS-RunShellScript" \
--parameters commands=["cat > /tmp/v2ray-config.exp << 'EOFEXP'
$(cat v2ray-ws-tls-v2.exp)
EOFEXP
","chmod +x /tmp/v2ray-config.exp","expect /tmp/v2ray-config.exp"] \
--query "Command.CommandId" \
--output text)
echo "Configuration Command ID: $COMMAND_ID"aws ssm wait command-executed \
--command-id $COMMAND_ID \
--instance-id $INSTANCE_ID
echo "✅ Configuration complete!"aws ssm get-command-invocation \
--command-id $COMMAND_ID \
--instance-id $INSTANCE_ID \
--query "StandardOutputContent" \
--output text⏱️ Wait time: 2-3 minutes (includes Let's Encrypt certificate generation)
Get the connection details needed for your Clash client.
COMMAND_ID=$(aws ssm send-command \
--instance-ids $INSTANCE_ID \
--document-name "AWS-RunShellScript" \
--parameters 'commands=["v2ray url"]' \
--query "Command.CommandId" \
--output text)
aws ssm wait command-executed \
--command-id $COMMAND_ID \
--instance-id $INSTANCE_ID
aws ssm get-command-invocation \
--command-id $COMMAND_ID \
--instance-id $INSTANCE_ID \
--query "StandardOutputContent" \
--output textCOMMAND_ID=$(aws ssm send-command \
--instance-ids $INSTANCE_ID \
--document-name "AWS-RunShellScript" \
--parameters 'commands=["v2ray info"]' \
--query "Command.CommandId" \
--output text)
aws ssm wait command-executed \
--command-id $COMMAND_ID \
--instance-id $INSTANCE_ID
aws ssm get-command-invocation \
--command-id $COMMAND_ID \
--instance-id $INSTANCE_ID \
--query "StandardOutputContent" \
--output textSave the output - you'll need these details for Clash configuration.
Ensure V2Ray is running correctly.
COMMAND_ID=$(aws ssm send-command \
--instance-ids $INSTANCE_ID \
--document-name "AWS-RunShellScript" \
--parameters 'commands=["v2ray status"]' \
--query "Command.CommandId" \
--output text)
aws ssm wait command-executed \
--command-id $COMMAND_ID \
--instance-id $INSTANCE_ID
aws ssm get-command-invocation \
--command-id $COMMAND_ID \
--instance-id $INSTANCE_ID \
--query "StandardOutputContent" \
--output textExpected output: V2Ray should be running (active)
You must manually configure an A record in your DNS provider to point your domain to the EC2 public IP address.
Basic requirements:
- Create an A record
- Point it to your EC2 instance's public IP
- Disable any proxy/CDN features (if offered by your provider)
- Use a low TTL (300 seconds) for easier updates
AWS Systems Manager (SSM) allows you to execute commands on EC2 instances without SSH access.
# Send a command
COMMAND_ID=$(aws ssm send-command \
--instance-ids $INSTANCE_ID \
--document-name "AWS-RunShellScript" \
--parameters 'commands=["YOUR_COMMAND_HERE"]' \
--query "Command.CommandId" \
--output text)
# Wait for completion
aws ssm wait command-executed \
--command-id $COMMAND_ID \
--instance-id $INSTANCE_ID
# Get output
aws ssm get-command-invocation \
--command-id $COMMAND_ID \
--instance-id $INSTANCE_ID \
--query "StandardOutputContent" \
--output text- Go to AWS Systems Manager Console
- Click Session Manager or Run Command
- Select AWS-RunShellScript
- Select your instance
- Enter commands in the text box
- Click Run
- View output in the command history
# Check if command finished
aws ssm list-commands \
--command-id $COMMAND_ID \
--query "Commands[0].Status" \
--output textPossible statuses:
Pending- Command is queuedInProgress- Command is executingSuccess- Command completed successfullyFailed- Command failedTimedOut- Command exceeded timeoutCancelled- Command was cancelled
# Get standard output
aws ssm get-command-invocation \
--command-id $COMMAND_ID \
--instance-id $INSTANCE_ID \
--query "StandardOutputContent" \
--output text
# Get error output
aws ssm get-command-invocation \
--command-id $COMMAND_ID \
--instance-id $INSTANCE_ID \
--query "StandardErrorContent" \
--output text
# Get full details (JSON)
aws ssm get-command-invocation \
--command-id $COMMAND_ID \
--instance-id $INSTANCE_IDFor interactive shell access (like SSH but through SSM):
aws ssm start-session --target $INSTANCE_IDRequirements:
- Install Session Manager Plugin
- IAM permissions for
ssm:StartSession
Add this proxy configuration to your clash.yaml file:
proxies:
# V2Ray Server with VLESS-H2-TLS
- name: My V2Ray Server (us-east-1)
server: my.server.com
port: 443
type: vless
uuid: YOUR-UUID-HERE
udp: true
tls: true
servername: my.server.com
alpn:
- h2
network: h2
h2-opts:
host:
- my.server.com
path: /YOUR-UUID-HEREmixed-port: 7890
allow-lan: true
mode: Rule
log-level: info
external-controller: :9090
proxies:
# us-east-1 V2Ray Server
- {
name: My V2Ray Server (us-east-1),
server: my.server.com,
port: 443,
type: vless,
uuid: YOUR-UUID-HERE,
udp: true,
tls: true,
servername: my.server.com,
alpn: [h2],
network: h2,
h2-opts: { host: [my.server.com], path: /YOUR-UUID-HERE },
}
proxy-groups:
# Master proxy selector
- name: 🌏 Select Servers...
type: select
proxies:
- My V2Ray Server (us-east-1)
rules:
- MATCH,🌏 Select Servers...Run this command to extract your configuration:
COMMAND_ID=$(aws ssm send-command \
--instance-ids $INSTANCE_ID \
--document-name "AWS-RunShellScript" \
--parameters 'commands=["v2ray info"]' \
--query "Command.CommandId" \
--output text)
aws ssm wait command-executed \
--command-id $COMMAND_ID \
--instance-id $INSTANCE_ID
aws ssm get-command-invocation \
--command-id $COMMAND_ID \
--instance-id $INSTANCE_ID \
--query "StandardOutputContent" \
--output textLook for:
- UUID: Usually in the format
xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx - Path: Usually
/YOUR-UUID
-
Start Clash with your config:
clash -f clash.yaml
-
Test the proxy:
curl -x http://127.0.0.1:7890 https://api.ipify.org?format=json -
Should return your server's IP, not your local IP
All commands are executed via SSM:
aws ssm send-command \
--instance-ids $INSTANCE_ID \
--document-name "AWS-RunShellScript" \
--parameters 'commands=["v2ray status"]'aws ssm send-command \
--instance-ids $INSTANCE_ID \
--document-name "AWS-RunShellScript" \
--parameters 'commands=["v2ray restart"]'aws ssm send-command \
--instance-ids $INSTANCE_ID \
--document-name "AWS-RunShellScript" \
--parameters 'commands=["v2ray stop"]'aws ssm send-command \
--instance-ids $INSTANCE_ID \
--document-name "AWS-RunShellScript" \
--parameters 'commands=["v2ray start"]'aws ssm send-command \
--instance-ids $INSTANCE_ID \
--document-name "AWS-RunShellScript" \
--parameters 'commands=["v2ray url"]'aws ssm send-command \
--instance-ids $INSTANCE_ID \
--document-name "AWS-RunShellScript" \
--parameters 'commands=["v2ray info"]'aws ssm send-command \
--instance-ids $INSTANCE_ID \
--document-name "AWS-RunShellScript" \
--parameters 'commands=["v2ray qr"]'aws ssm send-command \
--instance-ids $INSTANCE_ID \
--document-name "AWS-RunShellScript" \
--parameters 'commands=["tail -n 100 /var/log/v2ray/access.log"]'aws ssm send-command \
--instance-ids $INSTANCE_ID \
--document-name "AWS-RunShellScript" \
--parameters 'commands=["tail -n 100 /var/log/v2ray/error.log"]'aws ssm send-command \
--instance-ids $INSTANCE_ID \
--document-name "AWS-RunShellScript" \
--parameters 'commands=["cat /var/log/user-data.log"]'aws ssm send-command \
--instance-ids $INSTANCE_ID \
--document-name "AWS-RunShellScript" \
--parameters 'commands=["journalctl -u v2ray -n 100"]'aws ssm send-command \
--instance-ids $INSTANCE_ID \
--document-name "AWS-RunShellScript" \
--parameters 'commands=["v2ray change"]'aws ssm send-command \
--instance-ids $INSTANCE_ID \
--document-name "AWS-RunShellScript" \
--parameters 'commands=["v2ray port 8443"]'aws ssm send-command \
--instance-ids $INSTANCE_ID \
--document-name "AWS-RunShellScript" \
--parameters 'commands=["bash <(wget -qO- -o- https://github.com/233boy/v2ray/raw/master/install.sh)"]'aws ssm send-command \
--instance-ids $INSTANCE_ID \
--document-name "AWS-RunShellScript" \
--parameters 'commands=["free -h && df -h && uptime"]'aws ssm send-command \
--instance-ids $INSTANCE_ID \
--document-name "AWS-RunShellScript" \
--parameters 'commands=["netstat -tlnp | grep v2ray"]'Symptoms:
describe-instance-informationreturns empty or showsConnectionLost- Cannot send commands via SSM
Solutions:
-
Check IAM Role: Ensure instance has the correct IAM role attached
aws ec2 describe-instances \ --instance-ids $INSTANCE_ID \ --query 'Reservations[0].Instances[0].IamInstanceProfile'
-
Verify SSM Agent Status via SSH (if you have SSH access):
systemctl status snap.amazon-ssm-agent.amazon-ssm-agent.service
-
Restart SSM Agent:
aws ssm send-command \ --instance-ids $INSTANCE_ID \ --document-name "AWS-RunShellScript" \ --parameters 'commands=["systemctl restart snap.amazon-ssm-agent.amazon-ssm-agent.service"]'
-
Check instance system log:
aws ec2 get-console-output --instance-id $INSTANCE_ID
Symptoms:
v2ray statusshows inactive/failed- Cannot connect to server
Solutions:
-
Check service status:
aws ssm send-command \ --instance-ids $INSTANCE_ID \ --document-name "AWS-RunShellScript" \ --parameters 'commands=["systemctl status v2ray"]'
-
Check error logs:
aws ssm send-command \ --instance-ids $INSTANCE_ID \ --document-name "AWS-RunShellScript" \ --parameters 'commands=["journalctl -u v2ray -n 50"]'
-
Verify configuration file:
aws ssm send-command \ --instance-ids $INSTANCE_ID \ --document-name "AWS-RunShellScript" \ --parameters 'commands=["cat /etc/v2ray/config.json"]'
-
Restart V2Ray:
aws ssm send-command \ --instance-ids $INSTANCE_ID \ --document-name "AWS-RunShellScript" \ --parameters 'commands=["v2ray restart"]'
Symptoms:
- Connection errors related to TLS/SSL
- Certificate verification failures
Solutions:
-
Verify DNS is resolving correctly:
dig +short my.server.com
Should return your EC2 instance's public IP
-
Check certificate status:
aws ssm send-command \ --instance-ids $INSTANCE_ID \ --document-name "AWS-RunShellScript" \ --parameters 'commands=["ls -la /root/.acme.sh/my.server.com/"]'
-
Renew certificate manually:
aws ssm send-command \ --instance-ids $INSTANCE_ID \ --document-name "AWS-RunShellScript" \ --parameters 'commands=["/root/.acme.sh/acme.sh --renew -d my.server.com --force"]'
-
Check time synchronization (critical for TLS):
aws ssm send-command \ --instance-ids $INSTANCE_ID \ --document-name "AWS-RunShellScript" \ --parameters 'commands=["timedatectl status"]'
Symptoms:
- Clash shows connection timeout or failure
curltests fail
Solutions:
-
Verify security group rules:
aws ec2 describe-security-groups \ --group-ids $SG_ID \ --query 'SecurityGroups[0].IpPermissions'
Ensure ports 80 and 443 are open
-
Test network connectivity:
# Test HTTP curl -I http://my.server.com # Test HTTPS curl -I https://my.server.com
-
Verify V2Ray is listening:
aws ssm send-command \ --instance-ids $INSTANCE_ID \ --document-name "AWS-RunShellScript" \ --parameters 'commands=["netstat -tlnp | grep 443"]'
-
Check V2Ray access logs:
aws ssm send-command \ --instance-ids $INSTANCE_ID \ --document-name "AWS-RunShellScript" \ --parameters 'commands=["tail -f /var/log/v2ray/access.log"]'
Symptoms:
- Clash accepts config but connections fail
- Wrong IP reported by
curl -x
Solutions:
-
Verify UUID and path match server:
aws ssm send-command \ --instance-ids $INSTANCE_ID \ --document-name "AWS-RunShellScript" \ --parameters 'commands=["v2ray info"]'
Compare UUID and path with your Clash config
-
Ensure correct protocol type:
- Must be
vless, notvmess - Network must be
h2(HTTP/2) - TLS must be enabled
- Must be
-
Check Clash logs:
clash -f clash.yaml -d .Look for connection errors or TLS issues
-
Test with minimal config:
proxies: - name: Test server: my.server.com port: 443 type: vless uuid: YOUR-UUID udp: true tls: true network: h2
Symptoms:
- DNS points to old IP
- Connection was working, now fails
Note: This issue should NOT occur if you followed the deployment guide and allocated an Elastic IP in Step 5. Elastic IPs never change.
Solutions:
-
Check if you have an Elastic IP:
aws ec2 describe-addresses \ --filters "Name=instance-id,Values=$INSTANCE_ID" \ --query 'Addresses[0].[PublicIp,AllocationId]' \ --output table
-
If no Elastic IP, allocate one now (recommended):
# Allocate Elastic IP ALLOCATION_ID=$(aws ec2 allocate-address \ --domain vpc \ --query 'AllocationId' \ --output text) # Associate with instance aws ec2 associate-address \ --instance-id $INSTANCE_ID \ --allocation-id $ALLOCATION_ID # Get the new Elastic IP ELASTIC_IP=$(aws ec2 describe-addresses \ --allocation-ids $ALLOCATION_ID \ --query 'Addresses[0].PublicIp' \ --output text) echo "Your permanent Elastic IP: $ELASTIC_IP"
-
Update DNS A record to point to the Elastic IP (see Step 6: Configure DNS)
Symptoms:
- V2Ray service crashes unexpectedly
- Instance becomes unresponsive
- System logs show OOM killer messages
Note: This issue is significantly reduced with t3.micro (1 GB RAM) compared to t3.nano (0.5 GB RAM). If you're using t3.nano, upgrade to t3.micro.
Solutions:
-
Check for OOM events:
aws ssm send-command \ --instance-ids $INSTANCE_ID \ --document-name "AWS-RunShellScript" \ --parameters 'commands=["dmesg | grep -i \"out of memory\"","journalctl --since \"1 hour ago\" | grep -i oom"]'
-
Upgrade to t3.micro (if using t3.nano):
# Stop instance aws ec2 stop-instances --instance-ids $INSTANCE_ID aws ec2 wait instance-stopped --instance-ids $INSTANCE_ID # Upgrade instance type aws ec2 modify-instance-attribute \ --instance-id $INSTANCE_ID \ --instance-type "{\"Value\": \"t3.micro\"}" # Start instance aws ec2 start-instances --instance-ids $INSTANCE_ID
-
Monitor memory usage:
aws ssm send-command \ --instance-ids $INSTANCE_ID \ --document-name "AWS-RunShellScript" \ --parameters 'commands=["free -h","ps aux --sort=-%mem | head -10"]'
-
Check instance CPU/memory:
aws ssm send-command \ --instance-ids $INSTANCE_ID \ --document-name "AWS-RunShellScript" \ --parameters 'commands=["top -bn1 | head -20"]'
-
Upgrade instance type if needed:
# Stop instance aws ec2 stop-instances --instance-ids $INSTANCE_ID aws ec2 wait instance-stopped --instance-ids $INSTANCE_ID # Change instance type aws ec2 modify-instance-attribute \ --instance-id $INSTANCE_ID \ --instance-type "{\"Value\": \"t3.small\"}" # Start instance aws ec2 start-instances --instance-ids $INSTANCE_ID
-
Consider different AWS region:
- Choose region closer to your physical location
- Test with
pingto compare latency
Enable V2Ray debug logging:
aws ssm send-command \
--instance-ids $INSTANCE_ID \
--document-name "AWS-RunShellScript" \
--parameters 'commands=["sed -i \"s/\\\"loglevel\\\": \\\"warning\\\"/\\\"loglevel\\\": \\\"debug\\\"/\" /etc/v2ray/config.json && v2ray restart"]'View debug logs:
aws ssm send-command \
--instance-ids $INSTANCE_ID \
--document-name "AWS-RunShellScript" \
--parameters 'commands=["tail -f /var/log/v2ray/error.log"]'On-Demand Pricing (us-east-1):
- Hourly: $0.0104
- Daily: $0.2496 (24 hours)
- Monthly: ~$7.60 (730 hours)
Other regions may vary:
- eu-west-1 (Ireland):
$0.0116/hour ($8.47/month) - ap-southeast-1 (Singapore):
$0.0128/hour ($9.34/month)
Comparison with t3.nano:
| Instance Type | RAM | Monthly Cost | Recommended Use |
|---|---|---|---|
| t3.nano | 0.5 GB | ~$3.80 | Not recommended - may experience OOM |
| t3.micro | 1 GB | ~$7.60 | Recommended - stable VPN operation ✅ |
| t3.small | 2 GB | ~$15.20 | Overkill for personal VPN use |
Why t3.micro? The additional ~$3.80/month provides double the RAM (1 GB vs 0.5 GB), significantly reducing the risk of Out Of Memory (OOM) errors that can crash your VPN server unexpectedly.
- 8 GB GP3 volume: ~$0.64/month
- IOPS: 3,000 baseline (included, sufficient for VPN)
- Throughput: 125 MB/s (included)
Outbound data transfer:
- First 100 GB/month: $0.09/GB ($9.00 max)
- Next 10 TB/month: $0.085/GB
- Over 10 TB/month: Lower rates apply
Inbound data transfer:
- FREE (all traffic to AWS)
Example usage scenarios:
| Usage Level | Monthly Transfer | Transfer Cost | Total Cost |
|---|---|---|---|
| Light | 20 GB | $1.80 | ~$6.24 |
| Moderate | 50 GB | $4.50 | ~$8.94 |
| Heavy | 100 GB | $9.00 | ~$13.44 |
- Elastic IP (recommended): $0.00/month while associated with running instance, ~$3.60/month if instance stopped but IP reserved
- SSM usage: FREE (no additional cost for Systems Manager)
- CloudWatch logs: First 5 GB ingestion FREE, then $0.50/GB
Note: With the new t3.micro standard and recommended Elastic IP setup, there are no additional monthly costs beyond the instance and data transfer when your instance is running.
Recommended setup (with Elastic IP, light usage):
EC2 t3.micro: $7.60
GP3 Storage: $0.64
Elastic IP: $0.00 (associated with running instance)
Data Transfer: $1.80 (20 GB)
─────────────────────────
TOTAL: ~$10.04/month
Moderate usage (recommended):
EC2 t3.micro: $7.60
GP3 Storage: $0.64
Elastic IP: $0.00 (associated with running instance)
Data Transfer: $4.50 (50 GB)
─────────────────────────
TOTAL: ~$12.74/month
Heavy usage:
EC2 t3.micro: $7.60
GP3 Storage: $0.64
Elastic IP: $0.00 (associated with running instance)
Data Transfer: $9.00 (100 GB)
─────────────────────────
TOTAL: ~$17.24/month
Budget option (t3.nano without Elastic IP - NOT recommended):
EC2 t3.nano: $3.80
GP3 Storage: $0.64
Data Transfer: $1.80 (20 GB)
─────────────────────────
TOTAL: ~$6.24/month
Note: Risk of OOM errors and IP changes on restart
- Use Elastic IP strategically: FREE while instance is running, but costs $3.60/month if instance is stopped. Always release Elastic IPs if you plan to stop the instance for extended periods.
- Stop instance when not in use: Only pay for stopped EBS storage (~$0.64/month). Remember to release or reassociate Elastic IP.
- Monitor data transfer: Use CloudWatch to track bandwidth usage - this is typically your largest variable cost
- Right-size instance: t3.micro provides the right balance of cost and stability; t3.small is overkill for personal VPN use
- Use AWS Free Tier: New accounts get 750 hours/month free for 12 months (t2.micro or t3.micro qualify)
- Avoid unnecessary upgrades: The t3.micro is sufficient for personal use with multiple concurrent connections
Set up billing alarm to avoid surprises:
# Create SNS topic for billing alerts
TOPIC_ARN=$(aws sns create-topic \
--name billing-alerts \
--query 'TopicArn' \
--output text)
# Subscribe your email
aws sns subscribe \
--topic-arn $TOPIC_ARN \
--protocol email \
--notification-endpoint [email protected]
# Create CloudWatch alarm (requires us-east-1 region for billing)
aws cloudwatch put-metric-alarm \
--alarm-name "MonthlyBillingAlarm" \
--alarm-description "Alert when monthly charges exceed $15" \
--namespace "AWS/Billing" \
--metric-name "EstimatedCharges" \
--dimensions Name=Currency,Value=USD \
--statistic Maximum \
--period 21600 \
--evaluation-periods 1 \
--threshold 15 \
--comparison-operator GreaterThanThreshold \
--alarm-actions $TOPIC_ARN \
--region us-east-1View current month charges:
# Get current month charges (requires AWS Cost Explorer enabled)
aws ce get-cost-and-usage \
--time-period Start=$(date -u +%Y-%m-01),End=$(date -u +%Y-%m-%d) \
--granularity MONTHLY \
--metrics "UnblendedCost" \
--group-by Type=SERVICE# Get instance ID
INSTANCE_ID=i-xxxxxxxxxxxxx
# Check SSM status
aws ssm describe-instance-information \
--filters "Key=InstanceIds,Values=$INSTANCE_ID"
# Quick send command
aws ssm send-command \
--instance-ids $INSTANCE_ID \
--document-name "AWS-RunShellScript" \
--parameters 'commands=["COMMAND"]'
# Check V2Ray status
# ... send-command with: v2ray status
# Get V2Ray info
# ... send-command with: v2ray info
# Restart V2Ray
# ... send-command with: v2ray restart
# View logs
# ... send-command with: tail -n 50 /var/log/v2ray/error.log| Purpose | Path |
|---|---|
| V2Ray config | /etc/v2ray/config.json |
| V2Ray binary | /usr/local/bin/v2ray |
| TLS certificates | /root/.acme.sh/your-domain.com/ |
| Access logs | /var/log/v2ray/access.log |
| Error logs | /var/log/v2ray/error.log |
| User-data logs | /var/log/user-data.log |
| V2Ray management script | /usr/local/bin/v2ray (wrapper script) |
-
Use strong domain name: Don't use obvious VPN-related names
-
Keep system updated: Regularly update packages
aws ssm send-command \ --instance-ids $INSTANCE_ID \ --document-name "AWS-RunShellScript" \ --parameters 'commands=["apt-get update && apt-get upgrade -y"]'
-
Monitor access logs: Check for suspicious activity
-
Rotate UUID periodically: Change UUID every few months for better security
-
Use firewall rules: Only open necessary ports (80, 443)
-
Enable AWS CloudTrail: Track API calls for audit purposes
-
Don't share configuration: Each user should have their own UUID
-
Enable CloudWatch alarms for unusual traffic:
aws cloudwatch put-metric-alarm \ --alarm-name "HighNetworkOut" \ --alarm-description "Alert on high network out" \ --namespace "AWS/EC2" \ --metric-name "NetworkOut" \ --dimensions Name=InstanceId,Value=$INSTANCE_ID \ --statistic Average \ --period 300 \ --evaluation-periods 2 \ --threshold 1000000000 \ --comparison-operator GreaterThanThreshold
-
Use AWS WAF (optional, for advanced protection):
- Protect against DDoS
- Filter malicious traffic
- Additional cost applies
-
Implement fail2ban (optional):
aws ssm send-command \ --instance-ids $INSTANCE_ID \ --document-name "AWS-RunShellScript" \ --parameters 'commands=["apt-get install -y fail2ban"]'
Save your configuration locally:
# Get V2Ray config
aws ssm send-command \
--instance-ids $INSTANCE_ID \
--document-name "AWS-RunShellScript" \
--parameters 'commands=["cat /etc/v2ray/config.json"]' \
--query "Command.CommandId" \
--output text | \
xargs -I {} aws ssm get-command-invocation \
--command-id {} \
--instance-id $INSTANCE_ID \
--query "StandardOutputContent" \
--output text > v2ray-config-backup.jsonCreate AMI for quick recovery:
# Create AMI
AMI_ID=$(aws ec2 create-image \
--instance-id $INSTANCE_ID \
--name "V2Ray-Backup-$(date +%Y%m%d)" \
--description "V2Ray server backup" \
--query 'ImageId' \
--output text)
echo "AMI ID: $AMI_ID"# Launch new instance from AMI
NEW_INSTANCE_ID=$(aws ec2 run-instances \
--image-id $AMI_ID \
--instance-type t3.micro \
--security-group-ids $SG_ID \
--iam-instance-profile Name=V2Ray-SSM-InstanceProfile \
--query 'Instances[0].InstanceId' \
--output text)
# Associate your existing Elastic IP (recommended)
aws ec2 associate-address \
--instance-id $NEW_INSTANCE_ID \
--allocation-id $ELASTIC_IP_ALLOCATION
# If no Elastic IP, update DNS to point to new instance
# ... (follow DNS configuration steps from Step 6)When you want to terminate the server:
-
Stop V2Ray service:
aws ssm send-command \ --instance-ids $INSTANCE_ID \ --document-name "AWS-RunShellScript" \ --parameters 'commands=["v2ray stop"]'
-
Terminate instance:
aws ec2 terminate-instances --instance-ids $INSTANCE_ID -
Delete security group:
aws ec2 delete-security-group --group-id $SG_ID -
Delete IAM resources:
# Remove role from instance profile aws iam remove-role-from-instance-profile \ --instance-profile-name V2Ray-SSM-InstanceProfile \ --role-name V2Ray-SSM-Role # Delete instance profile aws iam delete-instance-profile \ --instance-profile-name V2Ray-SSM-InstanceProfile # Detach policies aws iam detach-role-policy \ --role-name V2Ray-SSM-Role \ --policy-arn arn:aws:iam::aws:policy/AmazonSSMManagedInstanceCore # Delete role aws iam delete-role --role-name V2Ray-SSM-Role
-
Release Elastic IP (if used):
# Get allocation ID ALLOCATION_ID=$(aws ec2 describe-addresses \ --filters "Name=instance-id,Values=$INSTANCE_ID" \ --query 'Addresses[0].AllocationId' \ --output text) # Release aws ec2 release-address --allocation-id $ALLOCATION_ID
-
Delete DNS record: Remove A record from your DNS provider
- Clash for Windows: https://github.com/Fndroid/clash_for_windows_pkg
- ClashX (macOS): https://github.com/yichengchen/clashX
- Clash for Android: https://github.com/Kr328/ClashForAndroid
- Clash Premium (official): https://github.com/Dreamacro/clash/releases/tag/premium
- V2Ray Config Generator: https://www.v2ray.com/tools/
- UUID Generator: https://www.uuidgenerator.net/
- DNS Checker: https://dnschecker.org/
- IP Location Checker: https://ipinfo.io/
Major Updates:
-
Changed standard instance type from t3.nano to t3.micro
- Doubled RAM from 0.5 GB to 1 GB for improved stability
- Significantly reduces Out Of Memory (OOM) errors
- Additional cost: ~$3.80/month for better reliability
-
Added required Elastic IP allocation step (new Step 5)
- Prevents IP address changes on instance stop/start
- Eliminates need for DNS updates after maintenance
- FREE while instance is running
- Includes complete setup instructions and IAM policy
-
Renumbered all deployment steps
- Original Step 5 (DNS) → Step 6
- Original Step 6 (SSM) → Step 7
- Original Steps 7-10 → Steps 8-11
-
Updated DNS configuration guidance
- Emphasizes using Elastic IP instead of auto-assigned IP
- Added warnings about IP permanence
- Clarified Cloudflare proxy settings
-
Updated cost calculations throughout
- t3.micro pricing: ~$7.60/month (vs t3.nano $3.80/month)
- Updated total cost estimates for all usage tiers
- Added Elastic IP cost breakdown
- New recommended setup: ~$10-17/month depending on data transfer
-
Enhanced troubleshooting section
- Added Issue 7: Out Of Memory (OOM) Errors
- Updated Issue 6 with Elastic IP guidance
- Added upgrade instructions from t3.nano to t3.micro
-
Updated examples and references
- Backup/restore commands now use t3.micro
- Cost optimization tips reflect new recommendations
- Initial release
- Complete deployment playbook for V2Ray VLESS-H2-TLS
- AWS SSM-based management without SSH
- Comprehensive troubleshooting guide
- Cost analysis and optimization tips
This playbook is based on the V2Ray deployment process completed on 2026-01-03, updated on 2026-01-04.
Tested with:
- AWS Region: us-east-1
- Instance Type: t3.micro (recommended), t3.nano (deprecated)
- OS: Ubuntu 22.04 LTS
- V2Ray: 233boy script (latest)
- Protocol: VLESS-H2-TLS
- Client: Clash
For issues or improvements, refer to the official V2Ray and AWS documentation.
End of Playbook
Last updated: 2026-01-04 Deployment time: ~20 minutes (including Elastic IP allocation and DNS propagation)