我的博客又集成CICD了

继上一章,填坑

CICD基于Gitea+Drone+Docker

基础环境准备

  1. 用上一章的KVM + Cockpit的组合,创建一台debian13.3的虚拟机,完成基础配置
  2. 用上一章的安装docker方法,完成基础安装
  3. usermod -aG sudo 用户名给普通用户root权限
  4. 切换到普通用户

环境搭建

docker

  1. 配置docker镜像加速

    bash
    sudo mkdir -p /etc/docker sudo tee /etc/docker/daemon.json > /dev/null << 'EOF' { "registry-mirrors": [ "https://docker.1ms.run" ] } EOF

    然后重载docker

    bash
    sudo systemctl daemon-reload sudo systemctl restart docker
  2. sudo usermod -aG docker 用户名添加自己的普通用户到docker组,以实现docker命令时不用sudo。操作完需要exit再重新登陆

Drone

按照Gitea | Drone(中文站与 Gitea 集成 | Drone)配置Drone

我需要用docker同时部署Drone Server和Runner,所以这里直接在/opt/drone/目录下创建文件docker-compose.yaml

yaml
version: "3.8" services: drone-server: image: drone/drone:2 container_name: drone-server ports: - "30000:80" # 这里30000是外部端口 volumes: - /opt/drone/data:/data # 绝对路径的方式挂载/opt/drone/data目录到容器的/data目录 environment: - DRONE_GITEA_SERVER= # 填写Gitea服务器地址 - DRONE_GITEA_CLIENT_ID= # Gitea OAuth应用ID - DRONE_GITEA_CLIENT_SECRET= # Gitea OAuth应用密钥 - DRONE_RPC_SECRET= # 可通过openssl rand -hex 16生成共享密钥,用于和runner通信 - DRONE_SERVER_HOST= # Drone Server的主机名 - DRONE_SERVER_PROTO=https # 这里按需求,我最终要用https协议,所以填https - DRONE_USER_CREATE= # 非常建议填写!内容是username:Gitea用户名,admin:true drone-runner: image: drone/drone-runner-docker:1 container_name: drone-runner depends_on: - drone-server volumes: - /var/run/docker.sock:/var/run/docker.sock environment: - DRONE_RPC_HOST=drone-server # 这里也根据实际情况,用该yaml也可以不用改 - DRONE_RPC_PROTO=http - DRONE_RPC_SECRET= # 与drone-server中生成的共享密钥一致 - DRONE_RUNNER_CAPACITY=2 - DRONE_RUNNER_NAME=drone-runner-main # 按照自己心情取名儿

最终drone server服务暴露在30000端口,我通过frp的tcp隧道反代该服务,并再通过openResty进行一次http反代,实现公网https协议加密访问;

其中遇到过的一些坑:

  • 经过测试,需要在上述配置中将drone-server.environment.DRONE_SERVER_PROTO调整为https协议,经过两层反代和一层TLS加密(最终是以https协议访问的该webui)的webui才能正常登陆!否则会在登陆时,出现错误:

    text
    授权失败 Unregistered Redirect URI 因为检测到无效请求,授权失败。请尝试联系您授权应用的管理员。
  • DRONE_USER_CREATE配置的重要性,在下面会体现。如果不填写这一行,有可能第一个创建的用户不会默认是具备Admin权限的账户,导致后续做CICD的时候,解决不了linter: untrusted repositories cannot mount host volumes的错误

