// Created At 2026-03-16

GitHub Actions + Docker 生产级自动化部署(进阶篇)

📚 系列导航

本系列共四篇,覆盖从静态网站到生产级 Docker 部署的全流程,建议按顺序阅读:

  1. 静态网站自动化部署(静态篇)
    —— 纯前端资源的自动化发布,Caddy 自动 HTTPS 和 SPA 路由支持。
  2. 动态网站自动化部署(动态篇)
    —— 后端服务进程管理、环境变量注入、数据库迁移,结合 Caddy 反向代理。
  3. Docker 极简入门(入门篇)
    —— 从零开始用 Docker + GitHub Actions 实现 CI/CD 流水线。
  4. Docker 生产级部署(进阶篇)
    —— 多容器编排、健康检查、数据库迁移、自动 HTTPS,打造可靠的生产环境。

📌 版本声明

本文档所有工具均采用 2026 年最新稳定版

工具版本说明
Node.js24.x最新 LTS 版本
pnpm10.x高性能包管理器
Docker Engine29.x支持 BuildKit 和多阶段构建
Docker Composev5新版 Compose 规范,支持 name 项目和健康检查依赖
Caddy2.8+自动 HTTPS 的反向代理
PostgreSQL17 (alpine)轻量级数据库
Drizzle ORM0.30+TypeScript ORM,支持迁移
PM25+进程守护工具
GitHub Actions最新CI/CD 平台(checkout@v4, ssh-action@v1.0.0 等)

🎯 本章目标

在入门篇的基础上,你将学会:

  • ✅ 多容器生产级编排(应用 + 数据库 + 反向代理)
  • ✅ 健康检查与容器启动顺序控制
  • ✅ 容器网络与服务发现(通过服务名通信)
  • ✅ 环境变量安全传递(GitHub Secrets + 服务器 .env 权限)
  • ✅ 数据持久化与自动备份
  • ✅ 数据库迁移自动化(以 Drizzle ORM 为例)
  • ✅ 零停机部署策略(解决端口冲突)
  • ✅ 镜像加速器配置与国内优化
  • ✅ 常见问题深度排查手册

最终你将拥有一套可上生产、自动修复、安全可控的 Docker 部署流水线。


🏗️ 系统架构图

┌─────────────┐     ┌─────────────────┐     ┌─────────────────┐
│  本地开发    │────▶│  GitHub Actions │────▶│   阿里云 ACR    │
└─────────────┘     └─────────────────┘     └─────────────────┘
                                                      │
                                                      ▼
┌───────────────────────────────────────────────────────┐
│                    阿里云 ECS                          │
│  ┌─────────────┐    ┌─────────────┐    ┌─────────────┐ │
│  │  PostgreSQL │◀──▶│  Nuxt 应用  │◀──▶│    Caddy    │ │
│  │   (容器)    │    │   (容器)    │    │   (容器)    │ │
│  └─────────────┘    └─────────────┘    └─────────────┘ │
│         ▲                 ▲                  ▲         │
│         └─────────────────┼──────────────────┘         │
│                   同一网络 `app-network`                │
└───────────────────────────────────────────────────────┘

📦 前置准备

  1. 完成入门篇,已能跑通单容器部署。
  2. 一个域名(例如 your-domain.com)并解析到服务器 IP。
  3. 阿里云 ACR 已配置好命名空间和固定密码。
  4. 服务器安全组开放 8044322 端口。
  5. 项目已集成 Drizzle ORM,并生成迁移文件(已提交至 Git)。

🚀 第一部分:生产级 Docker Compose 配置

1.1 目录结构

/var/www/my-app/
├── docker-compose.yml
├── .env                  # 环境变量(手动创建,不提交 Git)
├── Caddyfile             # Caddy 配置
└── backups/              # 数据库备份目录(可选)

1.2 编写生产级 docker-compose.yml

name: my-app  # 固定项目名,避免网络混乱

