DevOpsTechnical Deep Dive

保姆级教程:GitHub CI/CD + Docker 自动部署到腾讯云服务器

发布时间2026/04/08
分类DevOps
预计阅读15 分钟
作者吴长龙
*

从零开始搭建 CI/CD 流水线,实现代码推送自动部署到腾讯云服务器,敏感数据全程加密。

01.前言

每次代码更新都要手动 SSH 登录服务器、拉取代码、重启服务?太繁琐了!

本文将手把手教你搭建一套 GitHub CI/CD + Docker Compose 自动化部署流程:

code snippetcode
代码推送 → 自动测试 → 构建 Docker 镜像 → 推送到 Docker Hub → SSH 部署到服务器

效果:代码推送到 GitHub 后,1-3 分钟内自动完成部署,全程无需人工干预。

---

02.一、整体架构

先看一张架构图,理解整个流程:

code snippetcode
┌─────────────────────────────────────────────────────────────────────┐
│                         GitHub Actions                               │
├─────────────────────────────────────────────────────────────────────┤
│  ┌──────────┐    ┌──────────┐    ┌──────────┐    ┌──────────┐      │
│  │  代码    │ →  │  测试    │ →  │  构建    │ →  │  部署    │      │
│  │  推送    │    │  阶段    │    │  镜像    │    │  阶段    │      │
│  └──────────┘    └──────────┘    └──────────┘    └──────────┘      │
│                                      ↓                 ↓            │
│                              ┌──────────────┐  ┌──────────────┐     │
│                              │  Docker Hub  │  │  SSH 连接    │     │
│                              │  镜像仓库    │  │  腾讯云      │     │
│                              └──────────────┘  └──────────────┘     │
└─────────────────────────────────────────────────────────────────────┘
                                      ↓
┌─────────────────────────────────────────────────────────────────────┐
│                          腾讯云服务器                                 │
├─────────────────────────────────────────────────────────────────────┤
│  ┌─────────────────────────────────────────────────────────────┐   │
│  │                    Docker Compose                            │   │
│  │  ┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────┐           │   │
│  │  │Frontend │ │ Backend │ │  Redis  │ │Postgres │           │   │
│  │  │ :80     │ │ :3001   │ │ :6379   │ │ :5432   │           │   │
│  │  └─────────┘ └─────────┘ └─────────┘ └─────────┘           │   │
│  └─────────────────────────────────────────────────────────────┘   │
└─────────────────────────────────────────────────────────────────────┘

关键点

  • 敏感数据加密:所有密钥存储在 GitHub Secrets,不会暴露在代码中
  • 容器化部署:Docker Compose 自动管理所有服务
  • 自动化流程:推送代码即触发部署

---

03.二、服务器准备(腾讯云)

2.1 购买服务器

推荐配置:

  • CPU:2 核
  • 内存:4GB
  • 系统:Ubuntu 22.04 或 CentOS 8
  • 带宽:3Mbps+

2.2 安装 Docker

SSH 登录服务器后,执行以下命令:

bash snippetbash
# 1. 更新系统
apt update && apt upgrade -y

# 2. 一键安装 Docker(官方脚本)
curl -fsSL https://get.docker.com | sh

# 3. 启动 Docker 并设置开机自启
systemctl enable docker
systemctl start docker

# 4. 验证安装
docker --version
docker compose version

就这样! 不需要安装 PostgreSQL、Redis 等,Docker Compose 会自动部署。

2.3 配置防火墙

在腾讯云控制台开放端口:

端口用途
22SSH
80HTTP
443HTTPS

---

04.三、配置 SSH 密钥

3.1 生成 SSH 密钥对

在你本地电脑执行:

bash snippetbash
# 生成新的 SSH 密钥对
ssh-keygen -t ed25519 -C "github-actions" -f ~/.ssh/github_actions_key

# 查看公钥(下一步要用)
cat ~/.ssh/github_actions_key.pub

# 查看私钥(后面要添加到 GitHub)
cat ~/.ssh/github_actions_key

3.2 添加公钥到服务器

方式一:手动添加

bash snippetbash
# SSH 登录服务器
ssh root@你的服务器IP

