技术 · 2026/06/06
CI-CD实践
CI-CD实践操作
一、阿里云服务的初始部署设置
1.1 配置密钥对,禁止密码登录
//登录
ssh root@ip
//修改SSH 核心配置文件
sudo nano /etc/ssh/sshd_config
PasswordAuthentication no
PubkeyAuthentication yes
登录阿里云控制台上传密钥绑定实例
1.2 初始化云服务器,下载必要软件
//下载软件
apt update
apt install -y git curl ufw
curl -fsSL https://get.docker.com | sh
apt install -y docker-compose-plugin
systemctl enable docker
systemctl start docker
//开放端口80,443,22,阿里云控制台安全组也要放行
ufw allow 22
ufw allow 80
ufw allow 443
ufw enable
🔍
80是用户输入域名重定向到443加密接口,必须打开,22是管理连接后端接口也得打开,443接口是https接口就是应用的流量接口
二、dockerfile和docker.compose.yml编写
# 服务器专用:只拉取镜像运行,不在服务器上构建
# 用法:
# docker compose -f docker-compose.prod.yml pull
# docker compose -f docker-compose.prod.yml up -d
#
# 需要在同目录的 .env 中设置 IMAGE_REGISTRY、IMAGE_TAG 以及各项密钥
# 注意:NEXT_PUBLIC_API_BASE_URL 需要在构建 myblog-web 镜像时通过 build arg 写入
services:
logs-init:
image: busybox:1.36
init: true
command:
[
"sh",
"-c",
"mkdir -p /logs/api /logs/web /logs/nginx /logs/postgresql /logs/redis && chmod -R 0777 /logs"
]
volumes:
- ./logs:/logs
restart: "no"
nginx:
image: nginx:1.27-alpine
init: true
ports:
- "${NGINX_PORT:-80}:80"
- "443:443"
volumes:
- ./nginx/default.conf:/etc/nginx/conf.d/default.conf:ro
- ./logs/nginx:/var/log/nginx
- ./certs:/etc/nginx/certs:ro
- ./acme-challenge:/var/www/acme-challenge:ro
depends_on:
logs-init:
condition: service_completed_successfully
web:
condition: service_started
api:
condition: service_started
logging:
driver: json-file
options:
max-size: "20m"
max-file: "5"
restart: unless-stopped
web:
image: ${IMAGE_REGISTRY:?请在 .env 中设置 IMAGE_REGISTRY}/myblog-web:${IMAGE_TAG:-latest}
init: true
environment:
NEXT_PUBLIC_API_BASE_URL: ${NEXT_PUBLIC_API_BASE_URL:-http://localhost:4000}
expose:
- "3000"
command:
[
"sh",
"-c",
"node apps/web/server.js 2>&1 | tee -a /app/logs/app-$(date +%F).log"
]
volumes:
- ./logs/web:/app/logs
depends_on:
logs-init:
condition: service_completed_successfully
api:
condition: service_started
logging:
driver: json-file
options:
max-size: "20m"
max-file: "5"
restart: unless-stopped
api:
image: ${IMAGE_REGISTRY:?请在 .env 中设置 IMAGE_REGISTRY}/myblog-api:${IMAGE_TAG:-latest}
init: true
environment:
API_PORT: 4000
LOG_DIR: /app/logs
LOG_FILE_LEVEL: ${LOG_FILE_LEVEL:-log}
LOG_CONSOLE_LEVEL: ${LOG_CONSOLE_LEVEL:-warn}
WEB_ORIGIN: ${WEB_ORIGIN:-http://localhost:3000}
POSTGRES_URL: postgresql://postgres:${POSTGRES_PASSWORD:?请在 .env 中设置 POSTGRES_PASSWORD}@postgres:5432/blog_db
REDIS_URL: redis://redis:6379/0
JWT_SECRET: ${JWT_SECRET:?请在 .env 中设置 JWT_SECRET}
JWT_EXPIRES_IN: ${JWT_EXPIRES_IN:-7d}
AUTH_COOKIE_SECURE: ${AUTH_COOKIE_SECURE:-true}
RESEND_API_KEY: ${RESEND_API_KEY:-}
RESEND_FROM_EMAIL: ${RESEND_FROM_EMAIL:-}
EMAIL_CODE_REAL_SEND: ${EMAIL_CODE_REAL_SEND:-false}
OWNER_EMAIL: ${OWNER_EMAIL:-}
FEISHU_APP_ID: ${FEISHU_APP_ID:-}
FEISHU_APP_SECRET: ${FEISHU_APP_SECRET:-}
FEISHU_BITABLE_WIKI_TOKEN: ${FEISHU_BITABLE_WIKI_TOKEN:-}
FEISHU_BITABLE_APP_TOKEN: ${FEISHU_BITABLE_APP_TOKEN:-}
FEISHU_BITABLE_TABLE_ID: ${FEISHU_BITABLE_TABLE_ID:-}
FEISHU_SYNC_INTERVAL_MS: ${FEISHU_SYNC_INTERVAL_MS:-600000}
PAYMENT_FM_API_BASE_URL: ${PAYMENT_FM_API_BASE_URL:-}
PAYMENT_FM_MERCHANT_NUM: ${PAYMENT_FM_MERCHANT_NUM:-}
PAYMENT_FM_PAY_TYPE: ${PAYMENT_FM_PAY_TYPE:-aloop}
PAYMENT_FM_CALLBACK_URL: ${PAYMENT_FM_CALLBACK_URL:-https://example.com/api/zhifuxpay/notify}
PAYMENT_FM_RETURN_URL: ${PAYMENT_FM_RETURN_URL:-http://localhost:3000/payment/result}
PAYMENT_FM_SECRET: ${PAYMENT_FM_SECRET:-}
PAYMENT_ORDER_EXPIRE_MINUTES: ${PAYMENT_ORDER_EXPIRE_MINUTES:-5}
LOG_ALERT_ENABLED: ${LOG_ALERT_ENABLED:-true}
LOG_ALERT_ENV: ${LOG_ALERT_ENV:-production}
LOG_ERROR_DEDUPE_SECONDS: ${LOG_ERROR_DEDUPE_SECONDS:-300}
LOG_ERROR_LIMIT_PER_MINUTE: ${LOG_ERROR_LIMIT_PER_MINUTE:-20}
LOG_4XX_WARN_THRESHOLD: ${LOG_4XX_WARN_THRESHOLD:-20}
LOG_4XX_WARN_WINDOW_SECONDS: ${LOG_4XX_WARN_WINDOW_SECONDS:-300}
PERSONAL_WECHAT_PUSH_WEBHOOK_URL: ${PERSONAL_WECHAT_PUSH_WEBHOOK_URL:-}
FEISHU_ERROR_WEBHOOK_URL: ${FEISHU_ERROR_WEBHOOK_URL:-}
FEISHU_ERROR_WEBHOOK_SECRET: ${FEISHU_ERROR_WEBHOOK_SECRET:-}
FEISHU_ERROR_MENTION_ALL: ${FEISHU_ERROR_MENTION_ALL:-true}
FEISHU_WARN_WEBHOOK_URL: ${FEISHU_WARN_WEBHOOK_URL:-}
FEISHU_WARN_WEBHOOK_SECRET: ${FEISHU_WARN_WEBHOOK_SECRET:-}
FEISHU_WARN_MENTION_ALL: ${FEISHU_WARN_MENTION_ALL:-false}
expose:
- "4000"
volumes:
- ./logs/api:/app/logs
depends_on:
logs-init:
condition: service_completed_successfully
postgres:
condition: service_healthy
redis:
condition: service_healthy
logging:
driver: json-file
options:
max-size: "20m"
max-file: "5"
restart: unless-stopped
postgres:
image: postgres:16-alpine
command:
[
"postgres",
"-c",
"shared_buffers=128MB",
"-c",
"max_connections=30",
"-c",
"logging_collector=on",
"-c",
"log_directory=/var/log/postgresql",
"-c",
"log_filename=postgresql-%Y-%m-%d.log",
"-c",
"log_statement=none"
]
environment:
POSTGRES_DB: blog_db
POSTGRES_USER: postgres
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:?请在 .env 中设置 POSTGRES_PASSWORD}
volumes:
- postgres_data:/var/lib/postgresql/data
- ./sql:/docker-entrypoint-initdb.d:ro
- ./logs/postgresql:/var/log/postgresql
healthcheck:
test: ["CMD-SHELL", "pg_isready -U postgres -d blog_db"]
interval: 10s
timeout: 5s
retries: 5
depends_on:
logs-init:
condition: service_completed_successfully
logging:
driver: json-file
options:
max-size: "20m"
max-file: "5"
restart: unless-stopped
redis:
image: redis:7-alpine
command:
[
"redis-server",
"--save",
"",
"--appendonly",
"no",
"--logfile",
"/var/log/redis/redis.log"
]
volumes:
- ./logs/redis:/var/log/redis
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 10s
timeout: 5s
retries: 5
depends_on:
logs-init:
condition: service_completed_successfully
logging:
driver: json-file
options:
max-size: "20m"
max-file: "5"
restart: unless-stopped
volumes:
postgres_data:
三、.github CI CD自动化脚本编写
CI
name: CI
on:
pull_request:
push:
branches: [main]
jobs:
check:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v4
with:
version: 10.33.0
- uses: actions/setup-node@v4
with:
node-version: 22
cache: pnpm
- run: pnpm install --frozen-lockfile
- run: pnpm lint
- run: pnpm test
- run: pnpm typecheck
- run: pnpm build
CD
name: Deploy
on:
workflow_run:
workflows: [CI]
types: [completed]
branches: [main]
permissions:
contents: read
packages: write
jobs:
build-and-deploy:
if: ${{ github.event.workflow_run.conclusion == 'success' }}
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- uses: docker/build-push-action@v6
with:
context: .
file: apps/web/Dockerfile
push: true
tags: ghcr.io/linxiaodong1/myblog-web:latest
build-args: |
NEXT_PUBLIC_API_BASE_URL=${{ secrets.NEXT_PUBLIC_API_BASE_URL }}
- uses: docker/build-push-action@v6
with:
context: .
file: apps/api/Dockerfile
push: true
tags: ghcr.io/linxiaodong1/myblog-api:latest
- name: Pack deploy files
run: |
mkdir -p deploy-bundle
cp docker-compose.prod.yml deploy-bundle/
cp -r nginx deploy-bundle/
cp -r sql deploy-bundle/
tar -czf deploy-bundle.tar.gz -C deploy-bundle .
- uses: appleboy/scp-action@v0.1.7
with:
host: ${{ secrets.SERVER_HOST }}
username: ${{ secrets.SERVER_USER }}
key: ${{ secrets.SERVER_SSH_KEY }}
source: deploy-bundle.tar.gz
target: ${{ secrets.SERVER_APP_DIR }}
- uses: appleboy/ssh-action@v1.2.0
with:
host: ${{ secrets.SERVER_HOST }}
username: ${{ secrets.SERVER_USER }}
key: ${{ secrets.SERVER_SSH_KEY }}
script: |
cd ${{ secrets.SERVER_APP_DIR }}
tar -xzf deploy-bundle.tar.gz
rm -f deploy-bundle.tar.gz
docker compose -f docker-compose.prod.yml pull
docker compose -f docker-compose.prod.yml up -d
docker compose -f docker-compose.prod.yml ps
docker image prune -f