为项目添加CICD

  1. 打开Drone Server的webui,使用Gitea进行登陆

  2. 接下来在仓库列表中,找到需要的仓库,并选择在Setting页面点击Activite Repository按钮以激活仓库

  3. 仓库 -> Setting -> General -> Project Settings,找到Trusted,打开~然后点下面按钮保存更改

    因为咱在上面给Runner挂载了docker.socket,如果不信任项目,则部署时会出现错误linter: untrusted repositories cannot mount host volumes

  4. 仓库 -> Setting -> Secrets,右侧点击New Secret按钮添加项目需要用到的密钥。填写Name和Value即可,Allow Pull Request选项不要勾选

  5. 接下来可以回到项目了。在项目的根目录,创建一个.drone.yml文件,内容参考:

    yaml
    kind: pipeline type: docker name: deploy trigger: branch: - main - master # 这里需要根据自己的项目情况进行调整,我的主分支叫master,所以这里加一个,否则即使提交到主分支,也不会触发构建! steps: - name: deploy image: docker:27 environment: NEXT_PUBLIC_API_BASE: from_secret: NEXT_PUBLIC_API_BASE # 我的前端需要,因此按照格式填写 DATABASE_HOST: from_secret: DATABASE_HOST DATABASE_PORT: from_secret: DATABASE_PORT DATABASE_NAME: from_secret: DATABASE_NAME DATABASE_USERNAME: from_secret: DATABASE_USERNAME DATABASE_PASSWORD: from_secret: DATABASE_PASSWORD WEBAUTHN_RP_ID: from_secret: WEBAUTHN_RP_ID WEBAUTHN_ORIGIN: from_secret: WEBAUTHN_ORIGIN WEBAUTHN_RP_NAME: from_secret: WEBAUTHN_RP_NAME ALIYUN_ACCESS_KEY_ID: from_secret: ALIYUN_ACCESS_KEY_ID ALIYUN_ACCESS_KEY_SECRET: from_secret: ALIYUN_ACCESS_KEY_SECRET ALIYUN_OSS_STS_ROLE_ARN: from_secret: ALIYUN_OSS_STS_ROLE_ARN # 后端需要,依旧按需填写 volumes: - name: dockersock path: /var/run/docker.sock commands: - docker compose build - docker compose up -d volumes: - name: dockersock host: path: /var/run/docker.sock
  6. 依旧项目根目录,创建文件docker-compose.yml,内容参考:

    yaml
    version: "3.9" services: frontend: build: context: ./apps/frontend args: NEXT_PUBLIC_API_BASE: ${NEXT_PUBLIC_API_BASE} # 前端在编译时需要,因此从这里传入 container_name: tonepage-frontend restart: always ports: - "3000:3000" backend: build: context: ./apps/backend container_name: tonepage-backend restart: always ports: - "3001:3001" environment: DATABASE_HOST: ${DATABASE_HOST} DATABASE_PORT: ${DATABASE_PORT} DATABASE_NAME: ${DATABASE_NAME} DATABASE_USERNAME: ${DATABASE_USERNAME} DATABASE_PASSWORD: ${DATABASE_PASSWORD} WEBAUTHN_RP_ID: ${WEBAUTHN_RP_ID} WEBAUTHN_ORIGIN: ${WEBAUTHN_ORIGIN} WEBAUTHN_RP_NAME: ${WEBAUTHN_RP_NAME} ALIYUN_ACCESS_KEY_ID: ${ALIYUN_ACCESS_KEY_ID} ALIYUN_ACCESS_KEY_SECRET: ${ALIYUN_ACCESS_KEY_SECRET} ALIYUN_OSS_STS_ROLE_ARN: ${ALIYUN_OSS_STS_ROLE_ARN} # 后端运行时需要,这样传入
  7. 分别进入前后端项目,编写Dockerfile

    dockerfile
    # /apps/frontend/Dockerfile 前端 # 安装依赖 FROM node:22-alpine AS deps RUN apk add --no-cache libc6-compat RUN npm install -g pnpm WORKDIR /app COPY package.json pnpm-lock.yaml ./ RUN pnpm install --frozen-lockfile # 编译 FROM node:22-alpine AS builder RUN npm install -g pnpm WORKDIR /app ARG NEXT_PUBLIC_API_BASE ENV NEXT_PUBLIC_API_BASE=$NEXT_PUBLIC_API_BASE COPY --from=deps /app/node_modules ./node_modules COPY . . RUN pnpm run build # 运行 FROM node:22-alpine AS runner WORKDIR /app ENV NODE_ENV=production ENV TZ=Asia/Shanghai RUN apk add --no-cache tzdata COPY --from=builder /app/.next/standalone ./ COPY --from=builder /app/.next/static ./.next/static COPY --from=builder /app/public ./public EXPOSE 3000 CMD ["node", "server.js"]
    dockerfile
    # /apps/backend/Dockerfile 后端 FROM node:22-alpine AS builder RUN npm install -g pnpm WORKDIR /app COPY package.json pnpm-lock.yaml ./ RUN pnpm install --frozen-lockfile COPY . . RUN pnpm run build RUN CI=true pnpm prune --prod FROM node:22-alpine AS runner WORKDIR /app ENV NODE_ENV=production ENV TZ=Asia/Shanghai ENV PGTZ=UTC RUN apk add --no-cache tzdata COPY --from=builder /app/node_modules ./node_modules COPY --from=builder /app/dist ./dist COPY --from=builder /app/package.json ./package.json EXPOSE 3001 CMD ["node", "dist/main.js"]
  8. 提交上述文件,然后push,接着坐等Runner编译部署...第一次会等比较久,后续有缓存就快很多了

    图片

    通过啦通过啦~经过测试也确实是部署上了,现在就实现了:代码编写并提交到主分支,然后push一下,等待runner编译部署,就可以直接在生产环境链接看到最新代码的效果了!


