3 Commits

Author SHA1 Message Date
zl-q 8a7a5217c4 infra: add meeyao reverse proxy config 2026-07-27 12:07:56 +08:00
zl-q 7e06100f7a ci: skip production deploy on dev dispatch 2026-07-27 11:50:15 +08:00
zl-q 6b0baa0333 ci: publish production image to Volcengine registry 2026-07-27 11:48:53 +08:00
4 changed files with 111 additions and 102 deletions
+43 -64
View File
@@ -23,14 +23,14 @@ jobs:
git fetch --no-tags --depth=1 origin "${GITHUB_SHA}" git fetch --no-tags --depth=1 origin "${GITHUB_SHA}"
git checkout --detach FETCH_HEAD git checkout --detach FETCH_HEAD
- name: Validate ECR configuration - name: Validate Volcengine registry configuration
env:
VOLCENGINE_REGISTRY_USERNAME: ${{ secrets.VOLCENGINE_REGISTRY_USERNAME }}
VOLCENGINE_REGISTRY_PASSWORD: ${{ secrets.VOLCENGINE_REGISTRY_PASSWORD }}
run: | run: |
set -euo pipefail set -euo pipefail
test -n "${{ secrets.AWS_ACCESS_KEY_ID }}" test -n "${VOLCENGINE_REGISTRY_USERNAME}"
test -n "${{ secrets.AWS_SECRET_ACCESS_KEY }}" test -n "${VOLCENGINE_REGISTRY_PASSWORD}"
test -n "${{ secrets.AWS_REGION }}"
test -n "${{ secrets.AWS_ACCOUNT_ID }}"
test -n "${{ secrets.ECR_REPOSITORY }}"
- name: Build backend production image - name: Build backend production image
run: | run: |
@@ -63,35 +63,16 @@ jobs:
${IMAGE_NAME}:prod-${GITHUB_SHA} \ ${IMAGE_NAME}:prod-${GITHUB_SHA} \
-c "import app; print(app.app.title)" -c "import app; print(app.app.title)"
- name: Push backend image to ECR - name: Push backend image to Volcengine registry
env: env:
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} VOLCENGINE_REGISTRY_USERNAME: ${{ secrets.VOLCENGINE_REGISTRY_USERNAME }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} VOLCENGINE_REGISTRY_PASSWORD: ${{ secrets.VOLCENGINE_REGISTRY_PASSWORD }}
AWS_DEFAULT_REGION: ${{ secrets.AWS_REGION }}
AWS_REGION: ${{ secrets.AWS_REGION }}
AWS_ACCOUNT_ID: ${{ secrets.AWS_ACCOUNT_ID }}
ECR_REPOSITORY: ${{ secrets.ECR_REPOSITORY }}
run: | run: |
set -euo pipefail set -euo pipefail
export HTTP_PROXY= HTTPS_PROXY= ALL_PROXY= http_proxy= https_proxy= all_proxy= export HTTP_PROXY= HTTPS_PROXY= ALL_PROXY= http_proxy= https_proxy= all_proxy=
caller_account_id="$(aws sts get-caller-identity --query Account --output text)" volcengine_registry="linksy-docker-cn-beijing.cr.volces.com"
if [ "${caller_account_id}" != "${AWS_ACCOUNT_ID}" ]; then volcengine_image="${volcengine_registry}/eryao/${IMAGE_NAME}"
echo "AWS_ACCOUNT_ID does not match caller identity" >&2
exit 1
fi
ecr_registry="${AWS_ACCOUNT_ID}.dkr.ecr.${AWS_REGION}.amazonaws.com"
ecr_image="${ecr_registry}/${ECR_REPOSITORY}"
aws ecr describe-repositories \
--region "${AWS_REGION}" \
--repository-names "${ECR_REPOSITORY}" >/dev/null 2>&1 \
|| aws ecr create-repository \
--region "${AWS_REGION}" \
--repository-name "${ECR_REPOSITORY}" \
--image-scanning-configuration scanOnPush=true \
--encryption-configuration encryptionType=AES256 >/dev/null
retry() { retry() {
for attempt in 1 2 3; do for attempt in 1 2 3; do
@@ -105,31 +86,20 @@ jobs:
done done
} }
ecr_login() { volcengine_login() {
aws ecr get-login-password --region "${AWS_REGION}" \ printf '%s' "${VOLCENGINE_REGISTRY_PASSWORD}" \
| docker login --username AWS --password-stdin "${ecr_registry}" | docker login --username "${VOLCENGINE_REGISTRY_USERNAME}" --password-stdin "${volcengine_registry}"
} }
retry ecr_login retry volcengine_login
docker tag "${IMAGE_NAME}:prod-${GITHUB_SHA}" "${ecr_image}:latest" docker tag "${IMAGE_NAME}:prod-${GITHUB_SHA}" "${volcengine_image}:${GITHUB_SHA}"
retry docker push "${ecr_image}:latest" docker tag "${IMAGE_NAME}:prod-${GITHUB_SHA}" "${volcengine_image}:latest"
retry docker push "${volcengine_image}:${GITHUB_SHA}"
untagged_image_ids="$(aws ecr list-images \ retry docker push "${volcengine_image}:latest"
--region "${AWS_REGION}" \
--repository-name "${ECR_REPOSITORY}" \
--filter tagStatus=UNTAGGED \
--query 'imageIds[*]' \
--output json)"
if [ "${untagged_image_ids}" != "[]" ]; then
aws ecr batch-delete-image \
--region "${AWS_REGION}" \
--repository-name "${ECR_REPOSITORY}" \
--image-ids "${untagged_image_ids}" >/dev/null \
|| echo "Warning: ECR image cleanup failed; ensure the CI AWS user has ecr:BatchDeleteImage" >&2
fi
deploy-production: deploy-production:
needs: build-backend-image needs: build-backend-image
if: github.event_name != 'workflow_dispatch' || github.ref == 'refs/heads/main'
runs-on: wsl2-docker-host runs-on: wsl2-docker-host
steps: steps:
- name: Validate deploy configuration - name: Validate deploy configuration
@@ -137,27 +107,23 @@ jobs:
DEPLOY_SSH_KEY: ${{ secrets.DEPLOY_SSH_KEY }} DEPLOY_SSH_KEY: ${{ secrets.DEPLOY_SSH_KEY }}
DEPLOY_HOST: ${{ secrets.DEPLOY_HOST }} DEPLOY_HOST: ${{ secrets.DEPLOY_HOST }}
DEPLOY_USER: ${{ secrets.DEPLOY_USER }} DEPLOY_USER: ${{ secrets.DEPLOY_USER }}
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} VOLCENGINE_REGISTRY_USERNAME: ${{ secrets.VOLCENGINE_REGISTRY_USERNAME }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} VOLCENGINE_REGISTRY_PASSWORD: ${{ secrets.VOLCENGINE_REGISTRY_PASSWORD }}
AWS_REGION: ${{ secrets.AWS_REGION }}
run: | run: |
set -euo pipefail set -euo pipefail
test -n "${DEPLOY_SSH_KEY}" test -n "${DEPLOY_SSH_KEY}"
test -n "${DEPLOY_HOST}" test -n "${DEPLOY_HOST}"
test -n "${DEPLOY_USER}" test -n "${DEPLOY_USER}"
test -n "${AWS_ACCESS_KEY_ID}" test -n "${VOLCENGINE_REGISTRY_USERNAME}"
test -n "${AWS_SECRET_ACCESS_KEY}" test -n "${VOLCENGINE_REGISTRY_PASSWORD}"
test -n "${AWS_REGION}"
- name: Deploy production server - name: Deploy production server
env: env:
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
AWS_DEFAULT_REGION: ${{ secrets.AWS_REGION }}
AWS_REGION: ${{ secrets.AWS_REGION }}
DEPLOY_HOST: ${{ secrets.DEPLOY_HOST }} DEPLOY_HOST: ${{ secrets.DEPLOY_HOST }}
DEPLOY_USER: ${{ secrets.DEPLOY_USER }} DEPLOY_USER: ${{ secrets.DEPLOY_USER }}
DEPLOY_SSH_KEY: ${{ secrets.DEPLOY_SSH_KEY }} DEPLOY_SSH_KEY: ${{ secrets.DEPLOY_SSH_KEY }}
VOLCENGINE_REGISTRY_USERNAME: ${{ secrets.VOLCENGINE_REGISTRY_USERNAME }}
VOLCENGINE_REGISTRY_PASSWORD: ${{ secrets.VOLCENGINE_REGISTRY_PASSWORD }}
run: | run: |
set -euo pipefail set -euo pipefail
@@ -165,21 +131,34 @@ jobs:
printf '%s\n' "${DEPLOY_SSH_KEY}" > ~/.ssh/eryao_deploy_key printf '%s\n' "${DEPLOY_SSH_KEY}" > ~/.ssh/eryao_deploy_key
chmod 600 ~/.ssh/eryao_deploy_key chmod 600 ~/.ssh/eryao_deploy_key
ssh-keyscan -H "${DEPLOY_HOST}" >> ~/.ssh/known_hosts ssh-keyscan -H "${DEPLOY_HOST}" >> ~/.ssh/known_hosts
remote_registry_username="$(printf '%q' "${VOLCENGINE_REGISTRY_USERNAME}")"
remote_registry_password="$(printf '%q' "${VOLCENGINE_REGISTRY_PASSWORD}")"
ssh -i ~/.ssh/eryao_deploy_key \ ssh -i ~/.ssh/eryao_deploy_key \
-o IdentitiesOnly=yes \ -o IdentitiesOnly=yes \
"${DEPLOY_USER}@${DEPLOY_HOST}" \ "${DEPLOY_USER}@${DEPLOY_HOST}" \
"AWS_ACCESS_KEY_ID='${AWS_ACCESS_KEY_ID}' AWS_SECRET_ACCESS_KEY='${AWS_SECRET_ACCESS_KEY}' AWS_DEFAULT_REGION='${AWS_REGION}' AWS_REGION='${AWS_REGION}' bash -se" <<'REMOTE' "VOLCENGINE_REGISTRY_USERNAME=${remote_registry_username} VOLCENGINE_REGISTRY_PASSWORD=${remote_registry_password} bash -se" <<'REMOTE'
set -euo pipefail set -euo pipefail
cd ~/deploy cd ~/deploy
test -f ./.env
for required_key in ERYAO_DATABASE__HOST ERYAO_DATABASE__PORT ERYAO_DATABASE__NAME ERYAO_DATABASE__USER ERYAO_DATABASE__PASSWORD; do
grep -q "^${required_key}=" ./.env
done
sed -i \
-e 's#^ERYAO_DATABASE__HOST=.*#ERYAO_DATABASE__HOST=pgm-j6ckocm243gd2997.pg.cnhk.rds.aliyuncs.com#' \
-e 's#^ERYAO_DATABASE__PORT=.*#ERYAO_DATABASE__PORT=5432#' \
-e 's#^ERYAO_DATABASE__NAME=.*#ERYAO_DATABASE__NAME=supabase_db#' \
-e 's#^ERYAO_DATABASE__USER=.*#ERYAO_DATABASE__USER=postgres#' \
./.env
set -a set -a
. ./.env . ./.env
set +a set +a
ecr_registry="${AWS_ACCOUNT_ID}.dkr.ecr.${AWS_REGION}.amazonaws.com" volcengine_registry="linksy-docker-cn-beijing.cr.volces.com"
aws ecr get-login-password --region "${AWS_REGION}" \ printf '%s' "${VOLCENGINE_REGISTRY_PASSWORD}" \
| sudo docker login --username AWS --password-stdin "${ecr_registry}" | sudo docker login --username "${VOLCENGINE_REGISTRY_USERNAME}" --password-stdin "${volcengine_registry}"
sudo docker compose --env-file ./.env -f docker-compose.prod.yml --profile workers pull sudo docker compose --env-file ./.env -f docker-compose.prod.yml --profile workers pull
sudo docker compose --env-file ./.env -f docker-compose.prod.yml --profile workers up -d --remove-orphans sudo docker compose --env-file ./.env -f docker-compose.prod.yml --profile workers up -d --remove-orphans
+36 -37
View File
@@ -13,7 +13,7 @@
- Docker - Docker
- Docker Compose v2 - Docker Compose v2
- AWS CLI v2 - Docker CLI registry login support
- Node.js 22+ (前端 SSR 服务) - Node.js 22+ (前端 SSR 服务)
- PM2 (前端进程管理) - PM2 (前端进程管理)
@@ -22,7 +22,6 @@
```bash ```bash
docker --version docker --version
docker compose version docker compose version
aws --version
node --version # 需要 >= 22.12.0 node --version # 需要 >= 22.12.0
pm2 --version pm2 --version
``` ```
@@ -58,23 +57,13 @@ Internet → Nginx (443)
deploy/.env deploy/.env
``` ```
必须包含 AWS ECR 镜像定位变量 生产镜像固定从火山云镜像仓库拉取
```text ```text
AWS_ACCOUNT_ID=<你的 AWS 账号 ID> linksy-docker-cn-beijing.cr.volces.com/eryao/eryao-backend:latest
AWS_REGION=<ECR 所在区域>
ECR_REPOSITORY=<ECR 仓库名>
``` ```
如果本目录下的 `.env` 是从项目根目录 `.env` 复制过来的,通常还需要手动追加以上三个变量。 如果要手动指定完整镜像地址,可在 `.env` 中设置:
默认镜像地址会拼接为:
```text
${AWS_ACCOUNT_ID}.dkr.ecr.${AWS_REGION}.amazonaws.com/${ECR_REPOSITORY}:latest
```
如果要手动指定完整镜像地址,可以在 `.env` 中设置:
```text ```text
ERYAO_BACKEND_IMAGE=<完整镜像地址> ERYAO_BACKEND_IMAGE=<完整镜像地址>
@@ -116,7 +105,7 @@ ERYAO_WEB__WORKERS=2
ERYAO_WORKER__GROUPS__AGENT__CONCURRENCY=2 ERYAO_WORKER__GROUPS__AGENT__CONCURRENCY=2
``` ```
## 登录 ECR ## 登录火山云镜像仓库
进入部署目录,并把 `.env` 加载到当前 shell 进入部署目录,并把 `.env` 加载到当前 shell
@@ -127,12 +116,14 @@ set -a
set +a set +a
``` ```
在生产机器上配置好 AWS 凭据后执行: 在生产机器上配置好火山云仓库用户名和密码后执行:
```bash ```bash
aws ecr get-login-password --region "$AWS_REGION" \ printf '%s' "$VOLCENGINE_REGISTRY_PASSWORD" \
| docker login --username AWS --password-stdin \ | docker login \
"${AWS_ACCOUNT_ID}.dkr.ecr.${AWS_REGION}.amazonaws.com" --username "$VOLCENGINE_REGISTRY_USERNAME" \
--password-stdin \
linksy-docker-cn-beijing.cr.volces.com
``` ```
## 启动服务 ## 启动服务
@@ -180,28 +171,36 @@ docker logs -f eryao-prod-redis
当前 CI/CD 会在 `main` 分支构建后自动部署到生产机器: 当前 CI/CD 会在 `main` 分支构建后自动部署到生产机器:
- 推送前清空 ECR 仓库旧镜像,只保留新推送的 `latest` - 在 Gitea runner 构建后端镜像,并推送到火山云 `eryao` 命名空间,同时保留 commit SHA 和 `latest` 标签
- 通过 `DEPLOY_SSH_KEY` 登录生产机器。 - 通过 `DEPLOY_SSH_KEY` 登录生产机器。
- 在生产机器执行 ECR 登录、`docker compose pull``docker compose up -d` - 在生产机器登录火山云仓库,执行 `docker compose pull``docker compose up -d`
- 健康检查通过后清理 7 天前未使用的本地 Docker 镜像。 - 健康检查通过后清理 7 天前未使用的本地 Docker 镜像。
Gitea Secrets 需要包含: Gitea Secrets 需要包含:
```text ```text
AWS_ACCESS_KEY_ID VOLCENGINE_REGISTRY_USERNAME
AWS_SECRET_ACCESS_KEY VOLCENGINE_REGISTRY_PASSWORD
AWS_REGION
AWS_ACCOUNT_ID
ECR_REPOSITORY
DEPLOY_SSH_KEY DEPLOY_SSH_KEY
DEPLOY_HOST DEPLOY_HOST
DEPLOY_USER DEPLOY_USER
``` ```
`DEPLOY_SSH_KEY` 是已加入生产机器 `ubuntu` 用户 `~/.ssh/authorized_keys` 的部署专用私钥。 `DEPLOY_SSH_KEY` 是已加入生产机器 `root` 用户 `~/.ssh/authorized_keys` 的部署专用私钥。
当前生产机器对应:`DEPLOY_HOST=18.218.38.213``DEPLOY_USER=ubuntu` 当前生产机器对应:`DEPLOY_HOST=47.239.107.213``DEPLOY_USER=root`
CI 的 AWS 用户需要至少允许 ECR 登录、建仓库、推送镜像、列出镜像,以及为了覆盖式清理旧镜像所需的 `ecr:BatchDeleteImage`。如果缺少 `ecr:BatchDeleteImage`,部署仍可继续推送 `latest`,但 ECR 旧镜像不会被清空。 部署脚本会在每次发布前把后端数据库连接更新为觅爻港区 RDS:
```text
ERYAO_DATABASE__HOST=pgm-j6ckocm243gd2997.pg.cnhk.rds.aliyuncs.com
ERYAO_DATABASE__PORT=5432
ERYAO_DATABASE__NAME=supabase_db
ERYAO_DATABASE__USER=postgres
```
`ERYAO_DATABASE__PASSWORD` 保留生产服务器现有值,不写入仓库或 CI 日志。
`VOLCENGINE_REGISTRY_USERNAME` 当前为 `洵觅科技2025@2101878614``VOLCENGINE_REGISTRY_PASSWORD` 必须配置为火山云仓库访问密码;CI 会先登录 registry,再向 `eryao/eryao-backend` 推送镜像,推送失败时部署停止。
如需手动更新,在生产机器执行: 如需手动更新,在生产机器执行:
@@ -246,13 +245,13 @@ PUBLIC_API_URL=https://api.meeyao.com npm run build
```bash ```bash
# 上传构建产物 # 上传构建产物
rsync -avz -e "ssh -i xunmee.pem" web/dist/ ubuntu@18.218.38.213:/home/ubuntu/deploy/web/ rsync -avz -e "ssh -i 觅爻港区密钥对.pem" web/dist/ root@47.239.107.213:/root/deploy/web/
# 上传依赖配置 # 上传依赖配置
rsync -avz -e "ssh -i xunmee.pem" web/package.json web/package-lock.json ubuntu@18.218.38.213:/home/ubuntu/deploy/web/ rsync -avz -e "ssh -i 觅爻港区密钥对.pem" web/package.json web/package-lock.json root@47.239.107.213:/root/deploy/web/
# 安装生产依赖 # 安装生产依赖
ssh -i xunmee.pem ubuntu@18.218.38.213 "cd /home/ubuntu/deploy/web && npm install --omit=dev" ssh -i 觅爻港区密钥对.pem root@47.239.107.213 "cd /root/deploy/web && npm install --omit=dev"
``` ```
### PM2 管理 ### PM2 管理
@@ -287,13 +286,13 @@ server {
# 静态资源直接从 client 目录提供 # 静态资源直接从 client 目录提供
location /_astro { location /_astro {
root /home/ubuntu/deploy/web/client; root /root/deploy/web/client;
expires 30d; expires 30d;
add_header Cache-Control "public, immutable"; add_header Cache-Control "public, immutable";
} }
location /images { location /images {
root /home/ubuntu/deploy/web/client; root /root/deploy/web/client;
expires 30d; expires 30d;
} }
@@ -316,8 +315,8 @@ server {
cd web && PUBLIC_API_URL=https://api.meeyao.com npm run build cd web && PUBLIC_API_URL=https://api.meeyao.com npm run build
# 2. 上传 # 2. 上传
rsync -avz -e "ssh -i xunmee.pem" dist/ ubuntu@18.218.38.213:/home/ubuntu/deploy/web/ rsync -avz -e "ssh -i 觅爻港区密钥对.pem" dist/ root@47.239.107.213:/root/deploy/web/
# 3. 重启服务 # 3. 重启服务
ssh -i xunmee.pem ubuntu@18.218.38.213 "pm2 restart meeyao-web" ssh -i 觅爻港区密钥对.pem root@47.239.107.213 "pm2 restart meeyao-web"
``` ```
+1 -1
View File
@@ -1,7 +1,7 @@
name: eryao-prod name: eryao-prod
x-backend-common: &backend-common x-backend-common: &backend-common
image: ${ERYAO_BACKEND_IMAGE:-${AWS_ACCOUNT_ID:?AWS_ACCOUNT_ID is required}.dkr.ecr.${AWS_REGION:?AWS_REGION is required}.amazonaws.com/${ECR_REPOSITORY:?ECR_REPOSITORY is required}:latest} image: ${ERYAO_BACKEND_IMAGE:-linksy-docker-cn-beijing.cr.volces.com/eryao/eryao-backend:latest}
env_file: env_file:
- path: ./.env - path: ./.env
required: true required: true
+31
View File
@@ -0,0 +1,31 @@
server {
listen 80;
listen [::]:80;
server_name meeyao.com www.meeyao.com;
client_max_body_size 20m;
location /.well-known/acme-challenge/ {
root /var/www/certbot;
}
location /api/ {
proxy_pass http://127.0.0.1:5775;
proxy_http_version 1.1;
proxy_buffering off;
proxy_read_timeout 120s;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
location / {
proxy_pass http://127.0.0.1:4322;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}