# 添加公钥
echo "你的公钥内容" >> ~/.ssh/authorized_keys

# 设置权限
chmod 600 ~/.ssh/authorized_keys

方式二:一条命令添加

bash snippetbash
# 在本地电脑执行
ssh-copy-id -i ~/.ssh/github_actions_key.pub root@你的服务器IP

3.3 验证 SSH 连接

bash snippetbash
ssh -i ~/.ssh/github_actions_key root@你的服务器IP

能免密登录就成功了!

---

05.四、配置 Docker Hub

我们需要一个地方存储构建好的 Docker 镜像。

4.1 注册 Docker Hub

访问 hub.docker.com 注册账号。

4.2 创建 Access Token

  • 登录 Docker Hub
  • 点击右上角头像 → Account Settings
  • 选择 Security 标签
  • 点击 New Access Token
  • 填写名称(如 github-actions),权限选择 Read, Write, Delete
  • 立即复制 Token(只显示一次!)

---

06.五、配置 GitHub Secrets

这是最关键的一步,所有敏感数据都存储在这里。

5.1 进入 Secrets 页面

  • 打开你的 GitHub 仓库
  • 点击 SettingsSecrets and variablesActions
  • 点击 New repository secret

5.2 添加必需的 Secrets

#### 🔐 服务器连接

Secret 名称说明
REMOTE_HOST123.45.67.89服务器 IP 地址
REMOTE_USERrootSSH 用户名
SSH_PRIVATE_KEY-----BEGIN OPENSSH PRIVATE KEY-----\n...完整私钥内容
REMOTE_PORT22SSH 端口(可省略,默认 22)

SSH_PRIVATE_KEY 格式示例

code snippetcode
-----BEGIN OPENSSH PRIVATE KEY-----
b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW
...(很多行)...
UwJa2y8K7wJAAAAEHNzaC1hY3Rpb25zQGdtYWlsAQIDBAU=
-----END OPENSSH PRIVATE KEY-----

#### 🐳 Docker Hub

Secret 名称说明
DOCKER_USERNAMEyourusernameDocker Hub 用户名
DOCKER_PASSWORDdckr_pat_xxxAccess Token

#### 🔑 应用配置

Secret 名称说明
OPENAI_API_KEYsk-xxxOpenAI API Key
TAVILY_API_KEYtvly-xxxTavily 搜索 API(按需)

#### ⚠️ 可选配置

