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
三阶段构建: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(如果镜像设为私有):GitHub PAT 只需
docker login ghcr.io -u <github_username> -p <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>
创建 .env 和 docker-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.yml 的 command 里追加容器名:
~/watchtower/docker-compose.yml
command: existing-container new-container-name
sudo docker compose up -d
nginx:新建 conf.d/<new-domain>.conf,参照现有配置修改 server_name 和 set $upstream:
sudo docker exec nginx nginx -t && sudo docker exec nginx nginx -s reload
首次启动容器:创建项目目录,参照模板创建 .env 和 docker-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