services:
  postgres:
    image: postgres:alpine
    container_name: my-app-db
    restart: always
    environment:
      POSTGRES_DB: ${POSTGRES_DB}
      POSTGRES_USER: ${POSTGRES_USER}
      POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
    volumes:
      - postgres_data:/var/lib/postgresql/data
    networks:
      - app-network
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER} -d ${POSTGRES_DB}"]
      interval: 10s
      timeout: 5s
      retries: 5
      start_period: 10s
    logging:
      driver: "json-file"
      options:
        max-size: "10m"
        max-file: "3"

  app:
    image: ${ACR_REGISTRY}/my-app:latest
    container_name: my-app
    restart: always
    # 不暴露端口到宿主机,仅内部网络访问(由 Caddy 代理)
    environment:
      NUXT_PUBLIC_SITE_URL: ${NUXT_PUBLIC_SITE_URL}
      NUXT_SESSION_PASSWORD: ${NUXT_SESSION_PASSWORD}
      NUXT_OAUTH_GITHUB_CLIENT_ID: ${NUXT_OAUTH_GITHUB_CLIENT_ID}
      NUXT_OAUTH_GITHUB_CLIENT_SECRET: ${NUXT_OAUTH_GITHUB_CLIENT_SECRET}
      NUXT_DATABASE_URL: postgresql://${POSTGRES_USER}:${POSTGRES_PASSWORD}@postgres:5432/${POSTGRES_DB}
    depends_on:
      postgres:
        condition: service_healthy
    networks:
      - app-network
    healthcheck:
      test: ["CMD", "node", "-e", "require('http').get('http://localhost:3000', (r) => {process.exit(r.statusCode === 200 ? 0 : 1)})"]
      interval: 30s
      timeout: 5s
      retries: 3
      start_period: 15s
    logging:
      driver: "json-file"
      options:
        max-size: "10m"
        max-file: "3"

  caddy:
    image: caddy:alpine
    container_name: my-app-caddy
    restart: always
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - ./Caddyfile:/etc/caddy/Caddyfile
      - caddy_data:/data
      - caddy_config:/config
    networks:
      - app-network
    depends_on:
      app:
        condition: service_healthy
    logging:
      driver: "json-file"
      options:
        max-size: "10m"
        max-file: "3"

volumes:
  postgres_data:
  caddy_data:
  caddy_config:

networks:
  app-network:
    driver: bridge

关键点

  • name 固定项目名,避免因目录名变化导致网络不一致。
  • 所有服务在同一网络,通过服务名通信。
  • 应用容器不暴露端口,流量全走 Caddy,提升安全性。
  • depends_on + condition: service_healthy 确保启动顺序。
  • 日志切割防止磁盘爆满。

1.3 Caddyfile 配置(自动 HTTPS)

请将 your-domain.com 替换为你的实际域名。

www.your-domain.com {
    redir https://your-domain.com{uri} permanent
}

your-domain.com {
    reverse_proxy app:3000
    encode gzip zstd
    header {
        X-Content-Type-Options "nosniff"
        X-Frame-Options "DENY"
        X-XSS-Protection "1; mode=block"
        Referrer-Policy "strict-origin-when-cross-origin"
    }
}

🔐 第二部分:环境变量安全

2.1 GitHub Secrets 完整列表

