<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>CI/CD on Zata-砸它</title><link>https://www.zata.cc/tags/ci/cd/</link><description>Recent content in CI/CD on Zata-砸它</description><generator>Hugo -- gohugo.io</generator><language>zh-cn</language><copyright>Example Person</copyright><lastBuildDate>Mon, 08 Jun 2026 13:53:45 +0800</lastBuildDate><atom:link href="https://www.zata.cc/tags/ci/cd/index.xml" rel="self" type="application/rss+xml"/><item><title>Docker 和 Traefik 一键安装脚本</title><link>https://www.zata.cc/p/docker-traefik-install-script/</link><pubDate>Mon, 08 Jun 2026 00:30:00 +0800</pubDate><guid>https://www.zata.cc/p/docker-traefik-install-script/</guid><description>&lt;img src="https://www.zata.cc/p/docker-traefik-install-script/images/index/index.png" alt="Featured image of post Docker 和 Traefik 一键安装脚本" />&lt;p>在单台 VPS 上部署多个 Web 服务时，我现在更倾向于先准备一个公共入口层：Docker 负责跑应用，Traefik 负责根据域名把请求转发到不同容器，并自动接入 HTTPS。&lt;/p>
&lt;p>这篇文章放一个可直接运行的脚本，用来在 Ubuntu/Debian 服务器上安装 Docker Engine、Docker Compose 插件，并启动一个基础 Traefik 网关。&lt;/p>
&lt;p>脚本文件和这篇文章放在同一个目录里，可以直接查看或下载：&lt;a class="link" href="install-docker-traefik.sh" >install-docker-traefik.sh&lt;/a>。&lt;/p>
&lt;h2 id="适用场景">适用场景
&lt;/h2>&lt;p>适合这些情况：&lt;/p>
&lt;ul>
&lt;li>一台新的 Ubuntu/Debian 云服务器。&lt;/li>
&lt;li>准备用 Docker Compose 部署多个站点或服务。&lt;/li>
&lt;li>想用 Traefik 统一管理 &lt;code>80&lt;/code>、&lt;code>443&lt;/code> 入口。&lt;/li>
&lt;li>不想再为每个应用单独写 Nginx 配置。&lt;/li>
&lt;/ul>
&lt;p>不适合这些情况：&lt;/p>
&lt;ul>
&lt;li>已经使用 Kubernetes Ingress。&lt;/li>
&lt;li>已经由 1Panel、Coolify、Dokploy 等平台完整接管反向代理。&lt;/li>
&lt;li>需要复杂的企业级网关策略，比如多租户权限、统一认证、灰度发布等。&lt;/li>
&lt;/ul>
&lt;h2 id="快速运行">快速运行
&lt;/h2>&lt;p>如果只是安装 Docker 和 Traefik，不需要立即配置 HTTPS：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">chmod +x install-docker-traefik.sh
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">./install-docker-traefik.sh
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>如果要给真实网站签发 Let&amp;rsquo;s Encrypt 证书，建议填写 &lt;code>ACME_EMAIL&lt;/code>（&lt;strong>必须是真实邮箱&lt;/strong>，脚本会拒绝 &lt;code>you@example.com&lt;/code> 这类占位值，Let&amp;rsquo;s Encrypt 也只会用占位邮箱会让证书一直申请失败）：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">&lt;span class="nv">ACME_EMAIL&lt;/span>&lt;span class="o">=&lt;/span>you@your-domain.com ./install-docker-traefik.sh
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>脚本会把 Traefik 配置写到：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-text" data-lang="text">&lt;span class="line">&lt;span class="cl">/opt/traefik
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Dashboard 默认只监听服务器本机：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-text" data-lang="text">&lt;span class="line">&lt;span class="cl">http://127.0.0.1:8080/dashboard/
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>远程查看可以用 SSH 端口转发：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">ssh -L 8080:127.0.0.1:8080 user@server
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h2 id="参数说明">参数说明
&lt;/h2>&lt;p>常用环境变量如下：&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>变量&lt;/th>
&lt;th>默认值&lt;/th>
&lt;th>说明&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>&lt;code>TRAEFIK_DIR&lt;/code>&lt;/td>
&lt;td>&lt;code>/opt/traefik&lt;/code>&lt;/td>
&lt;td>Traefik 配置目录&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>TRAEFIK_NETWORK&lt;/code>&lt;/td>
&lt;td>&lt;code>traefik&lt;/code>&lt;/td>
&lt;td>公共 Docker 网络名&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>TRAEFIK_IMAGE&lt;/code>&lt;/td>
&lt;td>&lt;code>traefik:v3.7&lt;/code>&lt;/td>
&lt;td>Traefik 镜像版本&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>ACME_EMAIL&lt;/code>&lt;/td>
&lt;td>空&lt;/td>
&lt;td>Let&amp;rsquo;s Encrypt 邮箱，不填则不启用自动 HTTPS&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>ENABLE_HTTPS_REDIRECT&lt;/code>&lt;/td>
&lt;td>自动判断&lt;/td>
&lt;td>填了 &lt;code>ACME_EMAIL&lt;/code> 时默认开启 HTTP 到 HTTPS 跳转&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>INSTALL_SAMPLE&lt;/code>&lt;/td>
&lt;td>&lt;code>false&lt;/code>&lt;/td>
&lt;td>是否启动 &lt;code>whoami&lt;/code> 测试服务&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>WHOAMI_HOST&lt;/code>&lt;/td>
&lt;td>&lt;code>whoami.localhost&lt;/code>&lt;/td>
&lt;td>只给测试服务使用，不是你的真实网站域名&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>&lt;code>ACME_EMAIL&lt;/code> 可以不填。不填时，脚本仍然会安装 Docker 和 Traefik，但不会配置 Let&amp;rsquo;s Encrypt 证书解析器。&lt;/p>
&lt;p>&lt;code>WHOAMI_HOST&lt;/code> 也可以不填。它只在 &lt;code>INSTALL_SAMPLE=true&lt;/code> 时用于测试服务，真实网站应该在自己的 &lt;code>docker-compose.yml&lt;/code> 里配置 Traefik labels。&lt;/p>
&lt;h2 id="真实网站接入方式">真实网站接入方式
&lt;/h2>&lt;p>真实网站容器需要加入同一个 Traefik 网络：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="cl">&lt;span class="nt">networks&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">traefik&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">external&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="kc">true&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>然后给 Web 服务加 labels：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="cl">&lt;span class="nt">services&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">site&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">image&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">your-site-image&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">restart&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">unless-stopped&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">networks&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="l">traefik&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">labels&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="s2">&amp;#34;traefik.enable=true&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="s2">&amp;#34;traefik.http.routers.site.rule=Host(`example.com`)&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="s2">&amp;#34;traefik.http.routers.site.entrypoints=websecure&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="s2">&amp;#34;traefik.http.routers.site.tls=true&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="s2">&amp;#34;traefik.http.routers.site.tls.certresolver=letsencrypt&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="s2">&amp;#34;traefik.http.services.site.loadbalancer.server.port=80&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">networks&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">traefik&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">external&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="kc">true&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>这里的 &lt;code>example.com&lt;/code> 要替换成真实域名，并且 DNS 需要提前解析到这台服务器 IP。服务器安全组或防火墙也要放行 &lt;code>80&lt;/code> 和 &lt;code>443&lt;/code>。&lt;/p>
&lt;h2 id="后续如何做-cd">后续如何做 CD
&lt;/h2>&lt;p>这个脚本只解决服务器入口层：安装 Docker、启动 Traefik、准备公共网络。它不会负责“代码变了以后怎么自动发布”。真正的 CD 需要额外建立一条发布链路：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-text" data-lang="text">&lt;span class="line">&lt;span class="cl">push 代码
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> -&amp;gt; CI/CD 平台构建 Docker 镜像
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> -&amp;gt; 推送镜像到镜像仓库
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> -&amp;gt; SSH 到服务器
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> -&amp;gt; 更新应用 compose 的镜像 tag
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> -&amp;gt; docker compose pull &amp;amp;&amp;amp; docker compose up -d
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> -&amp;gt; Traefik 根据 labels 接管流量
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>这里用 GitHub Actions 举例，但思路对 GitLab CI、Woodpecker CI、Gitea Actions 也一样。Traefik 不关心你用哪个 CI/CD 平台，它只关心最终跑起来的容器是否在同一个 Docker 网络里，并且有没有正确的 labels。&lt;/p>
&lt;h3 id="一每个项目准备自己的镜像">一、每个项目准备自己的镜像
&lt;/h3>&lt;p>每个要部署的项目都应该能被构建成一个 Docker 镜像。不同技术栈的 &lt;code>Dockerfile&lt;/code> 不一样，但对 Traefik 来说只需要满足两点：&lt;/p>
&lt;ol>
&lt;li>容器内部有一个 HTTP 服务端口，比如 &lt;code>80&lt;/code>、&lt;code>3000&lt;/code>、&lt;code>8080&lt;/code>。&lt;/li>
&lt;li>不要直接把业务容器的端口暴露到公网，公网入口交给 Traefik。&lt;/li>
&lt;/ol>
&lt;p>如果是静态站点，可以用 Nginx 承载构建产物：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-dockerfile" data-lang="dockerfile">&lt;span class="line">&lt;span class="cl">&lt;span class="k">FROM&lt;/span>&lt;span class="s"> nginx:alpine&lt;/span>&lt;span class="err">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="err">&lt;/span>&lt;span class="k">COPY&lt;/span> public /usr/share/nginx/html&lt;span class="err">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>如果是 Node、Python、Go、Java 后端，则按对应技术栈构建镜像，只要最后服务监听一个明确端口即可。&lt;/p>
&lt;h3 id="二服务器上每个项目一个目录">二、服务器上每个项目一个目录
&lt;/h3>&lt;p>建议每个应用单独放到 &lt;code>/opt/apps/&amp;lt;app-name&amp;gt;&lt;/code>：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">sudo mkdir -p /opt/apps/my-app
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nb">cd&lt;/span> /opt/apps/my-app
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h4 id="组织方案选择">组织方案选择
&lt;/h4>&lt;p>每个项目目录里至少需要 &lt;code>.env&lt;/code>（存放部署变量）和 &lt;code>docker-compose.yml&lt;/code>（定义容器运行方式）。这两者的关系有几种组织方式：&lt;/p>
&lt;p>&lt;strong>方案 A：compose 文件直接放在项目目录（默认）&lt;/strong>&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-text" data-lang="text">&lt;span class="line">&lt;span class="cl">/opt/apps/my-app/
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">├── .env
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">└── docker-compose.yml
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>简单直观，适合 compose 文件不常变更的项目。&lt;/p>
&lt;p>&lt;strong>方案 B：compose 文件集中管理&lt;/strong>&lt;/p>
&lt;p>如果多个项目的 compose 模板相似，想统一维护，可以把 compose 文件放到共享目录：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-text" data-lang="text">&lt;span class="line">&lt;span class="cl">/opt/compose-templates/
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">├── traefik-app.yml
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">└── static-site.yml
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">/opt/apps/my-app/
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">└── .env
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>部署时用 &lt;code>-f&lt;/code> 指定模板路径：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">docker compose -f /opt/compose-templates/traefik-app.yml --env-file /opt/apps/my-app/.env up -d
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>CD 流水线里也这样写，每次更新 compose 模板时只需要改 &lt;code>/opt/compose-templates/&lt;/code> 下的文件。&lt;/p>
&lt;p>&lt;strong>方案 C：用脚本封装&lt;/strong>&lt;/p>
&lt;p>在项目目录下放一个启动脚本，把模板路径写死在脚本里：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">&lt;span class="cp">#!/bin/bash
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="cp">&lt;/span>&lt;span class="c1"># /opt/apps/my-app/deploy.sh&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nb">cd&lt;/span> /opt/compose-templates
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">docker compose -f traefik-app.yml --env-file /opt/apps/my-app/.env &lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="nv">$@&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>这样部署命令变成：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">/opt/apps/my-app/deploy.sh up -d
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">./deploy.sh logs -f
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>脚本本身也可以加入检查、重试、通知等逻辑，适合有统一运维规范的项目。&lt;/p>
&lt;p>&lt;strong>方案 D：用 git 仓库管理 compose 文件&lt;/strong>&lt;/p>
&lt;p>如果 CD 流水线已经配好了 SSH 密钥，服务器可以直接用 git 操作。只需要 clone 一次，之后 CD 时 &lt;code>git pull&lt;/code> 拉取最新配置：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">git clone git@github.com:your-org/server-configs.git /opt/configs
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>目录结构：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-text" data-lang="text">&lt;span class="line">&lt;span class="cl">/opt/configs/
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">├── compose.yml # git 管理
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">└── .env.example # 提交 git，真实值在服务器上配置
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">/opt/apps/my-app/
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">└── .env # 不提交 git，只存在服务器
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>CD 部署时：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">&lt;span class="nb">cd&lt;/span> /opt/configs &lt;span class="o">&amp;amp;&amp;amp;&lt;/span> git pull &lt;span class="o">&amp;amp;&amp;amp;&lt;/span> &lt;span class="nb">cd&lt;/span> /opt/apps/my-app &lt;span class="o">&amp;amp;&amp;amp;&lt;/span> docker compose -f /opt/configs/compose.yml --env-file .env up -d
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;code>.env&lt;/code> 里的密钥留在服务器本地，不进 git 仓库。compose 文件由 git 管理，可以 review 和回滚。&lt;/p>
&lt;p>&lt;strong>怎么选&lt;/strong>：如果 compose 模板基本固定、很少改，用默认的方案 A 就行。如果想统一管理模板、减少每个项目重复的 compose 内容，选方案 B 或 C。如果需要多人协作、版本控制和 PR review，选方案 D。&lt;/p>
&lt;p>下面以方案 A 为例继续说明。如果选了其他方案，记得相应调整后面的路径和命令。&lt;/p>
&lt;p>&lt;code>.env&lt;/code> 只放部署层变量：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">&lt;span class="nv">APP_IMAGE&lt;/span>&lt;span class="o">=&lt;/span>ghcr.io/your-org/my-app:initial
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nv">APP_HOST&lt;/span>&lt;span class="o">=&lt;/span>app.example.com
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nv">APP_PORT&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="m">3000&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;code>docker-compose.yml&lt;/code> 写成通用模板：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="cl">&lt;span class="nt">services&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">app&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">image&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">${APP_IMAGE}&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">restart&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">unless-stopped&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">networks&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="l">traefik&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">expose&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="s2">&amp;#34;${APP_PORT}&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">labels&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="s2">&amp;#34;traefik.enable=true&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="s2">&amp;#34;traefik.http.routers.my-app.rule=Host(`${APP_HOST}`)&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="s2">&amp;#34;traefik.http.routers.my-app.entrypoints=websecure&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="s2">&amp;#34;traefik.http.routers.my-app.tls=true&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="s2">&amp;#34;traefik.http.routers.my-app.tls.certresolver=letsencrypt&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="s2">&amp;#34;traefik.http.services.my-app.loadbalancer.server.port=${APP_PORT}&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">networks&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">traefik&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">external&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="kc">true&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>这里没有写 &lt;code>ports&lt;/code>。业务容器不需要直接占用宿主机端口，Traefik 会通过 &lt;code>traefik&lt;/code> 网络访问容器内部端口。&lt;/p>
&lt;p>如果你的 Traefik 没有启用 Let&amp;rsquo;s Encrypt，可以先用 HTTP：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="cl">&lt;span class="nt">labels&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="s2">&amp;#34;traefik.enable=true&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="s2">&amp;#34;traefik.http.routers.my-app.rule=Host(`${APP_HOST}`)&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="s2">&amp;#34;traefik.http.routers.my-app.entrypoints=web&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="s2">&amp;#34;traefik.http.services.my-app.loadbalancer.server.port=${APP_PORT}&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>真实项目的数据库密码、API key 等运行时环境变量，建议放在另一个只存在服务器上的文件，例如 &lt;code>app.env&lt;/code>，然后在 compose 里引用：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="cl">&lt;span class="nt">env_file&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="l">app.env&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>CD 流水线只更新 &lt;code>.env&lt;/code> 里的 &lt;code>APP_IMAGE&lt;/code>，不要覆盖服务器上的业务密钥文件。&lt;/p>
&lt;h3 id="三准备部署用户和-ssh-key">三、准备部署用户和 SSH key
&lt;/h3>&lt;p>生产环境建议创建专门的部署用户，不要长期用 &lt;code>root&lt;/code> 跑 CD：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">sudo adduser --disabled-password --gecos &lt;span class="s2">&amp;#34;&amp;#34;&lt;/span> deploy
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">sudo usermod -aG docker deploy
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>加入 &lt;code>docker&lt;/code> 组后，需要重新登录这个用户，组权限才会生效。&lt;/p>
&lt;p>生成一把专门给 CD 用的 SSH key：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">ssh-keygen -t ed25519 -C &lt;span class="s2">&amp;#34;cd-my-app&amp;#34;&lt;/span> -f ./cd-my-app -N &lt;span class="s2">&amp;#34;&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>公钥放到服务器：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">sudo install -d -m &lt;span class="m">700&lt;/span> -o deploy -g deploy /home/deploy/.ssh
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">cat ./cd-my-app.pub &lt;span class="p">|&lt;/span> sudo tee -a /home/deploy/.ssh/authorized_keys
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">sudo chmod &lt;span class="m">600&lt;/span> /home/deploy/.ssh/authorized_keys
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">sudo chown -R deploy:deploy /home/deploy/.ssh
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>私钥内容放到 CI/CD 平台的 Secret 里。以 GitHub Actions 为例，进入：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-text" data-lang="text">&lt;span class="line">&lt;span class="cl">Repository -&amp;gt; Settings -&amp;gt; Secrets and variables -&amp;gt; Actions
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>添加：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-text" data-lang="text">&lt;span class="line">&lt;span class="cl">SERVER_HOST 服务器 IP 或域名
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">SERVER_USER deploy
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">SERVER_SSH_KEY cd-my-app 私钥完整内容
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>注意：私钥不要提交到 Git 仓库。公钥留在服务器的 &lt;code>authorized_keys&lt;/code>，私钥只放 CI/CD Secret。&lt;/p>
&lt;h3 id="四准备镜像仓库">四、准备镜像仓库
&lt;/h3>&lt;p>镜像仓库可以用 GitHub Container Registry、Docker Hub、Harbor、阿里云/腾讯云镜像仓库等。通用原则是：&lt;/p>
&lt;ul>
&lt;li>CI/CD 需要有 &lt;code>push&lt;/code> 权限，用来推送新镜像。&lt;/li>
&lt;li>服务器需要有 &lt;code>pull&lt;/code> 权限，用来拉取新镜像。&lt;/li>
&lt;li>CD 部署时尽量使用不可变 tag，例如 Git commit SHA。&lt;/li>
&lt;li>&lt;code>latest&lt;/code> 可以保留给人工查看，但不要只依赖 &lt;code>latest&lt;/code> 做生产发布。&lt;/li>
&lt;/ul>
&lt;p>如果使用私有镜像，先在服务器上用部署用户登录一次：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">sudo -iu deploy
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nb">echo&lt;/span> &lt;span class="s2">&amp;#34;你的镜像仓库 token&amp;#34;&lt;/span> &lt;span class="p">|&lt;/span> docker login ghcr.io -u your-user --password-stdin
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>登录信息会保存在部署用户自己的 Docker 配置里。之后 CD 执行 &lt;code>docker compose pull&lt;/code> 时，就能拉取私有镜像。&lt;/p>
&lt;h3 id="五github-actions-通用-cd-模板">五、GitHub Actions 通用 CD 模板
&lt;/h3>&lt;p>下面这个模板做三件事：&lt;/p>
&lt;ol>
&lt;li>构建镜像。&lt;/li>
&lt;li>推送 &lt;code>latest&lt;/code> 和当前 commit SHA 两个 tag。&lt;/li>
&lt;li>SSH 到服务器，把 &lt;code>.env&lt;/code> 里的 &lt;code>APP_IMAGE&lt;/code> 改成当前 commit SHA 对应的镜像，然后重启应用。&lt;/li>
&lt;/ol>
&lt;p>保存为：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-text" data-lang="text">&lt;span class="line">&lt;span class="cl">.github/workflows/cd.yml
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="cl">&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">CD&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">on&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">push&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">branches&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="l">main&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">workflow_dispatch&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">permissions&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">contents&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">read&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">packages&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">write&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">env&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">REGISTRY_HOST&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">ghcr.io&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">IMAGE_NAME&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">ghcr.io/your-org/my-app&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">APP_DIR&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">/opt/apps/my-app&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">jobs&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">deploy&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">runs-on&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">ubuntu-latest&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">steps&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">Checkout&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">uses&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">actions/checkout@v6&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">Set up Docker Buildx&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">uses&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">docker/setup-buildx-action@v4&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">Login to registry&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">uses&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">docker/login-action@v4&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">with&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">registry&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">${{ env.REGISTRY_HOST }}&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">username&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">${{ secrets.REGISTRY_USERNAME }}&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">password&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">${{ secrets.REGISTRY_TOKEN }}&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">Build and push image&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">uses&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">docker/build-push-action@v7&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">with&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">context&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">.&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">push&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="kc">true&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">tags&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">|&lt;/span>&lt;span class="sd">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> ${{ env.IMAGE_NAME }}:${{ github.sha }}
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> ${{ env.IMAGE_NAME }}:latest&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">Deploy on server&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">env&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">SERVER_HOST&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">${{ secrets.SERVER_HOST }}&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">SERVER_USER&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">${{ secrets.SERVER_USER }}&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">SERVER_SSH_KEY&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">${{ secrets.SERVER_SSH_KEY }}&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">IMAGE&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">${{ env.IMAGE_NAME }}:${{ github.sha }}&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">run&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">|&lt;/span>&lt;span class="sd">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> mkdir -p ~/.ssh
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> printf &amp;#39;%s\n&amp;#39; &amp;#34;$SERVER_SSH_KEY&amp;#34; &amp;gt; ~/.ssh/deploy_key
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> chmod 600 ~/.ssh/deploy_key
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> ssh-keyscan -H &amp;#34;$SERVER_HOST&amp;#34; &amp;gt;&amp;gt; ~/.ssh/known_hosts
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> ssh -i ~/.ssh/deploy_key &amp;#34;$SERVER_USER@$SERVER_HOST&amp;#34; \
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> &amp;#34;cd &amp;#39;${APP_DIR}&amp;#39; &amp;amp;&amp;amp; \
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> sed -i &amp;#39;s#^APP_IMAGE=.*#APP_IMAGE=${IMAGE}#&amp;#39; .env &amp;amp;&amp;amp; \
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> docker compose pull &amp;amp;&amp;amp; \
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> docker compose up -d --remove-orphans &amp;amp;&amp;amp; \
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> docker image prune -f&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>对应需要在 GitHub Secrets 里添加：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-text" data-lang="text">&lt;span class="line">&lt;span class="cl">REGISTRY_USERNAME 镜像仓库用户名
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">REGISTRY_TOKEN 镜像仓库 token
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">SERVER_HOST 服务器 IP 或域名
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">SERVER_USER SSH 部署用户
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">SERVER_SSH_KEY SSH 私钥
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>如果使用 GitHub Container Registry，并且镜像属于当前仓库，构建推送阶段也可以用 &lt;code>${{ github.actor }}&lt;/code> 和 &lt;code>${{ secrets.GITHUB_TOKEN }}&lt;/code>。但服务器拉取私有镜像时，仍然需要能 &lt;code>pull&lt;/code> 该镜像的凭据，最清晰的做法是给服务器配置一份只读或低权限的 registry token。&lt;/p>
&lt;h3 id="六发布后检查和回滚">六、发布后检查和回滚
&lt;/h3>&lt;p>部署完成后，至少检查三处：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">&lt;span class="nb">cd&lt;/span> /opt/apps/my-app
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">docker compose ps
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">docker compose logs -f
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">curl -I https://app.example.com
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>如果应用提供健康检查接口，可以在 workflow 里加一步：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">curl -fsS https://app.example.com/health
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>回滚的本质是把 &lt;code>APP_IMAGE&lt;/code> 改回旧镜像 tag：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">&lt;span class="nb">cd&lt;/span> /opt/apps/my-app
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">sed -i &lt;span class="s1">&amp;#39;s#^APP_IMAGE=.*#APP_IMAGE=ghcr.io/your-org/my-app:old-commit-sha#&amp;#39;&lt;/span> .env
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">docker compose pull
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">docker compose up -d --remove-orphans
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>所以每次发布都用 commit SHA 做镜像 tag 很重要。它让你能明确知道当前服务器运行的是哪一次代码，也能快速回到上一个可用版本。&lt;/p>
&lt;h3 id="七多项目部署约定">七、多项目部署约定
&lt;/h3>&lt;p>单台服务器跑多个项目时，建议保持这些约定：&lt;/p>
&lt;ul>
&lt;li>每个项目一个目录：&lt;code>/opt/apps/&amp;lt;app-name&amp;gt;&lt;/code>。&lt;/li>
&lt;li>每个项目一个 compose：只管理自己的业务容器。&lt;/li>
&lt;li>所有需要公网 HTTP/HTTPS 的服务都加入同一个 &lt;code>traefik&lt;/code> 网络。&lt;/li>
&lt;li>每个项目使用不同的 router/service 名称，例如 &lt;code>blog&lt;/code>、&lt;code>api&lt;/code>、&lt;code>admin&lt;/code>。&lt;/li>
&lt;li>每个项目只在 labels 里声明自己的域名和内部端口。&lt;/li>
&lt;li>CD 流水线只更新项目目录，不要改 &lt;code>/opt/traefik&lt;/code>。&lt;/li>
&lt;/ul>
&lt;p>这样 Traefik 是稳定的公共入口，业务项目各自独立发布。新增项目时，只需要新建项目目录、写自己的 compose、配置 DNS、接入一条 CD workflow。&lt;/p>
&lt;h2 id="完整脚本">完整脚本
&lt;/h2>&lt;p>完整脚本不再内嵌在正文里，避免博客内容和实际可执行文件不同步。脚本文件放在这篇文章同目录：&lt;a class="link" href="install-docker-traefik.sh" >install-docker-traefik.sh&lt;/a>。&lt;/p>
&lt;h2 id="运行后检查">运行后检查
&lt;/h2>&lt;p>查看 Traefik 容器：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">sudo docker compose --env-file /opt/traefik/.env -f /opt/traefik/docker-compose.yml ps
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>查看日志：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">sudo docker compose --env-file /opt/traefik/.env -f /opt/traefik/docker-compose.yml logs -f
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>测试服务可以这样启动（同样，&lt;code>ACME_EMAIL&lt;/code> 必须是真实邮箱）：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">&lt;span class="nv">ACME_EMAIL&lt;/span>&lt;span class="o">=&lt;/span>you@your-domain.com &lt;span class="nv">INSTALL_SAMPLE&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="nb">true&lt;/span> &lt;span class="nv">WHOAMI_HOST&lt;/span>&lt;span class="o">=&lt;/span>whoami.example.com ./install-docker-traefik.sh
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>如果 &lt;code>whoami.example.com&lt;/code> 能访问，说明 DNS、80/443、防火墙、Traefik 路由和证书申请基本都通了。&lt;/p>
&lt;h2 id="如果浏览器一直显示此网站的证书无效">如果浏览器一直显示&amp;quot;此网站的证书无效&amp;quot;
&lt;/h2>&lt;p>按本博客早期示例用过 &lt;code>ACME_EMAIL=admin@example.com&lt;/code> 之类占位邮箱的用户会撞到这个问题。Let&amp;rsquo;s Encrypt 静默拒绝占位邮箱，acme.json 一直不生成，Traefik 只能回落到自签证书。&lt;/p>
&lt;p>修法是直接把 &lt;code>traefik.yml&lt;/code> 里的 &lt;code>email:&lt;/code> 改成真实邮箱，不需要重装：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># 1) 把真实邮箱替换进 traefik.yml&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">sudo sed -i &lt;span class="s1">&amp;#39;s#admin@example.com#you@your-domain.com#&amp;#39;&lt;/span> /opt/traefik/traefik.yml
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># （如果之前是别的占位值，把 sed 第一个参数里的字面量对应替换）&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># 2) 清掉任何残留的 acme.json，让 Traefik 重新申请&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">sudo rm -f /opt/traefik/letsencrypt/acme.json
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">sudo touch /opt/traefik/letsencrypt/acme.json
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">sudo chmod &lt;span class="m">600&lt;/span> /opt/traefik/letsencrypt/acme.json
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># 3) 重启 Traefik，让它从第一个 HTTPS 请求开始走真正的 LE 申请&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nb">cd&lt;/span> /opt/traefik &lt;span class="o">&amp;amp;&amp;amp;&lt;/span> sudo docker compose restart traefik
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>修完后用下面命令验证证书已经换成真证书：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">&lt;span class="nb">echo&lt;/span> &lt;span class="p">|&lt;/span> openssl s_client -connect your-domain.com:443 -servername your-domain.com 2&amp;gt;/dev/null &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> &lt;span class="p">|&lt;/span> openssl x509 -noout -issuer -subject -dates
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># 应该看到 issuer 含 &amp;#34;Let&amp;#39;s Encrypt&amp;#34;，subject 是你的域名&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h2 id="注意事项">注意事项
&lt;/h2>&lt;ol>
&lt;li>脚本会按 Docker 官方安装方式添加 apt 仓库，并安装 &lt;code>docker-ce&lt;/code>、&lt;code>docker-ce-cli&lt;/code>、&lt;code>containerd.io&lt;/code>、&lt;code>docker-buildx-plugin&lt;/code>、&lt;code>docker-compose-plugin&lt;/code>。&lt;/li>
&lt;li>脚本会移除可能冲突的旧包，例如 &lt;code>docker.io&lt;/code>、旧版 &lt;code>docker-compose&lt;/code>、&lt;code>podman-docker&lt;/code>、&lt;code>containerd&lt;/code>、&lt;code>runc&lt;/code>。&lt;/li>
&lt;li>Traefik dashboard 使用 &lt;code>api.insecure: true&lt;/code>，但端口只绑定到 &lt;code>127.0.0.1:8080&lt;/code>，不要改成公网监听，除非你额外加认证。&lt;/li>
&lt;li>Traefik 通过只读方式挂载 &lt;code>/var/run/docker.sock&lt;/code>。这依然是高权限入口，生产环境要限制谁能创建带 Traefik labels 的容器。&lt;/li>
&lt;li>Let&amp;rsquo;s Encrypt HTTP challenge 需要 &lt;code>80&lt;/code> 端口能从公网访问，否则证书申请会失败。&lt;/li>
&lt;li>&lt;code>ACME_EMAIL&lt;/code> 必须是真实邮箱。脚本会拒绝 &lt;code>admin@example.com&lt;/code>、&lt;code>you@example.com&lt;/code>、&lt;code>*@example.com&lt;/code> 等占位值；不要被早期示例误导。&lt;/li>
&lt;/ol>
&lt;h2 id="参考">参考
&lt;/h2>&lt;ul>
&lt;li>&lt;a class="link" href="https://docs.docker.com/engine/install/ubuntu/" target="_blank" rel="noopener"
>Docker Engine Ubuntu 安装文档&lt;/a>&lt;/li>
&lt;li>&lt;a class="link" href="https://docs.docker.com/engine/install/debian/" target="_blank" rel="noopener"
>Docker Engine Debian 安装文档&lt;/a>&lt;/li>
&lt;li>&lt;a class="link" href="https://doc.traefik.io/traefik/providers/docker/" target="_blank" rel="noopener"
>Traefik Docker provider 文档&lt;/a>&lt;/li>
&lt;li>&lt;a class="link" href="https://hub.docker.com/_/traefik" target="_blank" rel="noopener"
>Traefik Docker 官方镜像&lt;/a>&lt;/li>
&lt;li>&lt;a class="link" href="https://docs.github.com/en/actions/tutorials/publish-packages/publish-docker-images" target="_blank" rel="noopener"
>GitHub Actions 发布 Docker 镜像&lt;/a>&lt;/li>
&lt;li>&lt;a class="link" href="https://docs.github.com/en/actions/concepts/security/secrets" target="_blank" rel="noopener"
>GitHub Actions Secrets&lt;/a>&lt;/li>
&lt;li>&lt;a class="link" href="https://docs.docker.com/guides/gha/" target="_blank" rel="noopener"
>Docker 的 GitHub Actions 指南&lt;/a>&lt;/li>
&lt;/ul></description></item><item><title>Woodpecker CI 使用教程</title><link>https://www.zata.cc/p/woodpecker-ci-%E4%BD%BF%E7%94%A8%E6%95%99%E7%A8%8B/</link><pubDate>Sun, 07 Jun 2026 10:00:00 +0800</pubDate><guid>https://www.zata.cc/p/woodpecker-ci-%E4%BD%BF%E7%94%A8%E6%95%99%E7%A8%8B/</guid><description>&lt;img src="https://www.zata.cc/p/woodpecker-ci-%E4%BD%BF%E7%94%A8%E6%95%99%E7%A8%8B/images/index/index.png" alt="Featured image of post Woodpecker CI 使用教程" />&lt;p>Woodpecker CI 是一个开源、轻量、容器优先的 CI/CD 工具。它的定位很清晰：代码仓库发生 push、pull request、tag、手动触发等事件后，Woodpecker 读取仓库里的 YAML 配置，然后把每个步骤放进容器里执行。&lt;/p>
&lt;p>如果你已经熟悉 GitHub Actions，可以把 Woodpecker 理解成一个更偏自托管、更轻量、更贴近 Drone CI 风格的流水线系统。它不绑定某一家代码托管平台，可以接 GitHub、GitLab、Gitea、Forgejo、Bitbucket 等 forge，适合团队希望把 CI/CD 控制权放在自己服务器上的场景。&lt;/p>
&lt;hr>
&lt;h2 id="一如何安装-woodpecker-ci">一、如何安装 Woodpecker CI
&lt;/h2>&lt;p>第一次了解 Woodpecker CI，建议先用 Docker Compose 跑一个最小可用版本。先跑起来，再理解 pipeline、workflow、step 这些概念会更直观。&lt;/p>
&lt;p>下面以接入 GitHub 为例。如果你使用 Gitea、Forgejo 或 GitLab，整体步骤类似，只是 forge 相关环境变量不同。&lt;/p>
&lt;p>这篇教程优先写 Docker Compose，是因为它最适合单台服务器和第一次试用。如果你已经有 Kubernetes 集群，应该优先看本节后面的 Helm 安装方式，Helm Chart 也是官方提供的部署方式。&lt;/p>
&lt;h3 id="1-安装前准备">1. 安装前准备
&lt;/h3>&lt;p>你需要准备：&lt;/p>
&lt;ul>
&lt;li>一台已经安装 Docker 和 Docker Compose 的服务器。&lt;/li>
&lt;li>一个能访问 Woodpecker Web UI 的地址，例如 &lt;code>https://ci.example.com&lt;/code>。&lt;/li>
&lt;li>一个代码平台账号，例如 GitHub、GitLab、Gitea、Forgejo。&lt;/li>
&lt;li>目标仓库的管理员权限，因为 Woodpecker 需要创建 webhook。&lt;/li>
&lt;li>一个 Server 和 Agent 共享的随机密钥。&lt;/li>
&lt;/ul>
&lt;p>生成共享密钥：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">openssl rand -hex &lt;span class="m">32&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h3 id="2-创建-github-oauth-app">2. 创建 GitHub OAuth App
&lt;/h3>&lt;p>如果接 GitHub，需要在 GitHub 创建 OAuth App，不是 GitHub App。&lt;/p>
&lt;p>路径：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-text" data-lang="text">&lt;span class="line">&lt;span class="cl">GitHub -&amp;gt; Settings -&amp;gt; Developer settings -&amp;gt; OAuth Apps -&amp;gt; New OAuth App
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>关键配置：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-text" data-lang="text">&lt;span class="line">&lt;span class="cl">Homepage URL: https://ci.example.com
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">Authorization callback URL: https://ci.example.com/authorize
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>创建完成后，记录：&lt;/p>
&lt;ul>
&lt;li>&lt;code>Client ID&lt;/code>&lt;/li>
&lt;li>&lt;code>Client Secret&lt;/code>&lt;/li>
&lt;/ul>
&lt;p>它们会分别填到 &lt;code>WOODPECKER_GITHUB_CLIENT&lt;/code> 和 &lt;code>WOODPECKER_GITHUB_SECRET&lt;/code>。&lt;/p>
&lt;h3 id="3-编写-docker-composeyaml">3. 编写 docker-compose.yaml
&lt;/h3>&lt;p>创建一个目录，例如：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">mkdir -p /opt/woodpecker
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nb">cd&lt;/span> /opt/woodpecker
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>写入 &lt;code>docker-compose.yaml&lt;/code>：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="cl">&lt;span class="nt">services&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">woodpecker-server&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">image&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">woodpeckerci/woodpecker-server:v3&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">restart&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">always&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">ports&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="s2">&amp;#34;8000:8000&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">volumes&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="l">woodpecker-server-data:/var/lib/woodpecker/&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">environment&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="l">WOODPECKER_OPEN=true&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="l">WOODPECKER_HOST=${WOODPECKER_HOST}&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="l">WOODPECKER_GITHUB=true&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="l">WOODPECKER_GITHUB_CLIENT=${WOODPECKER_GITHUB_CLIENT}&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="l">WOODPECKER_GITHUB_SECRET=${WOODPECKER_GITHUB_SECRET}&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="l">WOODPECKER_AGENT_SECRET=${WOODPECKER_AGENT_SECRET}&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">woodpecker-agent&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">image&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">woodpeckerci/woodpecker-agent:v3&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">command&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">agent&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">restart&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">always&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">depends_on&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="l">woodpecker-server&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">volumes&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="l">woodpecker-agent-config:/etc/woodpecker&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="l">/var/run/docker.sock:/var/run/docker.sock&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">environment&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="l">WOODPECKER_SERVER=woodpecker-server:9000&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="l">WOODPECKER_AGENT_SECRET=${WOODPECKER_AGENT_SECRET}&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">volumes&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">woodpecker-server-data&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">woodpecker-agent-config&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h3 id="4-编写-env">4. 编写 .env
&lt;/h3>&lt;p>同目录创建 &lt;code>.env&lt;/code>：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">&lt;span class="nv">WOODPECKER_HOST&lt;/span>&lt;span class="o">=&lt;/span>https://ci.example.com
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nv">WOODPECKER_GITHUB_CLIENT&lt;/span>&lt;span class="o">=&lt;/span>你的 GitHub OAuth App Client ID
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nv">WOODPECKER_GITHUB_SECRET&lt;/span>&lt;span class="o">=&lt;/span>你的 GitHub OAuth App Client Secret
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nv">WOODPECKER_AGENT_SECRET&lt;/span>&lt;span class="o">=&lt;/span>用 openssl rand -hex &lt;span class="m">32&lt;/span> 生成的随机字符串
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>这里最容易写错的是 &lt;code>WOODPECKER_HOST&lt;/code>。它必须是浏览器和代码平台 webhook 都能访问到的 Woodpecker 外部地址，不要写成容器内地址，也不要在末尾加 &lt;code>/&lt;/code>。&lt;/p>
&lt;h3 id="5-启动服务">5. 启动服务
&lt;/h3>&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">docker compose up -d
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">docker compose ps
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>查看日志：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">docker compose logs -f woodpecker-server
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">docker compose logs -f woodpecker-agent
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>如果一切正常，打开：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-text" data-lang="text">&lt;span class="line">&lt;span class="cl">https://ci.example.com
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>使用 GitHub 登录，然后在 Woodpecker UI 里启用你的仓库。&lt;/p>
&lt;h3 id="6-让仓库真正跑起来">6. 让仓库真正跑起来
&lt;/h3>&lt;p>Woodpecker 安装完成后，还需要在仓库里添加流水线配置。最小示例是：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-text" data-lang="text">&lt;span class="line">&lt;span class="cl">.woodpecker/build.yaml
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="cl">&lt;span class="nt">when&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">event&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">push&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">branch&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">main&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">steps&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">hello&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">image&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">alpine&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">commands&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="l">echo &amp;#34;hello woodpecker&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>提交并 push 到 &lt;code>main&lt;/code> 后，Woodpecker 应该会收到 webhook 并开始执行 pipeline。&lt;/p>
&lt;h3 id="7-已有-dokploy--traefik不需要-nginx">7. 已有 Dokploy / Traefik：不需要 Nginx
&lt;/h3>&lt;p>如果你已经习惯用 Dokploy 配合 Traefik 快速部署应用，Woodpecker 并不会变难。Woodpecker 不自带 Traefik，但它可以像普通 Web 应用一样接入现有 Traefik 网关。&lt;/p>
&lt;p>如果服务器还没有安装 Docker 和 Traefik，可以先按这篇文章准备公共入口层：&lt;a class="link" href="https://www.zata.cc/p/docker-traefik-install-script/" >Docker 和 Traefik 一键安装脚本&lt;/a>。&lt;/p>
&lt;p>核心思路是：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-text" data-lang="text">&lt;span class="line">&lt;span class="cl">https://ci.example.com
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> |
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> v
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">Traefik / Dokploy 网关
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> |
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> v
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">woodpecker-server:8000
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Agent 不需要从公网访问 &lt;code>https://ci.example.com&lt;/code>。如果 &lt;code>woodpecker-server&lt;/code> 和 &lt;code>woodpecker-agent&lt;/code> 在同一个 Docker Compose 项目里，Agent 直接用内部地址连接即可：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-text" data-lang="text">&lt;span class="line">&lt;span class="cl">WOODPECKER_SERVER=woodpecker-server:9000
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>也就是说，Traefik 只负责 Web UI、OAuth 回调、webhook 入口；Agent 和 Server 的 gRPC 通信可以留在 Docker 内部网络里。&lt;/p>
&lt;p>如果由 Dokploy UI 管理域名，通常只需要：&lt;/p>
&lt;ol>
&lt;li>用 Dokploy 创建一个 Docker Compose 应用。&lt;/li>
&lt;li>填入 Woodpecker 的 compose 配置。&lt;/li>
&lt;li>给 &lt;code>woodpecker-server&lt;/code> 配域名，例如 &lt;code>ci.example.com&lt;/code>。&lt;/li>
&lt;li>让 Dokploy 把外部 HTTPS 转发到容器内部端口 &lt;code>8000&lt;/code>。&lt;/li>
&lt;li>&lt;code>.env&lt;/code> 中保持 &lt;code>WOODPECKER_HOST=https://ci.example.com&lt;/code>。&lt;/li>
&lt;/ol>
&lt;p>如果你直接写 Traefik labels，可以把 &lt;code>woodpecker-server&lt;/code> 改成类似下面这样。网络名、证书 resolver、entrypoint 要替换成你现有 Traefik 的配置：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="cl">&lt;span class="nt">services&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">woodpecker-server&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">image&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">woodpeckerci/woodpecker-server:v3&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">restart&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">always&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">expose&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="s2">&amp;#34;8000&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">volumes&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="l">woodpecker-server-data:/var/lib/woodpecker/&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">environment&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="l">WOODPECKER_OPEN=true&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="l">WOODPECKER_HOST=${WOODPECKER_HOST}&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="l">WOODPECKER_GITHUB=true&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="l">WOODPECKER_GITHUB_CLIENT=${WOODPECKER_GITHUB_CLIENT}&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="l">WOODPECKER_GITHUB_SECRET=${WOODPECKER_GITHUB_SECRET}&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="l">WOODPECKER_AGENT_SECRET=${WOODPECKER_AGENT_SECRET}&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">labels&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="s2">&amp;#34;traefik.enable=true&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="s2">&amp;#34;traefik.http.routers.woodpecker.rule=Host(`ci.example.com`)&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="s2">&amp;#34;traefik.http.routers.woodpecker.entrypoints=websecure&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="s2">&amp;#34;traefik.http.routers.woodpecker.tls.certresolver=letsencrypt&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="s2">&amp;#34;traefik.http.services.woodpecker.loadbalancer.server.port=8000&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">networks&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="l">traefik-public&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="l">default&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">woodpecker-agent&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">image&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">woodpeckerci/woodpecker-agent:v3&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">command&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">agent&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">restart&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">always&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">depends_on&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="l">woodpecker-server&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">volumes&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="l">woodpecker-agent-config:/etc/woodpecker&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="l">/var/run/docker.sock:/var/run/docker.sock&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">environment&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="l">WOODPECKER_SERVER=woodpecker-server:9000&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="l">WOODPECKER_AGENT_SECRET=${WOODPECKER_AGENT_SECRET}&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">volumes&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">woodpecker-server-data&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">woodpecker-agent-config&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">networks&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">traefik-public&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">external&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="kc">true&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>注意这里没有暴露：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="cl">&lt;span class="nt">ports&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="s2">&amp;#34;8000:8000&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>因为公网入口已经由 Traefik 接管。只有在没有 Traefik、Nginx、Caddy、Dokploy 这类网关时，才需要把端口直接映射出来。&lt;/p>
&lt;h3 id="8-kubernetes-环境用-helm-安装">8. Kubernetes 环境：用 Helm 安装
&lt;/h3>&lt;p>如果你的基础设施已经是 Kubernetes，不需要用上面的 Docker Compose。Woodpecker 官方提供了 Helm Chart，可以直接安装：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">helm install woodpecker oci://ghcr.io/woodpecker-ci/helm/woodpecker --version &amp;lt;VERSION&amp;gt;
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>实际使用时建议带上 namespace：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">helm install woodpecker oci://ghcr.io/woodpecker-ci/helm/woodpecker &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --version &amp;lt;VERSION&amp;gt; &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --namespace woodpecker &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --create-namespace
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>这里的 &lt;code>&amp;lt;VERSION&amp;gt;&lt;/code> 是 Helm Chart 版本，不一定等于 Woodpecker Server 的应用版本。安装前可以到官方 Helm 仓库或 release 页面确认当前 chart 版本。&lt;/p>
&lt;p>生产环境通常会准备一个 &lt;code>values.yaml&lt;/code>，把域名、OAuth、持久化、Ingress 等配置写进去。例如：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="cl">&lt;span class="nt">server&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">env&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">WOODPECKER_OPEN&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;true&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">WOODPECKER_HOST&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;https://ci.example.com&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">WOODPECKER_GITHUB&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;true&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">WOODPECKER_GITHUB_CLIENT&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;你的 GitHub OAuth App Client ID&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">WOODPECKER_GITHUB_SECRET&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;你的 GitHub OAuth App Client Secret&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">WOODPECKER_AGENT_SECRET&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;用 openssl rand -hex 32 生成的随机字符串&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">ingress&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">enabled&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="kc">true&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">ingressClassName&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">nginx&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">hosts&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">host&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">ci.example.com&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">paths&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">path&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">/&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">pathType&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">Prefix&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">agent&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">enabled&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="kc">true&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>然后执行：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">helm upgrade --install woodpecker oci://ghcr.io/woodpecker-ci/helm/woodpecker &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --version &amp;lt;VERSION&amp;gt; &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --namespace woodpecker &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --create-namespace &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> -f values.yaml
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>选择 Docker Compose 还是 Helm，可以这样判断：&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>场景&lt;/th>
&lt;th>推荐方式&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>单台云服务器、先学习、先跑通&lt;/td>
&lt;td>Docker Compose&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>已经用 Dokploy / Traefik 管理应用域名&lt;/td>
&lt;td>Docker Compose + 现有 Traefik&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>已经有 Kubernetes 集群&lt;/td>
&lt;td>Helm&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>需要 Ingress、StorageClass、PodMonitor、RBAC 等 K8s 能力&lt;/td>
&lt;td>Helm&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>不熟悉 Kubernetes，只想给个人项目跑 CI&lt;/td>
&lt;td>Docker Compose&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>Helm 不是更高级就一定更适合。它适合已经有 K8s 运维体系的团队；如果只是个人服务器或小团队单机部署，Docker Compose 反而更容易维护。&lt;/p>
&lt;h3 id="9-中国服务器注意事项">9. 中国服务器注意事项
&lt;/h3>&lt;p>如果服务器在中国大陆，安装本身没有特殊问题，但镜像和依赖源要提前规划：&lt;/p>
&lt;ul>
&lt;li>把 &lt;code>woodpeckerci/woodpecker-server:v3&lt;/code>、&lt;code>woodpeckerci/woodpecker-agent:v3&lt;/code> 同步到国内镜像仓库。&lt;/li>
&lt;li>如果使用 Helm，&lt;code>oci://ghcr.io/woodpecker-ci/helm/woodpecker&lt;/code> 也可能受 GHCR 网络影响，必要时提前准备代理或内部镜像/制品仓库。&lt;/li>
&lt;li>CI 里用到的 &lt;code>node&lt;/code>、&lt;code>python&lt;/code>、&lt;code>golang&lt;/code> 等基础镜像也尽量走私有仓库。&lt;/li>
&lt;li>npm、pip、Go modules 配置国内源。&lt;/li>
&lt;li>如果 GitHub webhook 到国内服务器不稳定，更推荐自建 Gitea、Forgejo 或 GitLab。&lt;/li>
&lt;/ul>
&lt;hr>
&lt;h2 id="二为什么关注-woodpecker-ci">二、为什么关注 Woodpecker CI
&lt;/h2>&lt;p>Woodpecker CI 值得了解的原因主要有几个：&lt;/p>
&lt;ol>
&lt;li>&lt;strong>轻量&lt;/strong>：核心组件少，部署成本比很多完整 DevOps 平台低。&lt;/li>
&lt;li>&lt;strong>容器化&lt;/strong>：每个 step 都运行在指定镜像里，构建环境可复现。&lt;/li>
&lt;li>&lt;strong>自托管友好&lt;/strong>：CI 服务、构建 agent、数据都可以放在自己的机器上。&lt;/li>
&lt;li>&lt;strong>YAML 简洁&lt;/strong>：常见的 build、test、deploy 流水线写法很直观。&lt;/li>
&lt;li>&lt;strong>多代码平台支持&lt;/strong>：不是只能依赖 GitHub Actions 这一类平台内置 CI。&lt;/li>
&lt;/ol>
&lt;p>它比较适合下面这些场景：&lt;/p>
&lt;ul>
&lt;li>使用 Gitea、Forgejo、GitLab 自建代码平台，希望配一套轻量 CI。&lt;/li>
&lt;li>项目需要访问内网环境，不方便把构建任务放到公有云 CI。&lt;/li>
&lt;li>小团队想要一个足够简单、可维护、可迁移的 CI/CD 系统。&lt;/li>
&lt;li>已经用 Docker 管理构建、测试和部署环境。&lt;/li>
&lt;/ul>
&lt;p>不太适合的场景：&lt;/p>
&lt;ul>
&lt;li>团队已经深度依赖 GitHub Actions Marketplace 或 GitLab CI 生态。&lt;/li>
&lt;li>需要复杂的企业级权限、审计、合规工作流。&lt;/li>
&lt;li>不希望维护任何 CI 基础设施。&lt;/li>
&lt;/ul>
&lt;hr>
&lt;h2 id="三核心架构">三、核心架构
&lt;/h2>&lt;p>Woodpecker CI 主要由两个组件组成：&lt;/p>
&lt;h3 id="1-server">1. Server
&lt;/h3>&lt;p>Server 负责 Web UI、用户登录、仓库管理、webhook 接收、流水线调度、状态回写等事情。&lt;/p>
&lt;p>代码平台有新事件时，例如 push 到 &lt;code>main&lt;/code>，平台会通过 webhook 通知 Woodpecker Server。Server 找到对应仓库的流水线配置，然后创建 pipeline。&lt;/p>
&lt;h3 id="2-agent">2. Agent
&lt;/h3>&lt;p>Agent 负责真正执行流水线任务。它从 Server 接收任务，然后启动容器运行 step。&lt;/p>
&lt;p>如果使用 Docker 后端，Agent 通常需要访问宿主机 Docker daemon，所以部署时经常会看到：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="cl">&lt;span class="nt">volumes&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="l">/var/run/docker.sock:/var/run/docker.sock&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>这意味着 Agent 可以控制宿主机 Docker，权限很高。生产环境里要认真限制谁能修改 CI 配置、谁能拿到 secret、哪些仓库可以运行构建。&lt;/p>
&lt;h3 id="3-执行流程">3. 执行流程
&lt;/h3>&lt;p>一个典型流程是：&lt;/p>
&lt;ol>
&lt;li>开发者 push 代码到仓库。&lt;/li>
&lt;li>Git 平台通过 webhook 通知 Woodpecker。&lt;/li>
&lt;li>Woodpecker Server 拉取仓库配置。&lt;/li>
&lt;li>Server 创建 pipeline，并分配给 Agent。&lt;/li>
&lt;li>Agent clone 代码，按 YAML 定义依次运行 step。&lt;/li>
&lt;li>每个 step 在独立容器里执行命令。&lt;/li>
&lt;li>Server 把成功或失败状态回写到代码平台。&lt;/li>
&lt;/ol>
&lt;hr>
&lt;h2 id="四基本概念">四、基本概念
&lt;/h2>&lt;h3 id="1-pipeline">1. Pipeline
&lt;/h3>&lt;p>Pipeline 是一次完整的 CI/CD 执行，例如某次 push 触发的一轮构建。&lt;/p>
&lt;h3 id="2-workflow">2. Workflow
&lt;/h3>&lt;p>一个 pipeline 至少包含一个 workflow。workflow 是一组共享同一个 workspace 的 step。最简单的配置可以写在 &lt;code>.woodpecker.yaml&lt;/code>，也可以把多个 workflow 拆成 &lt;code>.woodpecker/&lt;/code> 目录下的多个 YAML 文件。&lt;/p>
&lt;p>例如：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-text" data-lang="text">&lt;span class="line">&lt;span class="cl">.woodpecker/
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> lint.yaml
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> test.yaml
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> deploy.yaml
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Woodpecker 会把这些文件识别成多个 workflow。多个 workflow 可以并行执行，也可以通过 &lt;code>depends_on&lt;/code> 建立依赖。&lt;/p>
&lt;h3 id="3-step">3. Step
&lt;/h3>&lt;p>Step 是 workflow 里的一个执行单元。每个 step 指定一个镜像，然后在容器里运行命令。&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="cl">&lt;span class="nt">steps&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">test&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">image&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">node:22&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">commands&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="l">npm ci&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="l">npm test&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h3 id="4-plugin">4. Plugin
&lt;/h3>&lt;p>Plugin 本质上也是容器镜像，只是它把常见任务封装好了，例如上传文件、构建镜像、发送通知等。使用 plugin 时一般通过 &lt;code>settings&lt;/code> 传参。&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="cl">&lt;span class="nt">steps&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">upload&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">image&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">woodpeckerci/plugin-s3&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">settings&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">bucket&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">my-bucket&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">source&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">public/**/*&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">target&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">/site/&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">secret_key&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">from_secret&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">aws_secret_key&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h2 id="五创建第一条流水线">五、创建第一条流水线
&lt;/h2>&lt;p>在项目根目录创建：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-text" data-lang="text">&lt;span class="line">&lt;span class="cl">.woodpecker/build.yaml
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>写入：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="cl">&lt;span class="nt">when&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">event&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">push&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">branch&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">main&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">steps&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">install&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">image&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">node:22&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">commands&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="l">npm ci&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">test&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">image&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">node:22&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">commands&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="l">npm test&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">build&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">image&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">node:22&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">commands&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="l">npm run build&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>这条流水线的含义：&lt;/p>
&lt;ul>
&lt;li>只有 push 到 &lt;code>main&lt;/code> 分支才运行。&lt;/li>
&lt;li>使用 &lt;code>node:22&lt;/code> 镜像作为执行环境。&lt;/li>
&lt;li>依次执行安装依赖、测试、构建。&lt;/li>
&lt;li>任意命令返回非 0 状态码，后续 step 默认不会继续执行，workflow 标记失败。&lt;/li>
&lt;/ul>
&lt;p>如果项目是 Python：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="cl">&lt;span class="nt">when&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">event&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="l">push, pull_request]&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">steps&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">test&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">image&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">python:3.12&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">commands&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="l">pip install -r requirements.txt&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="l">pytest&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>如果项目是 Go：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="cl">&lt;span class="nt">when&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">event&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="l">push, pull_request]&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">steps&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">test&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">image&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">golang:1.23&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">commands&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="l">go test ./...&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;hr>
&lt;h2 id="六条件执行">六、条件执行
&lt;/h2>&lt;p>Woodpecker 使用 &lt;code>when&lt;/code> 控制 workflow 或 step 在什么情况下执行。&lt;/p>
&lt;h3 id="1-只在-main-分支执行">1. 只在 main 分支执行
&lt;/h3>&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="cl">&lt;span class="nt">when&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">event&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">push&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">branch&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">main&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h3 id="2-tag-时发布">2. tag 时发布
&lt;/h3>&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="cl">&lt;span class="nt">steps&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">release&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">image&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">alpine&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">commands&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="l">echo &amp;#34;release version $CI_COMMIT_TAG&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">when&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">event&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">tag&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h3 id="3-pull-request-只跑测试不发布">3. pull request 只跑测试，不发布
&lt;/h3>&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="cl">&lt;span class="nt">steps&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">test&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">image&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">node:22&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">commands&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="l">npm ci&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="l">npm test&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">deploy&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">image&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">alpine&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">commands&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="l">./deploy.sh&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">when&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">event&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">push&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">branch&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">main&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>这个模式很重要：不要在 pull request 事件里暴露部署密钥，尤其是公开仓库。&lt;/p>
&lt;hr>
&lt;h2 id="七多个-workflow-的拆分">七、多个 workflow 的拆分
&lt;/h2>&lt;p>大型项目不建议把所有事情塞进一个 YAML。可以拆成：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-text" data-lang="text">&lt;span class="line">&lt;span class="cl">.woodpecker/
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> lint.yaml
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> test.yaml
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> docker.yaml
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> deploy.yaml
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>例如 &lt;code>.woodpecker/lint.yaml&lt;/code>：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="cl">&lt;span class="nt">steps&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">lint&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">image&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">node:22&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">commands&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="l">npm ci&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="l">npm run lint&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;code>.woodpecker/test.yaml&lt;/code>：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="cl">&lt;span class="nt">steps&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">test&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">image&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">node:22&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">commands&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="l">npm ci&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="l">npm test&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;code>.woodpecker/deploy.yaml&lt;/code>：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="cl">&lt;span class="nt">depends_on&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="l">lint&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="l">test&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">when&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">event&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">push&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">branch&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">main&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">steps&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">deploy&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">image&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">alpine&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">commands&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="l">./deploy.sh&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>这样做的好处：&lt;/p>
&lt;ul>
&lt;li>lint 和 test 可以更早反馈。&lt;/li>
&lt;li>不同 workflow 的状态可以分别回写到代码平台。&lt;/li>
&lt;li>workflow 可以并行跑，提升整体速度。&lt;/li>
&lt;li>deploy 明确依赖 lint 和 test 成功。&lt;/li>
&lt;/ul>
&lt;hr>
&lt;h2 id="八secret-管理">八、Secret 管理
&lt;/h2>&lt;p>CI/CD 经常要用密钥，例如 Docker registry 密码、SSH 私钥、云服务 token。不要把这些值写进仓库 YAML。&lt;/p>
&lt;p>Woodpecker 的 secret 可以通过 UI 或 CLI 设置，然后在 YAML 中用 &lt;code>from_secret&lt;/code> 引用：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="cl">&lt;span class="nt">steps&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">deploy&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">image&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">alpine&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">environment&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">SSH_KEY&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">from_secret&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">production_ssh_key&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">commands&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="l">mkdir -p ~/.ssh&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="l">echo &amp;#34;$SSH_KEY&amp;#34; &amp;gt; ~/.ssh/id_rsa&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="l">chmod 600 ~/.ssh/id_rsa&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="l">./deploy.sh&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>也可以传给 plugin：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="cl">&lt;span class="nt">steps&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">publish-image&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">image&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">woodpeckerci/plugin-docker-buildx&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">settings&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">repo&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">registry.example.com/my-app&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">tags&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">latest&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">username&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">from_secret&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">registry_username&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">password&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">from_secret&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">registry_password&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Secret 使用建议：&lt;/p>
&lt;ul>
&lt;li>不要在日志里 &lt;code>echo&lt;/code> secret。&lt;/li>
&lt;li>不要默认把 secret 暴露给 pull request。&lt;/li>
&lt;li>公开仓库尤其要警惕来自 fork 的 PR。&lt;/li>
&lt;li>为生产环境和测试环境使用不同 secret。&lt;/li>
&lt;li>定期轮换部署密钥。&lt;/li>
&lt;/ul>
&lt;hr>
&lt;h2 id="九常见流水线模板">九、常见流水线模板
&lt;/h2>&lt;h3 id="1-前端项目测试并构建">1. 前端项目：测试并构建
&lt;/h3>&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="cl">&lt;span class="nt">when&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">event&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="l">push, pull_request]&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">steps&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">build&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">image&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">node:22&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">commands&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="l">npm ci&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="l">npm run lint&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="l">npm test&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="l">npm run build&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h3 id="2-docker-镜像构建">2. Docker 镜像构建
&lt;/h3>&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="cl">&lt;span class="nt">when&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">event&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">push&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">branch&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">main&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">steps&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">docker&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">image&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">woodpeckerci/plugin-docker-buildx&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">settings&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">repo&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">registry.example.com/my-app&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">tags&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="l">latest&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="l">${CI_COMMIT_SHA}&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">username&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">from_secret&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">registry_username&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">password&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">from_secret&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">registry_password&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h3 id="3-部署到服务器">3. 部署到服务器
&lt;/h3>&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="cl">&lt;span class="nt">depends_on&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="l">build&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">when&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">event&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">push&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">branch&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">main&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">steps&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">deploy&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">image&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">alpine:3.20&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">environment&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">SSH_KEY&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">from_secret&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">production_ssh_key&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">DEPLOY_HOST&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">from_secret&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">production_host&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">commands&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="l">apk add --no-cache openssh-client&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="l">mkdir -p ~/.ssh&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="l">echo &amp;#34;$SSH_KEY&amp;#34; &amp;gt; ~/.ssh/id_rsa&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="l">chmod 600 ~/.ssh/id_rsa&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="l">ssh -o StrictHostKeyChecking=no &amp;#34;$DEPLOY_HOST&amp;#34; &amp;#34;cd /opt/app &amp;amp;&amp;amp; docker compose pull &amp;amp;&amp;amp; docker compose up -d&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>这个例子能跑通，但生产中建议更严谨：&lt;/p>
&lt;ul>
&lt;li>不要长期关闭 &lt;code>StrictHostKeyChecking&lt;/code>。&lt;/li>
&lt;li>使用最小权限部署用户。&lt;/li>
&lt;li>服务器端只允许执行必要命令。&lt;/li>
&lt;li>部署前保留当前版本，方便回滚。&lt;/li>
&lt;/ul>
&lt;hr>
&lt;h2 id="十和-github-actions-的区别">十、和 GitHub Actions 的区别
&lt;/h2>&lt;table>
&lt;thead>
&lt;tr>
&lt;th>对比项&lt;/th>
&lt;th>Woodpecker CI&lt;/th>
&lt;th>GitHub Actions&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>部署方式&lt;/td>
&lt;td>主要面向自托管&lt;/td>
&lt;td>GitHub 托管为主，也支持 self-hosted runner&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>代码平台&lt;/td>
&lt;td>可接多种 forge&lt;/td>
&lt;td>深度绑定 GitHub&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>执行模型&lt;/td>
&lt;td>step 基于容器镜像&lt;/td>
&lt;td>action + shell + runner 环境&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>配置风格&lt;/td>
&lt;td>简洁，接近 Drone CI&lt;/td>
&lt;td>功能丰富，语法更庞大&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>生态&lt;/td>
&lt;td>插件生态较小&lt;/td>
&lt;td>Marketplace 很大&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>适合场景&lt;/td>
&lt;td>自建 Git、内网部署、轻量 CI&lt;/td>
&lt;td>GitHub 项目、丰富第三方 action、托管 CI&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>简单判断：&lt;/p>
&lt;ul>
&lt;li>你的代码主要放 GitHub，并且不想维护 CI 服务器：优先 GitHub Actions。&lt;/li>
&lt;li>你在用 Gitea/Forgejo，或者需要内网自托管：Woodpecker CI 很值得试。&lt;/li>
&lt;li>你希望 CI 配置尽量贴近容器和 shell：Woodpecker CI 的心智负担较低。&lt;/li>
&lt;/ul>
&lt;hr>
&lt;h2 id="十一生产使用建议">十一、生产使用建议
&lt;/h2>&lt;h3 id="1-给-ci-单独准备机器">1. 给 CI 单独准备机器
&lt;/h3>&lt;p>不要把 Woodpecker Agent 随便放在核心业务机器上。Agent 需要运行不可信代码，尤其在多人协作或开源仓库里，风险很高。&lt;/p>
&lt;h3 id="2-限制仓库和用户权限">2. 限制仓库和用户权限
&lt;/h3>&lt;p>谁能启用仓库、谁能改 &lt;code>.woodpecker/&lt;/code> 配置、谁能读取 secret，都要明确。&lt;/p>
&lt;h3 id="3-注意-docker-socket-风险">3. 注意 Docker socket 风险
&lt;/h3>&lt;p>挂载 &lt;code>/var/run/docker.sock&lt;/code> 等于让 Agent 拥有很高的宿主机控制能力。能不用特权就不用特权，能隔离 agent 就隔离 agent。&lt;/p>
&lt;h3 id="4-持久化和备份数据">4. 持久化和备份数据
&lt;/h3>&lt;p>Server 数据目录里包含用户、仓库、流水线等信息。Docker Compose 部署时要持久化 &lt;code>/var/lib/woodpecker/&lt;/code>，并做定期备份。&lt;/p>
&lt;h3 id="5-使用-https">5. 使用 HTTPS
&lt;/h3>&lt;p>&lt;code>WOODPECKER_HOST&lt;/code> 建议配置成 HTTPS 地址。已有 Dokploy、Traefik、Caddy、Nginx 或云平台网关时，直接复用现有网关即可，不需要为了 Woodpecker 额外改用 Nginx。&lt;/p>
&lt;h3 id="6-把流水线当代码审查">6. 把流水线当代码审查
&lt;/h3>&lt;p>&lt;code>.woodpecker/&lt;/code> 下的 YAML 能决定 CI 机器执行什么命令。它应该像业务代码一样经过 review。&lt;/p>
&lt;hr>
&lt;h2 id="十二排查问题的思路">十二、排查问题的思路
&lt;/h2>&lt;h3 id="1-仓库没有触发流水线">1. 仓库没有触发流水线
&lt;/h3>&lt;p>检查：&lt;/p>
&lt;ul>
&lt;li>仓库是否已在 Woodpecker UI 中启用。&lt;/li>
&lt;li>当前用户是否有仓库 admin 权限。&lt;/li>
&lt;li>代码平台 webhook 是否创建成功。&lt;/li>
&lt;li>&lt;code>.woodpecker.yaml&lt;/code> 或 &lt;code>.woodpecker/*.yaml&lt;/code> 路径是否正确。&lt;/li>
&lt;li>&lt;code>when&lt;/code> 条件是否把事件过滤掉了。&lt;/li>
&lt;/ul>
&lt;h3 id="2-agent-不执行任务">2. Agent 不执行任务
&lt;/h3>&lt;p>检查：&lt;/p>
&lt;ul>
&lt;li>&lt;code>woodpecker-agent&lt;/code> 容器是否在运行。&lt;/li>
&lt;li>&lt;code>WOODPECKER_SERVER&lt;/code> 是否能访问。&lt;/li>
&lt;li>&lt;code>WOODPECKER_AGENT_SECRET&lt;/code> 是否和 Server 一致。&lt;/li>
&lt;li>gRPC 端口是否被网络或反向代理挡住。&lt;/li>
&lt;li>Docker socket 是否挂载成功。&lt;/li>
&lt;/ul>
&lt;h3 id="3-step-找不到文件">3. step 找不到文件
&lt;/h3>&lt;p>检查：&lt;/p>
&lt;ul>
&lt;li>文件是否在仓库里。&lt;/li>
&lt;li>前一个 step 生成的文件是否写在 workspace 内。&lt;/li>
&lt;li>多 workflow 之间不共享 workspace，只有同一个 workflow 内的 step 共享。&lt;/li>
&lt;/ul>
&lt;h3 id="4-secret-为空">4. secret 为空
&lt;/h3>&lt;p>检查：&lt;/p>
&lt;ul>
&lt;li>secret 名称是否和 &lt;code>from_secret&lt;/code> 一致。&lt;/li>
&lt;li>当前事件是否允许使用该 secret。&lt;/li>
&lt;li>pull request 是否被默认限制访问 secret。&lt;/li>
&lt;li>secret 是否被限制到某些 plugin 或 image。&lt;/li>
&lt;/ul>
&lt;hr>
&lt;h2 id="十三总结">十三、总结
&lt;/h2>&lt;p>Woodpecker CI 的核心价值是：用较少的组件，搭建一套可自托管、容器化、配置直观的 CI/CD 系统。&lt;/p>
&lt;p>学习它时可以按这个顺序：&lt;/p>
&lt;ol>
&lt;li>先用 Docker Compose 或 Helm 跑一个最小实例。&lt;/li>
&lt;li>在仓库里创建 &lt;code>.woodpecker/build.yaml&lt;/code>，确认 webhook 能触发 pipeline。&lt;/li>
&lt;li>再理解 Server、Agent、Pipeline、Workflow、Step 的关系。&lt;/li>
&lt;li>掌握 &lt;code>steps&lt;/code>、&lt;code>commands&lt;/code>、&lt;code>when&lt;/code>、&lt;code>depends_on&lt;/code>、&lt;code>from_secret&lt;/code>。&lt;/li>
&lt;li>逐步拆分 lint、test、build、deploy 多个 workflow。&lt;/li>
&lt;li>最后考虑生产环境的权限、安全、备份和 agent 隔离。&lt;/li>
&lt;/ol>
&lt;p>如果你维护的是自建 Git 平台，或者项目部署强依赖内网环境，Woodpecker CI 是一个很务实的选择。&lt;/p>
&lt;hr>
&lt;h2 id="参考资料">参考资料
&lt;/h2>&lt;ul>
&lt;li>&lt;a class="link" href="https://woodpecker-ci.org/docs/intro" target="_blank" rel="noopener"
>Woodpecker CI 官方文档&lt;/a>&lt;/li>
&lt;li>&lt;a class="link" href="https://woodpecker-ci.org/docs/administration/installation/docker-compose" target="_blank" rel="noopener"
>Woodpecker CI Docker Compose 安装&lt;/a>&lt;/li>
&lt;li>&lt;a class="link" href="https://woodpecker-ci.org/docs/administration/installation/helm-chart" target="_blank" rel="noopener"
>Woodpecker CI Helm Chart 安装&lt;/a>&lt;/li>
&lt;li>&lt;a class="link" href="https://woodpecker-ci.org/docs/usage/workflow-syntax" target="_blank" rel="noopener"
>Woodpecker CI Workflow syntax&lt;/a>&lt;/li>
&lt;li>&lt;a class="link" href="https://woodpecker-ci.org/docs/usage/workflows" target="_blank" rel="noopener"
>Woodpecker CI Workflows&lt;/a>&lt;/li>
&lt;li>&lt;a class="link" href="https://woodpecker-ci.org/docs/usage/secrets" target="_blank" rel="noopener"
>Woodpecker CI Secrets&lt;/a>&lt;/li>
&lt;/ul></description></item></channel></rss>