到上面就差不多啦~这里我正好把我的博客部署架构微调一下,可以根据感兴趣程度选择是否接着看

最开始项目是用Gitea Actions Runner(act runner) + k3s部署的,所以访问入口是直接接在前端的,前端通过NextConfig的rewrites配置,将/api路径的请求从前端经过并转发到后端:

typescript
async rewrites() { return [ { source: '/api/:path*', destination: `${process.env.API_BASE}/api/:path*`, } ] }

后续运行k3s的主机没地儿放了,就得迁移到腾讯云EdgeOne,这里用rewrites就会出问题,折腾老半天,最终选择了直接让用户浏览器和nextjs的ssr访问后端,用户浏览器的跨域问题通过后端配置cors解决:

typescript
app.enableCors({ origin: [ 'http://localhost:3000', // 开发调试用 'https://www.tonesc.cn', // 生产环境用 ], credentials: true, methods: ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS'], allowedHeaders: ['Content-Type', 'Cookie'],// 这个时候鉴权系统改用cookie了 });

EO用了一段时间后,发现大陆延迟挺高,而且优选IP又有点需要维护成本、还具有违背平台服务条款的风险。最终还是回到了现在docker部署前后端的方案...

但是有过EO Pages这种云平台经历后,就不再想接着用rewrites的方案了,万一哪天又回去,可不又得重构嘛。

依然现在已经用上了docker compose,那何不再加一个nginx做/api转发呢?对于用户浏览器,直接请求同源网站即可,SSR就让它通过docker容器间网络 直接请求后端服务。因此直接在/opt/nginx创建docker-compose.yml

yaml
version: "3.9" services: nginx: image: nginx:alpine container_name: gateway-nginx restart: always network_mode: "host" # 这里我比较懒,直接用的host模式 volumes: - ./conf.d:/etc/nginx/conf.d - ./logs:/var/log/nginx # 相对路径挂载配置文件和日志文件目录

然后在./conf.d目录下创建一个site.conf文件,编写配置规则

text
server { listen 80; server_name tonesc.cn; location / { proxy_pass http://127.0.0.1:3000; 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; } location /api/ { proxy_pass http://127.0.0.1:3001; 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; } }

如果需要保留请求路径中的/api前缀,则location /api/下的proxy_pass转发地址内容就不要在最后添加“/”了。比如我添加“/”:proxy_pass http://127.0.0.1:3001/;那么匹配到/api/的请求例http://tonesc.cn/api/hello,请求到127.0.0.1:3001的就变成了http://127.0.0.1:3001/hello

最后docker compose up -d一下,让nginx跑起来

最最后,在宿主机的frp,添加一条指向该虚拟机80端口的tcp隧道,再让OpenResty指向frp反代的端口、添加TLS证书、添加DNS解析,就可以顺利在公网访问啦~