GitHub Actions 自动部署
使用 GitHub Actions + GHCR + Watchtower 实现 Docker 容器自动部署
架构概览
GitHub 仓库
└── push to main
│
▼
GitHub Actions(构建 Docker 镜像)
└── 推送至 GHCR(GitHub Container Registry)
│
▼
Watchtower(每 60 秒轮询 GHCR)
└── 检测到新镜像 → 自动拉取并重启容器
│
▼
Docker 容器(加入 webnet 网络)
└── nginx 通过容器名反向代理
│
▼
用户访问(HTTPS)
项目端配置
Dockerfile
多阶段构建,最终镜像只包含 .output/,不含源码:
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 ./
COPY docs/package.json ./docs/
RUN corepack install && pnpm install --frozen-lockfile
FROM deps AS build
COPY . .
RUN pnpm exec nuxt-module-build build --stub \
&& pnpm exec nuxt-module-build prepare \
&& pnpm exec nuxt build docs
FROM node:24-alpine AS runtime
WORKDIR /app
RUN addgroup -S app && adduser -S app -G app
COPY --from=build --chown=app:app /app/docs/.output ./
USER app
ENV NODE_ENV=production
ENV HOST=0.0.0.0
ENV PORT=3000
EXPOSE 3000
CMD ["node", "server/index.mjs"]
corepack install 读取 package.json 的 packageManager 字段自动激活对应 pnpm 版本,无需手动锁定版本号。GitHub Actions 工作流
.github/workflows/deploy.yml
name: Deploy Docs
on:
push:
branches:
- main
env:
REGISTRY: ghcr.io
IMAGE: ghcr.io/${{ github.repository }}/docs
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 }}
cache-from: type=gha
cache-to: type=gha,mode=max
无需配置任何 GitHub Secrets,
GITHUB_TOKEN 由 GitHub 自动提供。GHCR 镜像命名规则:
GITHUB_TOKEN 只能写入当前仓库关联的包。镜像名必须是 ghcr.io/<owner>/<repo> 或其子路径(如 ghcr.io/<owner>/<repo>/docs)。如果使用 ghcr.io/mhaibaraai/movk-nuxt-docs(在仓库名后追加 -docs),会被 GHCR 视为另一个独立包,GITHUB_TOKEN 无权写入,推送时报 permission_denied: write_package。正确做法是使用子路径格式 ghcr.io/${{ github.repository }}/docs,对应镜像名为 ghcr.io/mhaibaraai/movk-nuxt/docs。服务器端配置
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: movk-nuxt-docs
WATCHTOWER_POLL_INTERVAL=60:每 60 秒检查一次新镜像WATCHTOWER_CLEANUP=true:更新后自动删除旧镜像command: movk-nuxt-docs:只监控指定容器,多个容器用空格分隔/root/.docker:/config:ro:挂载 GHCR 登录凭证
首次使用前需在服务器上登录 GHCR(如果镜像设为私有):GitHub PAT 只需
docker login ghcr.io -u <github_username> -p <github_pat>
read:packages 权限。nginx 反向代理
~/nginx/conf.d/nuxt.mhaibaraai.cn.conf
server {
listen 80;
server_name nuxt.mhaibaraai.cn;
return 301 https://$server_name$request_uri;
}
server {
listen 443 ssl;
http2 on;
server_name nuxt.mhaibaraai.cn;
ssl_certificate /etc/nginx/ssl/mhaibaraai.cn.pem;
ssl_certificate_key /etc/nginx/ssl/mhaibaraai.cn.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 movk-nuxt-docs: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/nuxt.mhaibaraai.cn.access.log;
error_log /var/log/nginx/nuxt.mhaibaraai.cn.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
手动首次启动应用容器
在服务器上创建应用目录,用 docker-compose.yml + .env 管理容器和环境变量:
mkdir ~/webs/movk-nuxt-docs
~/webs/movk-nuxt-docs/.env
NUXT_GITHUB_TOKEN=your_token_here
AI_GATEWAY_API_KEY=your_key_here
NUXT_SESSION_PASSWORD=your_password_here
~/webs/movk-nuxt-docs/docker-compose.yml
services:
movk-nuxt-docs:
image: ghcr.io/mhaibaraai/movk-nuxt/docs:latest
container_name: movk-nuxt-docs
restart: unless-stopped
env_file: .env
networks:
- webnet
networks:
webnet:
external: true
Watchtower 只负责更新已有容器,不负责首次创建:
cd ~/webs/movk-nuxt-docs && sudo docker compose up -d
上传 nginx 配置并重载
# 上传 conf 文件到 ~/nginx/conf.d/
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.yml 的 command 里追加容器名:
~/watchtower/docker-compose.yml
command: movk-nuxt-docs new-project-name
sh
sudo docker compose up -d
nginx:新建 conf.d/new-domain.conf,参照 nuxt.mhaibaraai.cn.conf,修改 server_name 和 set $upstream:
sh
sudo docker exec nginx nginx -t && sudo docker exec nginx nginx -s reload
首次启动容器:创建项目目录,参照 movk-nuxt-docs 的模板创建 .env 和 docker-compose.yml,执行 sudo docker compose up -d。
DNS:域名解析控制台添加 new-domain.mhaibaraai.cn → 服务器 IP 的 A 记录。
常用运维命令
# 查看所有容器状态
sudo docker ps
# 查看应用日志
sudo docker logs movk-nuxt-docs -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/movk-nuxt-docs && sudo docker compose pull && sudo docker compose up -d