Secret 名称说明
POSTGRES_PASSWORDyour_secure_password数据库密码(不设则默认 postgres
JWT_SECRET32位以上随机字符串JWT 签名密钥
LANGSMITH_API_KEYlsv2-xxxLangSmith 追踪(可选)

---

07.六、创建 GitHub Actions 工作流

在项目根目录创建文件:

code snippetcode
.github/
└── workflows/
    └── ci-cd.yml

6.1 完整的 CI/CD 配置

yaml snippetyaml
name: CI/CD Pipeline

on:
  push:
    branches:
      - main
      - master
    paths-ignore:
      - '**.md'
      - 'docs/**'
  pull_request:
    branches:
      - main
      - master
  workflow_dispatch:  # 支持手动触发

env:
  NODE_VERSION: '20'
  PNPM_VERSION: '9.0.0'

jobs:
  # ==================== 测试阶段 ====================
  test:
    name: 测试
    runs-on: ubuntu-latest
    steps:
      - name: 检出代码
        uses: actions/checkout@v4

      - name: 安装 pnpm
        uses: pnpm/action-setup@v4
        with:
          version: ${{ env.PNPM_VERSION }}

      - name: 设置 Node.js
        uses: actions/setup-node@v4
        with:
          node-version: ${{ env.NODE_VERSION }}
          cache: 'pnpm'

      - name: 安装依赖
        run: pnpm install --frozen-lockfile

      - name: 运行测试
        run: pnpm test
        env:
          NODE_ENV: test

  # ==================== 构建阶段 ====================
  build:
    name: 构建 Docker 镜像
    runs-on: ubuntu-latest
    needs: test
    if: github.event_name == 'push'
    outputs:
      sha_tag: ${{ steps.meta.outputs.sha_tag }}
    steps:
      - name: 检出代码
        uses: actions/checkout@v4

      - name: 设置 Docker Buildx
        uses: docker/setup-buildx-action@v3

      - name: 登录 Docker Hub
        uses: docker/login-action@v3
        with:
          username: ${{ secrets.DOCKER_USERNAME }}
          password: ${{ secrets.DOCKER_PASSWORD }}

      - name: 提取镜像标签
        id: meta
        run: |
          SHA_SHORT=$(git rev-parse --short HEAD)
          echo "sha_tag=${SHA_SHORT}" >> $GITHUB_OUTPUT
          echo "backend_image=${{ secrets.DOCKER_USERNAME }}/your-app-backend" >> $GITHUB_OUTPUT
          echo "frontend_image=${{ secrets.DOCKER_USERNAME }}/your-app-frontend" >> $GITHUB_OUTPUT

      - name: 构建并推送后端镜像
        uses: docker/build-push-action@v5
        with:
          context: ./backend
          push: true
          tags: |
            ${{ steps.meta.outputs.backend_image }}:latest
            ${{ steps.meta.outputs.backend_image }}:${{ steps.meta.outputs.sha_tag }}
          cache-from: type=gha
          cache-to: type=gha,mode=max

      - name: 构建并推送前端镜像
        uses: docker/build-push-action@v5
        with:
          context: ./frontend
          push: true
          tags: |
            ${{ steps.meta.outputs.frontend_image }}:latest
            ${{ steps.meta.outputs.frontend_image }}:${{ steps.meta.outputs.sha_tag }}
          cache-from: type=gha
          cache-to: type=gha,mode=max

  # ==================== 部署阶段 ====================
  deploy:
    name: 部署到腾讯云
    runs-on: ubuntu-latest
    needs: build
    if: github.event_name == 'push'
    environment:
      name: production
      url: ${{ vars.DEPLOY_URL }}
    steps:
      - name: 检出代码
        uses: actions/checkout@v4

      - name: 部署到服务器
        uses: appleboy/ssh-action@v1.0.3
        with:
          host: ${{ secrets.REMOTE_HOST }}
          username: ${{ secrets.REMOTE_USER }}
          key: ${{ secrets.SSH_PRIVATE_KEY }}
          port: ${{ secrets.REMOTE_PORT || 22 }}
          script: |
            set -e

            echo "=========================================="
            echo "🚀 开始部署"
            echo "=========================================="

            # 项目目录
            PROJECT_DIR="/opt/your-app"
            DOCKER_USERNAME="${{ secrets.DOCKER_USERNAME }}"

            # 创建项目目录
            mkdir -p $PROJECT_DIR
            cd $PROJECT_DIR

            # 拉取最新代码(获取 docker-compose.prod.yml)
            if [ -d ".git" ]; then
              git pull origin main
            else
              git clone ${{ github.server_url }}/${{ github.repository }}.git .
            fi

            # 创建环境文件(敏感数据从 Secrets 注入)
            cat > .env << ENVEOF
            # ========== 必需配置 ==========
            NODE_ENV=production
            OPENAI_API_KEY=${{ secrets.OPENAI_API_KEY }}
            TAVILY_API_KEY=${{ secrets.TAVILY_API_KEY }}

            # ========== PostgreSQL 配置 ==========
            POSTGRES_USER=${{ secrets.POSTGRES_USER }}
            POSTGRES_PASSWORD=${{ secrets.POSTGRES_PASSWORD }}
            POSTGRES_DB=your_app

            # ========== 可选配置 ==========
            LANGSMITH_API_KEY=${{ secrets.LANGSMITH_API_KEY }}
            JWT_SECRET=${{ secrets.JWT_SECRET }}

            # Docker 镜像
            DOCKER_USERNAME=${DOCKER_USERNAME}
            ENVEOF

            # 拉取最新镜像
            echo "📦 拉取最新 Docker 镜像..."
            docker compose -f docker-compose.prod.yml pull

            # 重启服务
            echo "🔄 重启服务..."
            docker compose -f docker-compose.prod.yml down --remove-orphans
            docker compose -f docker-compose.prod.yml up -d

            # 等待服务启动
            echo "⏳ 等待服务启动..."
            sleep 10

            # 健康检查
            echo "🏥 执行健康检查..."
            for i in {1..30}; do
              if curl -sf http://localhost/health > /dev/null 2>&1; then
                echo "✅ 服务健康检查通过!"
                break
              fi
              if [ $i -eq 30 ]; then
                echo "❌ 健康检查失败,查看日志..."
                docker compose -f docker-compose.prod.yml logs --tail=50
                exit 1
              fi
              echo "等待服务就绪... ($i/30)"
              sleep 2
            done

            # 清理旧镜像
            echo "🧹 清理无用镜像..."
            docker image prune -af --filter "until=24h"

            echo "=========================================="
            echo "✅ 部署完成!"
            echo "=========================================="

---

08.七、创建 Docker Compose 配置

7.1 生产环境配置

创建 docker-compose.prod.yml

yaml snippetyaml
services:
  # ==================== 前端服务 ====================
  frontend:
    image: ${DOCKER_USERNAME}/your-app-frontend:latest
    ports:
      - "80:80"
    depends_on:
      backend:
        condition: service_healthy
    restart: unless-stopped
    networks:
      - app-network

  # ==================== 后端服务 ====================
  backend:
    image: ${DOCKER_USERNAME}/your-app-backend:latest
    environment:
      - NODE_ENV=production
      - PORT=3001
      - OPENAI_API_KEY=${OPENAI_API_KEY}
      - REDIS_URL=redis://redis:6379
      - DATABASE_URL=postgresql://${POSTGRES_USER:-postgres}:${POSTGRES_PASSWORD:-postgres}@postgres:5432/${POSTGRES_DB:-your_app}
    depends_on:
      redis:
        condition: service_healthy
      postgres:
        condition: service_healthy
    restart: unless-stopped
    networks:
      - app-network
    healthcheck:
      test: ["CMD", "wget", "--spider", "http://localhost:3001/health"]
      interval: 30s
      timeout: 10s
      retries: 3
      start_period: 30s

  # ==================== Redis 缓存 ====================
  redis:
    image: redis:7-alpine
    command: redis-server --appendonly yes
    volumes:
      - redis_data:/data
    restart: unless-stopped
    networks:
      - app-network
    healthcheck:
      test: ["CMD", "redis-cli", "ping"]
      interval: 10s
      timeout: 5s
      retries: 5

  # ==================== PostgreSQL 数据库 ====================
  postgres:
    image: postgres:15-alpine
    environment:
      - POSTGRES_USER=${POSTGRES_USER:-postgres}
      - POSTGRES_PASSWORD=${POSTGRES_PASSWORD:-postgres}
      - POSTGRES_DB=${POSTGRES_DB:-your_app}
    volumes:
      - postgres_data:/var/lib/postgresql/data
    restart: unless-stopped
    networks:
      - app-network
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U postgres"]
      interval: 10s
      timeout: 5s
      retries: 5

networks:
  app-network:
    driver: bridge

volumes:
  redis_data:
  postgres_data:

---

09.八、触发部署

8.1 自动触发

bash snippetbash
# 推送代码到 main 分支
git add .
git commit -m "feat: 添加 CI/CD 配置"
git push origin main

8.2 手动触发

  • 打开 GitHub 仓库
  • 点击 Actions 标签
  • 选择 CI/CD Pipeline
  • 点击 Run workflow

8.3 查看部署状态

  • Actions 页面查看工作流运行状态
  • 点击具体的运行记录查看详细日志
  • 成功后会显示绿色 ✅

---

10.九、验证部署

9.1 检查服务状态

bash snippetbash
# SSH 登录服务器
ssh root@你的服务器IP

# 查看容器状态
docker compose -f /opt/your-app/docker-compose.prod.yml ps

# 应该看到类似输出:
# NAME         STATUS    PORTS
# frontend     running   0.0.0.0:80->80/tcp
# backend      running   3001/tcp
# redis        running   6379/tcp
# postgres     running   5432/tcp

9.2 访问应用

浏览器打开 http://你的服务器IP,应该能看到你的应用。

9.3 查看日志

bash snippetbash
# 查看所有服务日志
docker compose -f /opt/your-app/docker-compose.prod.yml logs -f

# 查看特定服务日志
docker compose -f /opt/your-app/docker-compose.prod.yml logs -f backend

---

11.十、常见问题排查

问题 1:SSH 连接失败

原因

  • 私钥格式不正确
  • 公钥未添加到服务器
  • 防火墙阻止 22 端口

解决方案

bash snippetbash
# 检查私钥格式(确保包含换行符)
echo "${{ secrets.SSH_PRIVATE_KEY }}" | head -1
# 应该输出:-----BEGIN OPENSSH PRIVATE KEY-----

# 测试 SSH 连接
ssh -i ~/.ssh/github_actions_key root@你的服务器IP

问题 2:Docker 镜像拉取失败

原因

  • Docker Hub 凭据错误
  • 镜像不存在

解决方案

bash snippetbash
# 手动登录 Docker Hub 测试
docker login -u 你的用户名 -p 你的token

# 手动拉取镜像测试
docker pull 你的用户名/your-app-backend:latest

问题 3:健康检查失败

原因

  • 后端启动失败
  • 数据库连接失败
  • 环境变量未正确注入

解决方案

bash snippetbash
# 查看后端日志
docker compose -f /opt/your-app/docker-compose.prod.yml logs backend

# 检查环境变量
docker compose -f /opt/your-app/docker-compose.prod.yml exec backend env | grep OPENAI

问题 4:数据库连接失败

原因

  • PostgreSQL 未就绪
  • 连接字符串错误

解决方案

bash snippetbash
# 检查 PostgreSQL 状态
docker compose -f /opt/your-app/docker-compose.prod.yml exec postgres pg_isready

# 进入后端容器测试连接
docker compose -f /opt/your-app/docker-compose.prod.yml exec backend sh
# 然后测试数据库连接

---

12.十一、安全最佳实践

11.1 Secrets 管理

实践说明
✅ 使用 GitHub Secrets敏感数据不提交到代码
✅ 定期轮换密钥每 3-6 个月更新一次
✅ 最小权限原则只添加必需的 Secrets
❌ 不要在日志中打印echo $SECRET 会暴露

11.2 网络安全

yaml snippetyaml
# 限制端口暴露
services:
  backend:
    ports: []  # 不暴露到主机,仅内部网络访问

  postgres:
    ports: []  # 不暴露到主机

11.3 数据库安全

yaml snippetyaml
postgres:
  environment:
    - POSTGRES_PASSWORD=${POSTGRES_PASSWORD}  # 强密码
  # 不要暴露 5432 端口到公网

---

13.十二、进阶配置

12.1 添加 HTTPS(使用 Caddy)

yaml snippetyaml
# docker-compose.prod.yml 添加
caddy:
  image: caddy:2
  ports:
    - "443:443"
  volumes:
    - ./Caddyfile:/etc/caddy/Caddyfile
    - caddy_data:/data
  depends_on:
    - frontend

12.2 添加监控(使用 Prometheus + Grafana)

yaml snippetyaml
prometheus:
  image: prom/prometheus
  volumes:
    - ./prometheus.yml:/etc/prometheus/prometheus.yml
  ports:
    - "9090:9090"

grafana:
  image: grafana/grafana
  ports:
    - "3000:3000"

12.3 多环境部署

yaml snippetyaml
# dev 环境触发
on:
  push:
    branches: [develop]
    paths-ignore: ['**.md']

# prod 环境触发
on:
  push:
    branches: [main]

---

14.总结

恭喜你完成了 CI/CD 流水线的搭建!

核心要点回顾

  • 服务器只需要安装 Docker,其他服务由 Docker Compose 自动部署
  • 敏感数据存储在 GitHub Secrets,永远不会暴露在代码中
  • 推送代码即触发部署,整个过程 1-3 分钟完成
  • 健康检查确保服务可用,部署失败会自动回滚

下一步

  • 添加 HTTPS 支持
  • 配置域名解析
  • 设置监控告警

有问题欢迎留言讨论!