Imported from lawnvi/funny (
skills/create-singbox-vpn/SKILL.md). Install upstream withnpx skills add lawnvi/funny --skill create-singbox-vpn. Copyright stays with the author.
Create Sing-box VPN
Overview
Deploy a self-contained Sing-box node with VLESS Reality on TCP 443 and AnyTLS on TCP 8443. Caddy serves the HTTP camouflage site and maintains the trusted AnyTLS certificate. The user does not need to own a domain: if they do not provide one, derive an IP-based domain such as <server-ip>.sslip.io and verify it points to the server. The agent should execute the workflow step by step over SSH and use the bundled templates in assets/; do not depend on the original project directory or any external documentation.
Required Inputs
Start every deployment by asking the user for the missing information before touching the server. Keep it concise and accept defaults:
- SSH host/IP, port, and user.
- SSH key path if the default key is not enough. Recommend that the user configure key-based, passwordless SSH first; do not ask them to paste a server password.
- Install directory. Default to
/home/<user>/singbox; forroot, use/root/singboxunless the user explicitly wants another path. - Camouflage HTML direction:
notes,docs,studio,gallery, or a free-form idea; also ask for a short English site title. - Optional custom AnyTLS domain. If provided, its A/AAAA record must already point to this server. If omitted, automatically derive an IP-based domain such as
<server-ip>.sslip.ioand still deploy AnyTLS by default. - Optional public server address for Reality if different from the SSH host. If omitted, use the SSH host/IP.
- Optional Reality camouflage domain override. Usually omit it; the workflow should choose a region-matched public HTTPS domain after detecting the server location.
- Optional AnyTLS password. If omitted, generate a strong random password.
- Optional proxy name. If omitted, detect a likely location using cloud metadata first, then multiple GeoIP sources; never rely on
cip.ccalone. - Optional subscription name. On a redeploy, reuse the existing hosted subscription filename by default. For a new deployment, default to a readable slug from the proxy name, such as
singbox-korea-seoul. - Whether to skip the subscription URL. By default, publish a Mihomo/Clash subscription YAML under Caddy with a readable filename and a random
?token=query string so the user can paste the full URL directly into the client. On a redeploy, do not change the existing subscription URL unless the user explicitly asks to rotate it.
Ask one concise grouped question if any required input is missing, for example:
请给我服务器登录信息和伪装页方向:SSH host/IP、port(默认22)、user、ssh key路径(如需)、安装目录(默认/home/<user>/singbox,root则/root/singbox)、伪装页方向(notes/docs/studio/gallery或一句描述)、英文站点标题。可选:自定义AnyTLS域名(不提供则自动用IP.sslip.io)、Reality对外连接域名/IP、Reality伪装域名覆盖值、客户端节点名、订阅名、AnyTLS密码。
Do not ask for a server password; if SSH key login is not ready, ask the user to configure it first.
Do not present the AnyTLS domain as a required question. A custom domain is optional. If it is missing, tell the user briefly that the workflow will try an auto-derived IP hostname such as <server-ip>.sslip.io for AnyTLS.
Local Variables
Use these placeholders consistently while executing:
HOST="203.0.113.10"
PORT="22"
USER="ubuntu"
INSTALL_DIR="/home/ubuntu/singbox"
SERVER_ADDRESS="$HOST"
ANYTLS_DOMAIN=""
ENABLE_ANYTLS=1
REALITY_SERVER_NAME=""
SSH_BASE=(ssh -p "$PORT" -o BatchMode=yes -o StrictHostKeyChecking=accept-new "$USER@$HOST")
Enable AnyTLS by default. If the user provides ANYTLS_DOMAIN, use it. If they omit it and SERVER_ADDRESS is an IPv4 address, derive ANYTLS_DOMAIN="$SERVER_ADDRESS.sslip.io" and verify it resolves back to SERVER_ADDRESS from the server. If that does not resolve correctly, try "${SERVER_ADDRESS//./-}.sslip.io" and then "$SERVER_ADDRESS.nip.io". If no derived domain resolves correctly, tell the user that AnyTLS needs a resolvable certificate domain and continue with VLESS Reality only by setting ENABLE_ANYTLS=0.
Public ACME certificates cannot be issued for a bare IP address; the derived sslip.io or nip.io hostname is the certificate domain. Do not ask the user for a custom domain unless auto-derived domains fail or the user explicitly wants their own hostname.
AnyTLS Domain Choices
Use this order:
- User-owned domain, best for long-term or shared deployments. Ask the user to create an A/AAAA record pointing to the server, then use that hostname for AnyTLS.
- Auto-derived
sslip.iohostname, best for quick deployment without DNS setup. Example:203.0.113.10.sslip.io. - Auto-derived
nip.iohostname as a fallback ifsslip.ioresolution or certificate issuance fails. - Reality-only fallback if no AnyTLS hostname can be verified or if the user explicitly disables AnyTLS.
Explain the tradeoff when using sslip.io or nip.io: they are convenience DNS services and VPN traffic does not pass through them, but DNS control and availability belong to that third-party domain. They can also hit shared Let's Encrypt registered-domain rate limits. Recommend a user-owned domain for production, sensitive, or long-lived deployments.
If the user provides an identity file, add -i "$IDENTITY_FILE" to SSH_BASE. If the install directory is omitted, default to /home/<user>/singbox; for root, use /root/singbox unless the user explicitly wants /home/root/singbox.
Run local upload/render commands from the skill directory. If working from another directory, use absolute paths to the files under this skill's assets/ folder.
Reality Camouflage Domain
Do not hard-code www.microsoft.com. Choose a region-matched Reality server_name/handshake domain for every deployment, because repeated use of one large foreign domain across many servers creates an obvious pattern and can mismatch the server's region.
Use this order:
- User-provided
REALITY_SERVER_NAME, if they explicitly give one. - Server location from cloud metadata, then majority GeoIP agreement as described in "Name the Proxy".
- A conservative global fallback only if location cannot be determined.
Set both the Sing-box Reality TLS server_name and Reality handshake.server to the same value. Set the Mihomo/Clash servername to that exact value. Replace __REALITY_SERVER_NAME__ in every server and client template.
Region-matched candidate pool:
| Location signal | Candidate domains |
|---|---|
Korea, Seoul, ap-northeast-2, KR |
www.samsung.com, www.naver.com, www.kakao.com |
Japan, Tokyo, ap-northeast-1, JP |
www.sony.jp, www.rakuten.co.jp, www.yahoo.co.jp |
Singapore, ap-southeast-1, SG |
www.gov.sg, www.dbs.com.sg, www.singtel.com |
Hong Kong, cn-hongkong, HK |
www.hsbc.com.hk, www.hangseng.com, www.gov.hk |
United States, us-east-1, us-west-1, US |
www.apple.com, www.cloudflare.com, www.microsoft.com |
Germany, Frankfurt, eu-central-1, DE |
www.telekom.de, www.bmw.de, www.lufthansa.com |
United Kingdom, London, eu-west-1, GB |
www.bbc.co.uk, www.gov.uk, www.hsbc.co.uk |
| Unknown or disputed | www.cloudflare.com, www.apple.com, www.microsoft.com |
Validate the chosen domain from the server before rendering configs:
for domain in $REALITY_DOMAIN_CANDIDATES; do
if "${SSH_BASE[@]}" "curl -fsSI --connect-timeout 4 --max-time 8 https://$domain/ >/dev/null"; then
REALITY_SERVER_NAME="$domain"
break
fi
done
If all candidates fail, use the first candidate for the detected region and tell the user that HTTPS probing failed but the domain was still selected by region. Do not choose domains that require client certificates, fail normal browser HTTPS, or are unrelated to the detected region unless the user overrides it.
Deployment Workflow
1. Verify SSH
Run:
"${SSH_BASE[@]}" 'printf "ssh ok\n"'
2. Detect or Install Docker Compose
First detect existing Docker/Compose commands:
"${SSH_BASE[@]}" '
if docker compose version >/dev/null 2>&1; then
echo "DOCKER=docker"
echo "COMPOSE=docker compose"
elif docker-compose version >/dev/null 2>&1; then
echo "DOCKER=docker"
echo "COMPOSE=docker-compose"
elif sudo -n docker compose version >/dev/null 2>&1; then
echo "DOCKER=sudo -n docker"
echo "COMPOSE=sudo -n docker compose"
elif sudo -n docker-compose version >/dev/null 2>&1; then
echo "DOCKER=sudo -n docker"
echo "COMPOSE=sudo -n docker-compose"
else
echo "Docker Compose not available or sudo requires a password" >&2
exit 127
fi'
Remember the detected DOCKER and COMPOSE command strings. If detection fails because Docker or Compose is missing, tell the user briefly that Docker/Compose is not present and that the skill will install it automatically, then run the installer below. Do not ask for confirmation.
"${SSH_BASE[@]}" '
set -eu
if [ "$(id -u)" = "0" ]; then
SUDO=""
elif sudo -n true >/dev/null 2>&1; then
SUDO="sudo -n"
else
echo "Docker/Compose is missing and this user cannot run passwordless sudo." >&2
exit 126
fi
if command -v apt-get >/dev/null 2>&1; then
export DEBIAN_FRONTEND=noninteractive
$SUDO apt-get update
$SUDO apt-get install -y ca-certificates curl gnupg docker.io
$SUDO apt-get install -y docker-compose-v2 || \
$SUDO apt-get install -y docker-compose-plugin || \
$SUDO apt-get install -y docker-compose
$SUDO systemctl enable --now docker >/dev/null 2>&1 || true
elif command -v dnf >/dev/null 2>&1; then
$SUDO dnf install -y docker docker-compose-plugin || $SUDO dnf install -y docker docker-compose
$SUDO systemctl enable --now docker >/dev/null 2>&1 || true
elif command -v yum >/dev/null 2>&1; then
$SUDO yum install -y docker docker-compose-plugin || $SUDO yum install -y docker docker-compose
$SUDO systemctl enable --now docker >/dev/null 2>&1 || true
else
echo "Unsupported package manager. Install Docker and Docker Compose manually, then rerun." >&2
exit 127
fi
'
After installation, rerun the detection command and remember the detected DOCKER and COMPOSE values. If it still fails, read references/troubleshooting.md and report the exact package-manager error.
3. Create Directories and Upload Base Templates
Create the remote layout:
"${SSH_BASE[@]}" "mkdir -p '$INSTALL_DIR'/caddy/html '$INSTALL_DIR'/caddy/data '$INSTALL_DIR'/caddy/config '$INSTALL_DIR'/sing-box"
Upload the bundled Docker Compose file:
scp -P "$PORT" assets/docker-compose.yml "$USER@$HOST:$INSTALL_DIR/docker-compose.yml"
If an identity file is required, add -i "$IDENTITY_FILE" to each scp command.
4. Pull Images and Generate Credentials
Run the detected Docker command on the server:
"${SSH_BASE[@]}" "$DOCKER pull caddy:latest && $DOCKER pull ghcr.io/sagernet/sing-box:latest"
UUID=$("${SSH_BASE[@]}" "$DOCKER run --rm ghcr.io/sagernet/sing-box:latest generate uuid" | tail -n 1)
KEYPAIR=$("${SSH_BASE[@]}" "$DOCKER run --rm ghcr.io/sagernet/sing-box:latest generate reality-keypair")
if [ "${ENABLE_ANYTLS:-0}" = "1" ]; then
ANYTLS_PASSWORD=$(python3 - <<'PY'
import secrets
print(secrets.token_urlsafe(32))
PY
)
fi
Extract:
PRIVATE_KEYfrom the line beginning withPrivateKey:orPrivate key:.PUBLIC_KEYfrom the line beginning withPublicKey:orPublic key:.
Do not reveal the private key in the final answer unless the user explicitly asks for server-side recovery details.
4.5. Detect Location and Choose Reality Domain
Before rendering any server or client config, detect the server location using the metadata and GeoIP method in "Name the Proxy". Store a normalized location string such as Korea-Seoul, Japan-Tokyo, or Singapore.
If the user did not provide REALITY_SERVER_NAME, choose REALITY_DOMAIN_CANDIDATES from the "Reality Camouflage Domain" table using that location. Validate candidates from the server with HTTPS probing and set REALITY_SERVER_NAME to the first working region-matched domain. If the location sources disagree, prefer cloud metadata over GeoIP. If there is still no confident location, use the unknown/disputed candidate list and state that the Reality domain was selected from the conservative fallback pool.
5. Render sing-box/config.json
If ENABLE_ANYTLS=1, use assets/sing-box-config.template.json, replace:
__UUID__withUUID.__PRIVATE_KEY__withPRIVATE_KEY.__REALITY_SERVER_NAME__withREALITY_SERVER_NAME.__ANYTLS_DOMAIN__withANYTLS_DOMAIN.__ANYTLS_PASSWORD__withANYTLS_PASSWORD.
Use the Reality-only template only if the user explicitly asks to disable AnyTLS or if no custom/auto-derived AnyTLS domain can be verified. In that case, use assets/sing-box-config.reality-only.template.json and replace only:
__UUID__withUUID.__PRIVATE_KEY__withPRIVATE_KEY.__REALITY_SERVER_NAME__withREALITY_SERVER_NAME.
Prefer a JSON-aware edit such as jq or a short local Python one-liner that parses and writes JSON. Avoid ad hoc shell escaping for credentials; a bad replacement can produce invalid JSON.
Validate the rendered JSON locally before uploading:
jq empty /tmp/config.json >/dev/null 2>&1 || python3 -m json.tool /tmp/config.json >/dev/null
scp -P "$PORT" /tmp/config.json "$USER@$HOST:$INSTALL_DIR/sing-box/config.json"
Use a temp file path that will not be committed.
6. Generate the Camouflage Page
Ask the user for a simple preference if they have not given one. Recommended choices: personal notes, docs/status page, studio/portfolio, or gallery.
Read references/camouflage-html.md before generating or modifying the camouflage page. The page must be a polished English-language single-file HTML document with inline CSS/JS, Google Fonts, meaningful copy, responsive layout, motion details, and no framework dependency.
Use assets/index.template.html as the default base or generate a fresh single-file HTML page following the same standard. Replace:
__SITE_TITLE____HEADLINE____INTRO____CARD_1__,__CARD_2__,__CARD_3____IMAGE_URL____IMAGE_ALT____UPDATED_LABEL__
Keep the page static and plausible. External resources are limited to Google Fonts and image placeholders from unsplash.com or picsum.photos. Upload it to:
scp -P "$PORT" /tmp/index.html "$USER@$HOST:$INSTALL_DIR/caddy/html/index.html"
7. Name the Proxy
If the user did not provide <proxy-name>, detect a location with this priority:
- Cloud provider metadata from inside the server.
- Majority agreement from multiple GeoIP APIs queried for
SERVER_ADDRESS. - User-provided known region, if the sources disagree.
- Fallback to
SingBox-<server-address>instead of guessing.
Reuse the location detected in step 4.5 when it is already available. Do not run a separate, conflicting location decision for the proxy name.
For Alibaba Cloud, query local metadata first:
ALIYUN_REGION=$("${SSH_BASE[@]}" 'curl -fsSL --max-time 3 http://100.100.100.200/latest/meta-data/region-id 2>/dev/null || true')
ALIYUN_ZONE=$("${SSH_BASE[@]}" 'curl -fsSL --max-time 3 http://100.100.100.200/latest/meta-data/zone-id 2>/dev/null || true')
Use this built-in mapping for common Alibaba Cloud region IDs:
| Region ID | Proxy location |
|---|---|
ap-northeast-1 |
Japan-Tokyo |
ap-northeast-2 |
Korea-Seoul |
ap-southeast-1 |
Singapore |
ap-southeast-2 |
Australia-Sydney |
ap-southeast-3 |
Malaysia-KualaLumpur |
ap-southeast-5 |
Indonesia-Jakarta |
ap-southeast-6 |
Philippines-Manila |
ap-southeast-7 |
Thailand-Bangkok |
us-east-1 |
US-Virginia |
us-west-1 |
US-SiliconValley |
eu-central-1 |
Germany-Frankfurt |
eu-west-1 |
UK-London |
me-east-1 |
UAE-Dubai |
cn-hongkong |
HongKong |
If metadata is unavailable or unmapped, compare multiple GeoIP sources:
IPINFO=$(curl -fsSL --max-time 8 "https://ipinfo.io/$SERVER_ADDRESS/json" 2>/dev/null || true)
IP_API=$(curl -fsSL --max-time 8 "http://ip-api.com/json/$SERVER_ADDRESS?fields=status,country,countryCode,regionName,city,isp,org,as,query" 2>/dev/null || true)
IPWHO=$(curl -fsSL --max-time 8 "https://ipwho.is/$SERVER_ADDRESS" 2>/dev/null || true)
CIP_OUTPUT=$(curl -fsSL --max-time 8 "https://cip.cc/$SERVER_ADDRESS" 2>/dev/null || curl -fsSL --max-time 8 "http://cip.cc/$SERVER_ADDRESS" 2>/dev/null || true)
Prefer a country/city that appears in at least two of ipinfo.io, ip-api.com, and ipwho.is. Treat cip.cc as a low-priority hint because it can disagree with cloud metadata and other GeoIP providers. RDAP/WHOIS registration country identifies the IP allocation owner, not necessarily the VM location; use it for organization context, not proxy naming.
Examples:
- Alibaba metadata
ap-northeast-2->SingBox-Korea-Seoul. ipinfo.io,ip-api.com, andipwho.isall report Seoul/KR ->SingBox-Korea-Seoul.- Sources disagree and no metadata exists ->
SingBox-43-108-45-3or ask the user for a preferred proxy name.
If AnyTLS is enabled, set ANYTLS_PROXY_NAME to a readable sibling of the Reality node, such as SingBox-Korea-Seoul-AnyTLS. Keep the random password out of the display name.
8. Generate Client Config, Subscription Link, and Caddyfile
If ENABLE_ANYTLS=1, use assets/mihomo-client.template.yaml, replace:
__PROXY_NAME____ANYTLS_PROXY_NAME____SERVER_ADDRESS____REALITY_SERVER_NAME____ANYTLS_DOMAIN____ANYTLS_PASSWORD____UUID____PUBLIC_KEY____SUBSCRIPTION_NAME____SUBSCRIPTION_TOKEN____GENERATED_AT__
Use the Reality-only client template only if AnyTLS was explicitly disabled or auto-domain derivation failed. In that case, use assets/mihomo-client.reality-only.template.yaml and replace:
__PROXY_NAME____SERVER_ADDRESS____REALITY_SERVER_NAME____UUID____PUBLIC_KEY____SUBSCRIPTION_NAME____SUBSCRIPTION_TOKEN____GENERATED_AT__
This template is intentionally based on the repository's docker/singbox/client.yaml shape and includes rule-providers plus the full rules list. By default, Apple and iCloud rule sets use DIRECT. Do not simplify it to a bare MATCH,PROXY config unless the user explicitly asks for a minimal subscription.
First check whether this install already has a hosted subscription. If it does, reuse the existing subscription filename and token by default so Clash/Mihomo users do not need to edit their subscription URL after a redeploy:
EXISTING_SUBSCRIPTION=$("${SSH_BASE[@]}" "python3 - <<'PY'
from pathlib import Path
import re
base = Path('$INSTALL_DIR')
caddyfile = base / 'caddy' / 'Caddyfile'
html = base / 'caddy' / 'html' / 'subscriptions'
name = ''
token = ''
if caddyfile.exists():
text = caddyfile.read_text(errors='ignore')
path = re.search(r'path\s+/subscriptions/([A-Za-z0-9._-]+)\.yaml', text)
query = re.search(r'query\s+token=([A-Za-z0-9._~+-]+)', text)
if path:
name = path.group(1)
if query:
token = query.group(1)
if not name and html.exists():
files = sorted(html.glob('*.yaml'))
if files:
name = files[0].stem
if name and token:
print(f'{name} {token}')
PY
")
if [ -n "$EXISTING_SUBSCRIPTION" ]; then
SUBSCRIPTION_NAME=${EXISTING_SUBSCRIPTION%% *}
SUBSCRIPTION_TOKEN=${EXISTING_SUBSCRIPTION#* }
fi
If there is no existing hosted subscription, create a readable subscription filename from the user-provided subscription name or the proxy name:
: "${SUBSCRIPTION_NAME:=singbox-korea-seoul}"
Use lowercase letters, digits, and hyphens; strip spaces and punctuation. Keep it short because Clash/Mihomo clients may display the URL filename as the subscription name. Put randomness only in the query token, not in the filename.
Generate an unguessable token locally only when SUBSCRIPTION_TOKEN was not found on the server and the user did not provide one:
if [ -z "${SUBSCRIPTION_TOKEN:-}" ]; then
SUBSCRIPTION_TOKEN=$(python3 - <<'PY'
import secrets
print(secrets.token_urlsafe(32))
PY
)
fi
Never rotate SUBSCRIPTION_NAME or SUBSCRIPTION_TOKEN during a redeploy unless the user explicitly asks to revoke or change the subscription URL. It is fine for a redeploy to regenerate UUIDs, Reality keys, and AnyTLS password; the hosted YAML content changes behind the same stable URL and clients pick it up on refresh.
If ENABLE_ANYTLS=1, render assets/Caddyfile by replacing:
__ANYTLS_DOMAIN____SUBSCRIPTION_NAME____SUBSCRIPTION_TOKEN__
Use the Reality-only Caddyfile only if AnyTLS was explicitly disabled or auto-domain derivation failed. In that case, render assets/Caddyfile.reality-only by replacing:
__SUBSCRIPTION_NAME____SUBSCRIPTION_TOKEN__
Then upload the rendered Caddyfile:
scp -P "$PORT" /tmp/Caddyfile "$USER@$HOST:$INSTALL_DIR/caddy/Caddyfile"
Upload the generated YAML to Caddy by default:
"${SSH_BASE[@]}" "mkdir -p '$INSTALL_DIR/caddy/html/subscriptions'"
scp -P "$PORT" /tmp/client.yaml "$USER@$HOST:$INSTALL_DIR/caddy/html/subscriptions/$SUBSCRIPTION_NAME.yaml"
"${SSH_BASE[@]}" "chmod 644 '$INSTALL_DIR/caddy/html/subscriptions/$SUBSCRIPTION_NAME.yaml'"
The subscription URL is:
http://<SERVER_ADDRESS>/subscriptions/<SUBSCRIPTION_NAME>.yaml?token=<SUBSCRIPTION_TOKEN>
This is not password authentication. It relies on a high-entropy, non-guessable query token enforced by Caddy. Only users who receive the full URL can import the subscription, but anyone with that URL can use the node.
Verify the subscription from the local machine:
curl -fsI "http://$SERVER_ADDRESS/subscriptions/$SUBSCRIPTION_NAME.yaml?token=$SUBSCRIPTION_TOKEN"
curl -fsSL "http://$SERVER_ADDRESS/subscriptions/$SUBSCRIPTION_NAME.yaml?token=$SUBSCRIPTION_TOKEN" | head -n 12
curl -fsI "http://$SERVER_ADDRESS/subscriptions/$SUBSCRIPTION_NAME.yaml" && exit 1 || true
curl -fsI "http://$SERVER_ADDRESS/subscriptions/$SUBSCRIPTION_NAME.yaml?token=wrong" && exit 1 || true
Output both the subscription URL and the generated Mihomo/Clash config to the user. Tell the user the subscription URL can be pasted directly into Clash/Mihomo.
If the user explicitly opts out of a hosted subscription, skip the upload and provide only the YAML content.
9. Start Docker Compose
If ENABLE_ANYTLS=1, start Caddy first so it can obtain and store the AnyTLS certificate before Sing-box tries to read it:
"${SSH_BASE[@]}" "cd '$INSTALL_DIR' && $COMPOSE up -d caddy"
"${SSH_BASE[@]}" "
set -eu
crt='$INSTALL_DIR/caddy/data/caddy/certificates/acme-v02.api.letsencrypt.org-directory/$ANYTLS_DOMAIN/$ANYTLS_DOMAIN.crt'
key='$INSTALL_DIR/caddy/data/caddy/certificates/acme-v02.api.letsencrypt.org-directory/$ANYTLS_DOMAIN/$ANYTLS_DOMAIN.key'
for i in \$(seq 1 60); do
if [ -s \"\$crt\" ] && [ -s \"\$key\" ]; then
echo 'AnyTLS certificate ready'
exit 0
fi
sleep 5
done
echo 'Caddy did not obtain the AnyTLS certificate within 5 minutes' >&2
$DOCKER logs --tail=120 caddy >&2 || true
exit 1
"
If AnyTLS was disabled or auto-domain derivation failed, skip the certificate wait. Then start the full stack:
"${SSH_BASE[@]}" "cd '$INSTALL_DIR' && $COMPOSE up -d"
"${SSH_BASE[@]}" "cd '$INSTALL_DIR' && $COMPOSE ps"
When AnyTLS is enabled, do not expose Caddy on public TCP 443 in this template because VLESS Reality owns 443. The Caddyfile disables the ACME TLS-ALPN challenge and uses HTTP-01 on TCP 80, with Caddy's internal HTTPS port set to 9443 only to trigger certificate automation. Sing-box reads Caddy's persisted certificate files read-only and serves AnyTLS on TCP 8443.
Output Handling
Relay the generated subscription URL and Mihomo config to the user. By default, the subscription contains both the VLESS Reality node on 443 and the AnyTLS node on 8443. It contains only the VLESS Reality node if the user explicitly disabled AnyTLS or auto-domain derivation failed. Do not commit generated client configs, UUIDs, private keys, public keys, AnyTLS passwords, subscription tokens, or rendered server configs.
The subscription URL is the preferred handoff because the user can paste it directly into Clash/Mihomo. Include the credential-leak warning: anyone with the complete URL can import the node.
Verification
After deployment, check:
ssh ... 'cd <install-dir> && docker compose ps'or the compose command detected earlier.ssh ... 'docker logs --tail=80 sing-box'if the client cannot connect.curl -I http://<server-address>/to confirm Caddy serves the camouflage page.curl -fsI "http://<server-address>/subscriptions/<name>.yaml?token=<token>"to confirm the hosted subscription returns200 OK.curl -fsI "http://<server-address>/subscriptions/<name>.yaml"andcurl -fsI "http://<server-address>/subscriptions/<name>.yaml?token=wrong"should not return200 OK.curl -fsSL "http://<server-address>/subscriptions/<name>.yaml?token=<token>"to confirm the subscription YAML contains the expected proxy name,rule-providers, fullrules, and no private key.- If AnyTLS is enabled:
ssh ... 'test -s <install-dir>/caddy/data/caddy/certificates/acme-v02.api.letsencrypt.org-directory/<anytls-domain>/<anytls-domain>.crt'to confirm Caddy issued the certificate. - If AnyTLS is enabled: test TCP 8443 from outside the server and confirm cloud firewall/security group allows it.
- Cloud firewall/security group allows TCP 80 and TCP 443. If AnyTLS is enabled, it must also allow TCP 8443. TCP 80 is required for Caddy and certificate issuance/renewal; TCP 443 is Reality; TCP 8443 is AnyTLS.
- Server time is synchronized; Reality is sensitive to clock drift.
Read references/troubleshooting.md when Docker Compose is missing, ports fail, clients show Reality key errors, or Caddy does not respond.
Bundled Files
assets/docker-compose.yml: Caddy plus Sing-box service layout based on the source project, with Caddy certificate storage mounted read-only into Sing-box.assets/Caddyfile: HTTP static-site Caddy config plus AnyTLS domain certificate automation.assets/Caddyfile.reality-only: HTTP static-site Caddy config without AnyTLS certificate automation.assets/sing-box-config.template.json: VLESS Reality and AnyTLS server template with UUID/private-key/domain/password placeholders.assets/sing-box-config.reality-only.template.json: VLESS Reality-only server template.assets/index.template.html: polished single-file camouflage page template.assets/mihomo-client.template.yaml: Clash/Mihomo client config template with Reality and AnyTLS nodes.assets/mihomo-client.reality-only.template.yaml: Clash/Mihomo client config template with only the Reality node.references/camouflage-html.md: design and technical standard for generated camouflage pages.references/troubleshooting.md: operational notes and common failure checks.