Site Logo

YiXuan

GitHub Actions 自动部署

View source
使用 GitHub Actions + GHCR + Watchtower 实现 Docker 容器自动部署

架构概览

GitHub 仓库
  └── push to main
        │
        ▼
GitHub Actions(构建 Docker 镜像)
  └── 推送至 GHCR(GitHub Container Registry)
        │
        ▼
Watchtower(每 60 秒轮询 GHCR)
  └── 检测到新镜像 → 自动拉取并重启容器
        │
        ▼
Docker 容器(加入 webnet 网络)
  └── nginx 通过容器名反向代理
        │
        ▼
用户访问(HTTPS)

项目端配置

Dockerfile

三阶段构建:deps 安装依赖,build 编译产物,runtime 只包含 .output/,不含源码和 node_modules:

Dockerfile
FROM node:24-alpine AS base
RUN corepack enable

FROM base AS deps
WORKDIR /app
COPY package.json pnpm-lock.yaml pnpm-workspace.yaml ./
RUN --mount=type=cache,id=pnpm,target=/root/.local/share/pnpm/store \
    corepack install && pnpm install --frozen-lockfile --ignore-scripts

FROM deps AS build
WORKDIR /app
ENV NODE_OPTIONS=--max-old-space-size=8192
COPY . .
RUN --mount=type=secret,id=SECRET_1 \
    --mount=type=secret,id=SECRET_2 \
    for f in /run/secrets/*; do echo "$(basename $f)=$(cat $f)"; done > .env && \
    pnpm build && rm -f .env

FROM node:24-alpine AS runtime
WORKDIR /app
RUN addgroup -S app && adduser -S app -G app
COPY --from=build --chown=app:app /app/.output ./
USER app
ENV NODE_ENV=production \
    HOST=0.0.0.0 \
    PORT=3000
EXPOSE 3000
CMD ["node", "server/index.mjs"]
  • --mount=type=cache 缓存 pnpm store,源码变更时无需重新下载依赖。
  • --mount=type=secret 将 GitHub Secrets 以文件形式挂载到 /run/secrets/,只在该 RUN 指令执行期间可见,不会写入任何镜像层。
  • secrets 通过遍历写入 .env,Nuxt CLI 构建时自动加载,构建完成后立即删除。
  • NODE_OPTIONS=--max-old-space-size=8192 将 Node.js 堆内存上限设为 8GB,避免 Nitro 打包阶段 OOM。

GitHub Actions 工作流

.github/workflows/deploy.yml
name: Deploy

on:
  push:
    branches:
      - main

env:
  REGISTRY: ghcr.io
  IMAGE: ghcr.io/${{ github.repository }}

permissions:
  contents: read
  packages: write

jobs:
  deploy:
    runs-on: ubuntu-latest
    timeout-minutes: 20

    steps:
      - uses: actions/checkout@v6
      - uses: docker/setup-buildx-action@v4

      - name: Log in to GHCR
        uses: docker/login-action@v4
        with:
          registry: ${{ env.REGISTRY }}
          username: ${{ github.actor }}
          password: ${{ secrets.GITHUB_TOKEN }}

      - name: Build and push Docker image
        uses: docker/build-push-action@v7
        with:
          context: .
          push: true
          tags: |
            ${{ env.IMAGE }}:latest
            ${{ env.IMAGE }}:${{ github.sha }}
          secrets: |
            SECRET_1=${{ secrets.SECRET_1 }}
            SECRET_2=${{ secrets.SECRET_2 }}
          cache-from: type=gha
          cache-to: type=gha,mode=max

GitHub Secrets 配置

前往仓库 Settings → Secrets and variables → Actions,根据项目需要新增 Repository secrets:

Secret 名称用途
SECRET_1构建时和运行时所需的第一个密钥
SECRET_2构建时和运行时所需的第二个密钥
GITHUB_TOKEN 由 GitHub 自动提供,无需配置,仅用于推送镜像到 GHCR。其余 secrets 需要手动创建。
GHCR 镜像命名规则GITHUB_TOKEN 只能写入当前仓库关联的包。镜像名必须是 ghcr.io/<owner>/<repo> 或其子路径(如 ghcr.io/<owner>/<repo>/app)。如果使用与仓库名不匹配的独立包名,会被 GHCR 视为另一个独立包GITHUB_TOKEN 无权写入,推送时报 permission_denied: write_package。正确做法是使用 ghcr.io/${{ github.repository }} 或其子路径格式。
为什么不用 build-args 传递 secrets?ARG + ENV 会将变量值固化进镜像层,任何有镜像访问权限的人都可以通过 docker inspect 读取。--mount=type=secret 是 BuildKit 提供的安全方案,secret 仅在构建期间临时挂载,不留痕迹。

服务器端配置

Watchtower(自动更新)

~/watchtower/docker-compose.yml
services:
  watchtower:
    image: containrrr/watchtower:latest
    container_name: watchtower
    restart: unless-stopped
    environment:
      - WATCHTOWER_POLL_INTERVAL=60
      - WATCHTOWER_CLEANUP=true
      - WATCHTOWER_INCLUDE_RESTARTING=true
      - DOCKER_CONFIG=/config
      - DOCKER_API_VERSION=1.41
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock
      - /root/.docker:/config:ro
    command: <container-name>
  • WATCHTOWER_POLL_INTERVAL=60:每 60 秒检查一次新镜像
  • WATCHTOWER_CLEANUP=true:更新后自动删除旧镜像
  • command: <container-name>:只监控指定容器,多个容器用空格分隔
  • /root/.docker:/config:ro:挂载 GHCR 登录凭证
首次使用前需在服务器上登录 GHCR(如果镜像设为私有):
docker login ghcr.io -u <github_username> -p <github_pat>
GitHub PAT 只需 read:packages 权限。

应用容器

~/webs/<container-name>/docker-compose.yml
services:
  <container-name>:
    image: ghcr.io/<owner>/<repo>:latest
    container_name: <container-name>
    restart: unless-stopped
    env_file: .env
    networks:
      - webnet

networks:
  webnet:
    external: true
~/webs/<container-name>/.env
SECRET_1=your_value_here
SECRET_2=your_value_here
运行时环境变量(.env)与构建时 secrets 是两条独立的注入链路。Nuxt 生产服务器不读取 .env 文件,必须通过容器环境变量传入,env_file 是 Docker Compose 的标准做法。

nginx 反向代理

~/nginx/conf.d/<your-domain>.conf
server {
    listen 80;
    server_name <your-domain>;
    return 301 https://$server_name$request_uri;
}

server {
    listen 443 ssl;
    http2 on;
    server_name <your-domain>;

    ssl_certificate /etc/nginx/ssl/<your-domain>.pem;
    ssl_certificate_key /etc/nginx/ssl/<your-domain>.key;
    ssl_protocols TLSv1.2 TLSv1.3;
    ssl_ciphers HIGH:!aNULL:!MD5;
    ssl_prefer_server_ciphers on;
    ssl_session_cache shared:SSL:10m;
    ssl_session_timeout 10m;

    add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
    add_header X-Frame-Options "SAMEORIGIN" always;
    add_header X-Content-Type-Options "nosniff" always;

    location / {
        resolver 127.0.0.11 valid=30s;
        set $upstream <container-name>:3000;
        proxy_pass http://$upstream;
        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;
    }

    access_log /var/log/nginx/<your-domain>.access.log;
    error_log /var/log/nginx/<your-domain>.error.log;
}
resolver 127.0.0.11 使用 Docker 内置 DNS,配合 set $upstream 变量实现动态解析。容器不存在时 nginx 仍可正常启动(返回 502),避免了 upstream 块在启动时强制解析导致的报错。

首次部署步骤

登录 GHCR(如果镜像私有)

sudo docker login ghcr.io -u <github_username> -p <github_pat>

启动 Watchtower

cd ~/watchtower && sudo docker compose up -d

手动首次启动应用容器

mkdir -p ~/webs/<container-name>

创建 .envdocker-compose.yml(参照上方配置),然后启动:

cd ~/webs/<container-name> && sudo docker compose up -d

Watchtower 只负责更新已有容器,不负责首次创建。

上传 nginx 配置并重载

sudo docker exec nginx nginx -t && sudo docker exec nginx nginx -s reload

触发首次 CI 构建

git commit --allow-empty -m "chore: 触发首次部署"
git push

之后每次推送 main,全流程自动完成。

新增项目部署模板

每新增一个需要部署的 Nuxt/Node 项目,重复以下步骤:

项目仓库:新建 Dockerfile + .github/workflows/deploy.yml,修改镜像名和容器名。

Watchtower:在 ~/watchtower/docker-compose.ymlcommand 里追加容器名:

~/watchtower/docker-compose.yml
command: existing-container new-container-name
sudo docker compose up -d

nginx:新建 conf.d/<new-domain>.conf,参照现有配置修改 server_nameset $upstream

sudo docker exec nginx nginx -t && sudo docker exec nginx nginx -s reload

首次启动容器:创建项目目录,参照模板创建 .envdocker-compose.yml,执行 sudo docker compose up -d

DNS:域名解析控制台添加 <new-domain> → 服务器 IP 的 A 记录。

常用运维命令

# 查看所有容器状态
sudo docker ps

# 查看应用日志
sudo docker logs <container-name> -f --tail 100

# 查看 Watchtower 日志(确认自动更新是否生效)
sudo docker logs watchtower -f --tail 50

# 重载 nginx 配置(无停机)
sudo docker exec nginx nginx -t && sudo docker exec nginx nginx -s reload

# 手动触发镜像更新(无需等待 Watchtower 轮询)
cd ~/webs/<container-name> && sudo docker compose pull && sudo docker compose up -d

# 进入容器内部调试
sudo docker exec -it <container-name> sh
Copyright © 2024 - 2026 YiXuan - MIT License