Secret 名称说明
ACR_REGISTRY阿里云镜像仓库地址(如 crpi-xxx.cn-beijing.personal.cr.aliyuncs.com
ACR_USERNAME阿里云账号邮箱
ACR_PASSWORDACR 固定密码
SERVER_HOST服务器 IP
SERVER_USERSSH 用户名
SSH_PRIVATE_KEYSSH 私钥(包含 BEGINEND 行,保持完整换行)
POSTGRES_DB数据库名
POSTGRES_USER数据库用户
POSTGRES_PASSWORD数据库密码
NUXT_PUBLIC_SITE_URL网站域名(如 https://your-domain.com
NUXT_SESSION_PASSWORD会话加密密钥(至少 32 位)
NUXT_OAUTH_GITHUB_CLIENT_IDGitHub OAuth Client ID
NUXT_OAUTH_GITHUB_CLIENT_SECRETGitHub OAuth Client Secret

2.2 服务器 .env 文件权限

在首次部署前,手动创建 /var/www/my-app/.env,并设置权限:

chmod 600 /var/www/my-app/.env

内容示例(请替换为实际值):

POSTGRES_DB=myapp
POSTGRES_USER=postgres
POSTGRES_PASSWORD=StrongPassword123!
ACR_REGISTRY=crpi-xxx.cn-beijing.personal.cr.aliyuncs.com
NUXT_PUBLIC_SITE_URL=https://your-domain.com
NUXT_SESSION_PASSWORD=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
NUXT_OAUTH_GITHUB_CLIENT_ID=xxxxxxxxxx
NUXT_OAUTH_GITHUB_CLIENT_SECRET=xxxxxxxxxxxxxxxxxxxxxxxx

⚙️ 第三部分:GitHub Actions 工作流(进阶版)

name: Production Deploy

on:
  push:
    branches: [ main ]
  workflow_dispatch:  # 允许手动触发

jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Login to ACR
        uses: docker/login-action@v3
        with:
          registry: ${{ secrets.ACR_REGISTRY }}
          username: ${{ secrets.ACR_USERNAME }}
          password: ${{ secrets.ACR_PASSWORD }}

      - name: Build and push
        uses: docker/build-push-action@v5
        with:
          push: true
          tags: |
            ${{ secrets.ACR_REGISTRY }}/my-app:latest
            ${{ secrets.ACR_REGISTRY }}/my-app:${{ github.sha }}
          cache-from: type=gha
          cache-to: type=gha,mode=max

      - name: Deploy to server
        uses: appleboy/ssh-action@v1.0.0
        env:
          ACR_REGISTRY: ${{ secrets.ACR_REGISTRY }}
          ACR_USERNAME: ${{ secrets.ACR_USERNAME }}
          ACR_PASSWORD: ${{ secrets.ACR_PASSWORD }}
          POSTGRES_DB: ${{ secrets.POSTGRES_DB }}
          POSTGRES_USER: ${{ secrets.POSTGRES_USER }}
          POSTGRES_PASSWORD: ${{ secrets.POSTGRES_PASSWORD }}
          NUXT_PUBLIC_SITE_URL: ${{ secrets.NUXT_PUBLIC_SITE_URL }}
          NUXT_SESSION_PASSWORD: ${{ secrets.NUXT_SESSION_PASSWORD }}
          NUXT_OAUTH_GITHUB_CLIENT_ID: ${{ secrets.NUXT_OAUTH_GITHUB_CLIENT_ID }}
          NUXT_OAUTH_GITHUB_CLIENT_SECRET: ${{ secrets.NUXT_OAUTH_GITHUB_CLIENT_SECRET }}
        with:
          host: ${{ secrets.SERVER_HOST }}
          username: ${{ secrets.SERVER_USER }}
          key: ${{ secrets.SSH_PRIVATE_KEY }}
          envs: ACR_REGISTRY, ACR_USERNAME, ACR_PASSWORD, POSTGRES_DB, POSTGRES_USER, POSTGRES_PASSWORD, NUXT_PUBLIC_SITE_URL, NUXT_SESSION_PASSWORD, NUXT_OAUTH_GITHUB_CLIENT_ID, NUXT_OAUTH_GITHUB_CLIENT_SECRET
          script: |
            set -e
            cd /var/www/my-app

            # 写入环境变量(注意用 EOF 不加引号,确保变量展开)
            cat > .env << EOF
            POSTGRES_DB=$POSTGRES_DB
            POSTGRES_USER=$POSTGRES_USER
            POSTGRES_PASSWORD=$POSTGRES_PASSWORD
            ACR_REGISTRY=$ACR_REGISTRY
            NUXT_PUBLIC_SITE_URL=$NUXT_PUBLIC_SITE_URL
            NUXT_SESSION_PASSWORD=$NUXT_SESSION_PASSWORD
            NUXT_OAUTH_GITHUB_CLIENT_ID=$NUXT_OAUTH_GITHUB_CLIENT_ID
            NUXT_OAUTH_GITHUB_CLIENT_SECRET=$NUXT_OAUTH_GITHUB_CLIENT_SECRET
            EOF

            chmod 600 .env

            # 登录 ACR
            echo "$ACR_PASSWORD" | docker login "$ACR_REGISTRY" -u "$ACR_USERNAME" --password-stdin

            # 拉取最新镜像
            docker compose pull app

            # 执行数据库迁移(确保容器内有 drizzle-kit 或临时安装)
            # 方案:使用 npm 全局安装 drizzle-kit 后执行迁移
            docker compose run --rm app sh -c "npm install -g drizzle-kit && drizzle-kit migrate"

            # 重启应用(强制重新创建容器,避免端口冲突)
            docker compose up -d --force-recreate app

            # 重启 Caddy(如有更新)
            docker compose up -d --force-recreate caddy

            # 清理旧镜像(保留最近24小时)
            docker image prune -f --filter "until=24h"

进阶要点

  • 使用 cache-from 加速构建。
  • 远程脚本中执行数据库迁移(使用 npm install -g 确保有 drizzle-kit,避免依赖缺失)。
  • --force-recreate 确保旧容器被完全替换,解决端口残留问题。
  • 清理 24 小时前的旧镜像,避免磁盘占满。
  • envs 列表中包含了所有需要传递的变量,确保远程 shell 能正确读取。

🧪 第四部分:服务器初始化(生产准备)

4.1 安装 Docker 并配置镜像加速器

curl -fsSL https://get.docker.com | sh
sudo usermod -aG docker $USER
# 重新登录或执行 newgrp docker 使组生效

sudo tee /etc/docker/daemon.json <<-'EOF'
{
  "registry-mirrors": ["https://your-mirror-id.mirror.aliyuncs.com"],
  "log-driver": "json-file",
  "log-opts": {
    "max-size": "10m",
    "max-file": "3"
  }
}
EOF
sudo systemctl restart docker

4.2 创建项目目录并上传文件

mkdir -p /var/www/my-app
cd /var/www/my-app
# 将本地的 docker-compose.yml 和 Caddyfile 上传到该目录
# 例如:scp docker-compose.yml Caddyfile root@your-server:/var/www/my-app/

4.3 首次启动(手动)

# 手动创建 .env 文件(参照 2.2 的示例)
vim .env
chmod 600 .env

# 启动所有服务
docker compose up -d

4.4 验证服务

docker compose ps
curl -I http://localhost:3000  # 应返回 200(应用内部端口)
curl -I https://your-domain.com # 应返回 200(通过 Caddy)

🔧 第五部分:深度问题排查手册

现象可能原因解决方案
Actions 中 SSH 连接失败私钥格式错误 / 安全组未开放 22 端口检查 Secrets 中的私钥是否包含完整换行;检查安全组入方向规则
Caddy 容器无法启动,端口 80/443 被占用宿主机有其他 Web 服务(如系统级 Caddy、Nginx)sudo lsof -i :80 -i :443 找到并停止进程;或修改端口映射
应用无法连接数据库,日志显示 getaddrinfo EAI_AGAIN postgres容器间网络问题 / 数据库服务名错误确认 apppostgres 在同一网络;检查 DATABASE_URL 中的主机名是否为 postgres
数据库迁移失败,提示 drizzle-kit: not found容器内未安装 drizzle-kit已改为使用 npm install -g drizzle-kit,确保网络通畅;也可在 Dockerfile 中预装
应用容器不断重启健康检查失败 / 依赖服务未就绪查看日志:docker logs my-app --tail 50;检查 depends_on 条件
HTTPS 证书未自动生成域名解析未生效 / 80 端口未开放检查 DNS 解析;确保 Caddy 能访问外网
镜像拉取慢未配置镜像加速器按 4.1 配置加速器并重启 Docker
部署后网站未更新容器未重启 / 镜像标签未更新检查 Actions 日志;手动执行 docker compose pull && docker compose up -d
宿主机重启后容器未自动恢复未设置 Docker 开机自启sudo systemctl enable docker;容器已设置 restart: always,会自动启动
.env 文件权限导致 Secrets 泄露风险权限设置不当确保 .env 权限为 600,属主为运行 Docker 的用户

📈 第六部分:日常运维

6.1 常用命令

# 查看所有服务状态
docker compose ps

# 查看实时日志
docker compose logs -f app

# 进入容器
docker exec -it my-app sh

# 备份数据库
docker exec my-app-db pg_dump -U postgres myapp > backups/backup_$(date +%Y%m%d).sql

# 恢复数据库
cat backups/backup.sql | docker exec -i my-app-db psql -U postgres -d myapp

# 清理未使用的镜像
docker image prune -f

# 清理未使用的卷(谨慎操作)
docker volume prune -f

6.2 自动备份(可选)

添加定时任务(crontab -e):

0 2 * * * cd /var/www/my-app && docker exec my-app-db pg_dump -U postgres myapp > backups/backup_$(date +\%Y\%m\%d).sql

🏁 总结

至此,你已经构建了一套完整的、生产可用的 Docker 自动化部署体系:

  • ✅ 多服务容器化编排
  • ✅ 健康检查与依赖控制
  • ✅ 环境变量安全注入
  • ✅ 数据库迁移自动化
  • ✅ 零停机部署
  • ✅ 自动 HTTPS
  • ✅ 数据持久化与备份
  • ✅ 日志切割与镜像清理

这套方案可支撑中小型项目稳定运行。未来若需扩展微服务、K8s 等,亦可基于此基础演进。

如果这篇文档对你有帮助,可以请我喝杯咖啡 ☕️
Ali PayWechat Pay
评论区
© 2026 MOONGATE