SuperAPI AI Directory

deploy-ecs-service

Deployment & operations

View repository

Updated 10-15 23:42

About this skill

deploy-ecs-service: Full ECS service deployment workflow

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:

  1. aliyun CLI(preferred when installed, simplest):

    source ~/workspace/tools/markdown/.env
    aliyun dns AddDomainRecord --DomainName ZONE --RR RR --Type A --Value $SERVER_IP --TTL 600
    

    Check whether it is installed:which aliyun

  2. Python SDK(when pip is available):

    pip install aliyun-python-sdk-alidns
    
  3. 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:

  1. Whether the container is running:ssh ali-ecs-2c8g "docker compose -f DEPLOY_DIR/docker-compose.yml ps"
  2. 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