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 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: |
set -euo pipefail
test -n "${{ secrets.AWS_ACCESS_KEY_ID }}"
test -n "${{ secrets.AWS_SECRET_ACCESS_KEY }}"
test -n "${{ secrets.AWS_REGION }}"
test -n "${{ secrets.AWS_ACCOUNT_ID }}"
test -n "${{ secrets.ECR_REPOSITORY }}"
test -n "${VOLCENGINE_REGISTRY_USERNAME}"
test -n "${VOLCENGINE_REGISTRY_PASSWORD}"
- name: Build backend production image
run: |
@@ -63,35 +63,16 @@ jobs:
${IMAGE_NAME}:prod-${GITHUB_SHA} \
-c "import app; print(app.app.title)"
- name: Push backend image to ECR
- name: Push backend image to Volcengine registry
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 }}
AWS_ACCOUNT_ID: ${{ secrets.AWS_ACCOUNT_ID }}
ECR_REPOSITORY: ${{ secrets.ECR_REPOSITORY }}
VOLCENGINE_REGISTRY_USERNAME: ${{ secrets.VOLCENGINE_REGISTRY_USERNAME }}
VOLCENGINE_REGISTRY_PASSWORD: ${{ secrets.VOLCENGINE_REGISTRY_PASSWORD }}
run: |
set -euo pipefail
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)"
if [ "${caller_account_id}" != "${AWS_ACCOUNT_ID}" ]; then
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
volcengine_registry="linksy-docker-cn-beijing.cr.volces.com"
volcengine_image="${volcengine_registry}/eryao/${IMAGE_NAME}"
retry() {
for attempt in 1 2 3; do
@@ -105,31 +86,20 @@ jobs:
done
}
ecr_login() {
aws ecr get-login-password --region "${AWS_REGION}" \
| docker login --username AWS --password-stdin "${ecr_registry}"
volcengine_login() {
printf '%s' "${VOLCENGINE_REGISTRY_PASSWORD}" \
| docker login --username "${VOLCENGINE_REGISTRY_USERNAME}" --password-stdin "${volcengine_registry}"
}
retry ecr_login
docker tag "${IMAGE_NAME}:prod-${GITHUB_SHA}" "${ecr_image}:latest"
retry docker push "${ecr_image}:latest"
untagged_image_ids="$(aws ecr list-images \
--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
retry volcengine_login
docker tag "${IMAGE_NAME}:prod-${GITHUB_SHA}" "${volcengine_image}:${GITHUB_SHA}"
docker tag "${IMAGE_NAME}:prod-${GITHUB_SHA}" "${volcengine_image}:latest"
retry docker push "${volcengine_image}:${GITHUB_SHA}"
retry docker push "${volcengine_image}:latest"
deploy-production:
needs: build-backend-image
if: github.event_name != 'workflow_dispatch' || github.ref == 'refs/heads/main'
runs-on: wsl2-docker-host
steps:
- name: Validate deploy configuration
@@ -137,27 +107,23 @@ jobs:
DEPLOY_SSH_KEY: ${{ secrets.DEPLOY_SSH_KEY }}
DEPLOY_HOST: ${{ secrets.DEPLOY_HOST }}
DEPLOY_USER: ${{ secrets.DEPLOY_USER }}
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
AWS_REGION: ${{ secrets.AWS_REGION }}
VOLCENGINE_REGISTRY_USERNAME: ${{ secrets.VOLCENGINE_REGISTRY_USERNAME }}
VOLCENGINE_REGISTRY_PASSWORD: ${{ secrets.VOLCENGINE_REGISTRY_PASSWORD }}
run: |
set -euo pipefail
test -n "${DEPLOY_SSH_KEY}"
test -n "${DEPLOY_HOST}"
test -n "${DEPLOY_USER}"
test -n "${AWS_ACCESS_KEY_ID}"
test -n "${AWS_SECRET_ACCESS_KEY}"
test -n "${AWS_REGION}"
test -n "${VOLCENGINE_REGISTRY_USERNAME}"
test -n "${VOLCENGINE_REGISTRY_PASSWORD}"
- name: Deploy production server
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_USER: ${{ secrets.DEPLOY_USER }}
DEPLOY_SSH_KEY: ${{ secrets.DEPLOY_SSH_KEY }}
VOLCENGINE_REGISTRY_USERNAME: ${{ secrets.VOLCENGINE_REGISTRY_USERNAME }}
VOLCENGINE_REGISTRY_PASSWORD: ${{ secrets.VOLCENGINE_REGISTRY_PASSWORD }}
run: |
set -euo pipefail
@@ -165,21 +131,34 @@ jobs:
printf '%s\n' "${DEPLOY_SSH_KEY}" > ~/.ssh/eryao_deploy_key
chmod 600 ~/.ssh/eryao_deploy_key
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 \
-o IdentitiesOnly=yes \
"${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
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
. ./.env
set +a
ecr_registry="${AWS_ACCOUNT_ID}.dkr.ecr.${AWS_REGION}.amazonaws.com"
aws ecr get-login-password --region "${AWS_REGION}" \
| sudo docker login --username AWS --password-stdin "${ecr_registry}"
volcengine_registry="linksy-docker-cn-beijing.cr.volces.com"
printf '%s' "${VOLCENGINE_REGISTRY_PASSWORD}" \
| 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 up -d --remove-orphans
+36 -37
View File
@@ -13,7 +13,7 @@
- Docker
- Docker Compose v2
- AWS CLI v2
- Docker CLI registry login support
- Node.js 22+ (前端 SSR 服务)
- PM2 (前端进程管理)
@@ -22,7 +22,6 @@
```bash
docker --version
docker compose version
aws --version
node --version # 需要 >= 22.12.0
pm2 --version
```
@@ -58,23 +57,13 @@ Internet → Nginx (443)
deploy/.env
```
必须包含 AWS ECR 镜像定位变量
生产镜像固定从火山云镜像仓库拉取
```text
AWS_ACCOUNT_ID=<你的 AWS 账号 ID>
AWS_REGION=<ECR 所在区域>
ECR_REPOSITORY=<ECR 仓库名>
linksy-docker-cn-beijing.cr.volces.com/eryao/eryao-backend:latest
```
如果本目录下的 `.env` 是从项目根目录 `.env` 复制过来的,通常还需要手动追加以上三个变量。
默认镜像地址会拼接为:
```text
${AWS_ACCOUNT_ID}.dkr.ecr.${AWS_REGION}.amazonaws.com/${ECR_REPOSITORY}:latest
```
如果要手动指定完整镜像地址,可以在 `.env` 中设置:
如果要手动指定完整镜像地址,可在 `.env` 中设置:
```text
ERYAO_BACKEND_IMAGE=<完整镜像地址>
@@ -116,7 +105,7 @@ ERYAO_WEB__WORKERS=2
ERYAO_WORKER__GROUPS__AGENT__CONCURRENCY=2
```
## 登录 ECR
## 登录火山云镜像仓库
进入部署目录,并把 `.env` 加载到当前 shell
@@ -127,12 +116,14 @@ set -a
set +a
```
在生产机器上配置好 AWS 凭据后执行:
在生产机器上配置好火山云仓库用户名和密码后执行:
```bash
aws ecr get-login-password --region "$AWS_REGION" \
| docker login --username AWS --password-stdin \
"${AWS_ACCOUNT_ID}.dkr.ecr.${AWS_REGION}.amazonaws.com"
printf '%s' "$VOLCENGINE_REGISTRY_PASSWORD" \
| docker login \
--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` 分支构建后自动部署到生产机器:
- 推送前清空 ECR 仓库旧镜像,只保留新推送的 `latest`
- 在 Gitea runner 构建后端镜像,并推送到火山云 `eryao` 命名空间,同时保留 commit SHA 和 `latest` 标签
- 通过 `DEPLOY_SSH_KEY` 登录生产机器。
- 在生产机器执行 ECR 登录、`docker compose pull``docker compose up -d`
- 在生产机器登录火山云仓库,执行 `docker compose pull``docker compose up -d`
- 健康检查通过后清理 7 天前未使用的本地 Docker 镜像。
Gitea Secrets 需要包含:
```text
AWS_ACCESS_KEY_ID
AWS_SECRET_ACCESS_KEY
AWS_REGION
AWS_ACCOUNT_ID
ECR_REPOSITORY
VOLCENGINE_REGISTRY_USERNAME
VOLCENGINE_REGISTRY_PASSWORD
DEPLOY_SSH_KEY
DEPLOY_HOST
DEPLOY_USER
```
`DEPLOY_SSH_KEY` 是已加入生产机器 `ubuntu` 用户 `~/.ssh/authorized_keys` 的部署专用私钥。
当前生产机器对应:`DEPLOY_HOST=18.218.38.213``DEPLOY_USER=ubuntu`
`DEPLOY_SSH_KEY` 是已加入生产机器 `root` 用户 `~/.ssh/authorized_keys` 的部署专用私钥。
当前生产机器对应:`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
# 上传构建产物
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 管理
@@ -287,13 +286,13 @@ server {
# 静态资源直接从 client 目录提供
location /_astro {
root /home/ubuntu/deploy/web/client;
root /root/deploy/web/client;
expires 30d;
add_header Cache-Control "public, immutable";
}
location /images {
root /home/ubuntu/deploy/web/client;
root /root/deploy/web/client;
expires 30d;
}
@@ -316,8 +315,8 @@ server {
cd web && PUBLIC_API_URL=https://api.meeyao.com npm run build
# 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. 重启服务
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
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:
- path: ./.env
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;
}
}