Applicable scenario: Deploy a new service under the local code/ to ali-ecs-2c8gfor the first time, including an HTTPS domain.
How to invoke:
/deploy-ecs-service
Before invoking, ensure the session context already includes: domain, local source directory, service port, service description. If unclear, ask the user first.
Hard constraints (must be kept in mind before each step)
| Constraint | Description |
|---|---|
| nginx upstream | Must be host.docker.internal:PORT,Never write 127.0.0.1 |
| Compilation on the server is prohibited | ali-ecs-2c8g is a 2-core machine,prohibited on the server docker compose up --build, only up -d |
| Use API for DNS operations | .env has ALIYUN_DNS_ACCESS_KEY_ID/_SECRET, add A records by calling the API directly,do not ask the user to manually operate in the console |
| services-index.md | After deployment is completemust update, this is a mandatory requirement of CLAUDE.md, not updating means not complete |
Step 1: Collect deployment parameters
Extract from the session context; if missing, ask the user:
DOMAIN=<域名,如 codingplans.daokou.fun>
PORT=<容器宿主机端口,如 8430>
PROJECT_LOCAL=<本地源码目录,如 code/coding-plans-site>
SERVICE_DESC=<服务简述,用于 services-index.md>
SERVER=ali-ecs-2c8g
SERVER_IP=<ECS 公网 IP,查 .env 或询问用户>
NGINX_DIR=/root/https-nginx
DEPLOY_DIR=/root/<服务目录名>
Step 2: Pre-flight check
2a. Port not occupied
ssh ali-ecs-2c8g "ss -tlnp | grep :PORT || echo 'port free'"
If the port is already occupied, stop and prompt the user to change the port.
2b. No duplicate domain in services-index.md
grep "DOMAIN" ~/workspace/tools/markdown/tools/https-nginx/services-index.md
If an entry for this domain already exists, confirm whether it is an update or a redeployment.
2c. Server already has a docker compose file
ssh ali-ecs-2c8g "ls DEPLOY_DIR/ 2>/dev/null || echo 'not exists'"
Step 3: Sync files and start the container
If the project has a Makefile:
cd PROJECT_LOCAL && make deploy
If there is no Makefile, execute manually:
ssh ali-ecs-2c8g "mkdir -p DEPLOY_DIR"
rsync -avz --exclude '.git' --exclude 'node_modules' \
PROJECT_LOCAL/ ali-ecs-2c8g:DEPLOY_DIR/
ssh ali-ecs-2c8g "cd DEPLOY_DIR && docker compose up -d"
Verify the container is running:
ssh ali-ecs-2c8g "docker compose -f DEPLOY_DIR/docker-compose.yml ps"
Step 4: DNS A record (added directly via API)
Do not prompt the user to manually operate in the console. Read .env credentials and then call the Aliyun DNS API directly.
Choose the invocation method by priority:
-
aliyun CLI(preferred when installed, simplest):
source ~/workspace/tools/markdown/.env aliyun dns AddDomainRecord --DomainName ZONE --RR RR --Type A --Value $SERVER_IP --TTL 600Check whether it is installed:
which aliyun -
Python SDK(when pip is available):
pip install aliyun-python-sdk-alidns -
Hand-written REST signature(last resort when there is neither CLI nor SDK, see the code below): Hand-written HMAC-SHA1 signatures are error-prone; use only in environments where no tools can be installed.
source ~/workspace/tools/markdown/.env
# 解析主域名(去掉最左侧子域名)
# 例:codingplans.daokou.fun → RR=codingplans, ZONE=daokou.fun
RR=$(echo "DOMAIN" | sed 's/\..*//')
ZONE=$(echo "DOMAIN" | sed 's/^[^.]*\.//')
SERVER_IP=$SERVER_IP # 从 Step 1 收集的变量
python3 -c "
import hmac, hashlib, base64, urllib.parse, urllib.request, json, datetime, uuid
ak = '$ALIYUN_DNS_ACCESS_KEY_ID'
sk = '$ALIYUN_DNS_ACCESS_KEY_SECRET'
def sign(params, sk):
sorted_params = sorted(params.items())
query = urllib.parse.urlencode(sorted_params)
string_to_sign = 'GET&%2F&' + urllib.parse.quote(query, safe='')
h = hmac.new((sk + '&').encode(), string_to_sign.encode(), hashlib.sha1)
return base64.b64encode(h.digest()).decode()
params = {
'Action': 'AddDomainRecord',
'DomainName': '$ZONE',
'RR': '$RR',
'Type': 'A',
'Value': '$SERVER_IP',
'TTL': '600',
'Format': 'JSON',
'Version': '2015-01-09',
'AccessKeyId': ak,
'SignatureMethod': 'HMAC-SHA1',
'Timestamp': datetime.datetime.now(datetime.timezone.utc).strftime('%Y-%m-%dT%H:%M:%SZ'),
'SignatureVersion': '1.0',
'SignatureNonce': str(uuid.uuid4()),
}
params['Signature'] = sign(params, sk)
url = 'https://alidns.aliyuncs.com/?' + urllib.parse.urlencode(params)
resp = urllib.request.urlopen(url, timeout=10)
print(json.loads(resp.read()))
"
If the API returns an error (e.g., the domain is not in that account), only then switch to prompting the user to add it manually, and wait for confirmation before continuing.
Step 5: Wait for DNS to take effect
Poll the authoritative DNS to confirm resolution to $SERVER_IP:
until nslookup DOMAIN dns31.hichina.com 2>&1 | grep -q "$SERVER_IP"; do
echo "waiting for DNS..."; sleep 5
done
echo "DNS OK"
If the domain is not on hichina (e.g., Cloudflare), replace dns31.hichina.com with the corresponding authoritative NS, or query public DNS directly:
until nslookup DOMAIN 8.8.8.8 2>&1 | grep -q "$SERVER_IP"; do
echo "waiting..."; sleep 5
done
Step 6: HTTPS certificate + nginx configuration
6a. Apply for a Let's Encrypt certificate (HTTP-01)
ssh ali-ecs-2c8g "cd NGINX_DIR && bash certs/letsencrypt/certbot-http01.sh DOMAIN"
Prefer HTTP-01(does not depend on DNS API permissions). If HTTP-01 fails (e.g., the server's port 80 is unreachable), then try DNS-01:
ssh ali-ecs-2c8g "cd NGINX_DIR && bash certs/letsencrypt/certbot-dns01.sh DOMAIN"
6b. Write the nginx conf
ssh ali-ecs-2c8g "cat > NGINX_DIR/nginx/conf.d/DOMAIN.conf << 'EOF'
server {
listen 443 ssl;
server_name DOMAIN;
ssl_certificate /etc/letsencrypt/live/DOMAIN/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/DOMAIN/privkey.pem;
ssl_trusted_certificate /etc/letsencrypt/live/DOMAIN/chain.pem;
include /etc/nginx/snippets/ssl-common.conf;
location / {
proxy_pass http://host.docker.internal:PORT;
include /etc/nginx/snippets/proxy-common.conf;
}
}
EOF"
6c. Test and reload
ssh ali-ecs-2c8g "docker exec https-nginx nginx -t && docker exec https-nginx nginx -s reload && echo 'nginx reloaded OK'"
Step 7: Verify
# HTTPS 主页
curl -sI https://DOMAIN | head -5
# 若有数据接口(如 /data/*.md)
curl -sI https://DOMAIN/data/xxx.md | head -5
Expected:HTTP/1.1 200 OK. If not 200, check:
- Whether the container is running:
ssh ali-ecs-2c8g "docker compose -f DEPLOY_DIR/docker-compose.yml ps" - nginx logs:
ssh ali-ecs-2c8g "docker exec https-nginx cat /var/log/nginx/error.log | tail -20"
Step 8: Update services-index.md (mandatory, cannot be skipped)
File path:~/workspace/tools/markdown/tools/https-nginx/services-index.md
8a. Global quick reference table (top of file) append one row
| [DOMAIN](https://DOMAIN) | SERVICE_DESC | ali-ecs-2c8g | 无 | ✅ 运行中 |
8b. Alibaba Cloud ECS 2c8g section (## 阿里云 ECS 2c8g under) append service details
### DOMAIN
| 项目 | 值 |
|------|-----|
| **公网地址** | https://DOMAIN |
| **上游** | `host.docker.internal:PORT`(即 `127.0.0.1:PORT`)|
| **说明** | SERVICE_DESC |
| **应用服务** | Docker Compose(`DEPLOY_DIR/docker-compose.yml`)|
| **本地源码** | `PROJECT_LOCAL/` |
| **代码目录** | `DEPLOY_DIR` |
| **证书** | LE ECDSA 单证书,到期 CERT_EXPIRY |
| **nginx conf** | `NGINX_DIR/nginx/conf.d/DOMAIN.conf` |
| **认证** | 无 |
| **部署时间** | TODAY |
| **部署方式** | `cd PROJECT_LOCAL && make deploy` |
CERT_EXPIRY Read from certbot output, format YYYY-MM-DD。
If Step 8 is not completed, the deployment is considered incomplete.
Quick reference
| Variable | Description | Example |
|---|---|---|
DOMAIN |
Full domain name | codingplans.daokou.fun |
PORT |
Host port | 8430 |
PROJECT_LOCAL |
Local source directory | code/coding-plans-site |
DEPLOY_DIR |
Server directory | /root/coding-plans-site |
NGINX_DIR |
https-nginx directory | /root/https-nginx |
SERVER_IP |
ali-ecs-2c8g public IP | $SERVER_IP |