[{"data":1,"prerenderedAt":675},["ShallowReactive",2],{"content:\u002Fposts\u002Fftp-deploy":3,"surround:\u002Fposts\u002Fftp-deploy":664},{"id":4,"title":5,"body":6,"categories":637,"date":639,"description":640,"draft":641,"extension":642,"image":643,"meta":644,"navigation":646,"path":647,"permalink":648,"pinned":641,"published":648,"readingTime":649,"recommend":648,"references":648,"seo":654,"sitemap":655,"stem":656,"tags":657,"type":662,"updated":639,"__hash__":663},"content\u002Fposts\u002Fposts\u002Fftp-deploy.md","GitHub Actions FTP 自动部署教程",{"type":7,"value":8,"toc":619},"minimark",[9,19,22,32,36,39],[10,11,15],"alert",{"title":12,"type":13,"icon":14},"AI 迁移提示","info","tabler:robot",[16,17,18],"p",{},"本文由 AI 协助从旧站迁移，尚未完成逐篇人工审校；内容如有疏漏，将在复核后修订。",[10,20],{"title":21,"type":13},"> 本文部分内容由 AI 辅助整理，已结合实际操作进行校验，请根据自身环境谨慎使用。",[23,24,25],"blockquote",{},[16,26,27,31],{},[28,29,30],"span",{},"!IMPORTANT","\n此教程由 DeepSeek V4 Flash 编写，顾拾柒提供思路主导。",[33,34,35],"h2",{"id":35},"写在前面",[16,37,38],{},"群友问能不能搞个 GitHub Action 自动把博客部署到他的服务器上，我寻思不就传个文件的事嘛，分分钟搞定。结果一折腾就是一下午。",[10,40,43,56,62,66,73,83,86,94,101,108,157,160,176,179,182,195,199,206,214,217,224,227,232,239,245,254,257,263,267,278,359,365,370,374,381,387,394,398,403,443,449,458,461,467,471,481,485,491,537,543,555,559,562,568,575,581,588,591,594,597,612],{"title":41,"type":42},"适用场景","tip",[16,44,45,46,50,51,55],{},"自己的云服务器配置比较低，跑不动 ",[47,48,49],"code",{"code":49},"pnpm build","（比如 1C2G 的小鸡），但又想用自己服务器的带宽和存储。这种情况下，可以利用 ",[52,53,54],"strong",{},"GitHub Actions 的免费构建资源"," 来编译站点，再把产物推到你的服务器上。",[16,57,58,61],{},[28,59,60],{},"!NOTE","\n如果你有更好的服务器，可以直接在服务器上进行构建，可以直接使用 GitHub 的 web hook 去动态拉取、构建等等。",[33,63,65],{"id":64},"踩坑一逐文件上传","踩坑一：逐文件上传",[16,67,68,69,72],{},"一开始用的是 GitHub Marketplace 上最流行的 ",[47,70,71],{"code":71},"SamKirkland\u002FFTP-Deploy-Action","：",[74,75,81],"pre",{"className":76,"code":78,"language":79,"meta":80},[77],"language-yaml","- uses: SamKirkland\u002FFTP-Deploy-Action@v4.3.5\n  with:\n    server: ${{ secrets.FTP_HOST }}\n    username: ${{ secrets.FTP_USERNAME }}\n    password: ${{ secrets.FTP_PASSWORD }}\n    local-dir: .\u002Fdist\u002F\n    server-dir: \u002F\n","yaml","",[47,82,78],{"__ignoreMap":80},[16,84,85],{},"结果跑起来就报错：",[74,87,92],{"className":88,"code":90,"language":91},[89],"language-text","Error: Timeout when trying to open data connection to ***:39487\n","text",[47,93,90],{"__ignoreMap":80},[16,95,96],{},[97,98],"img",{"alt":99,"src":100},"能跑起来就需要超长时间","https:\u002F\u002Fimg.olinl.com\u002Ffile\u002Fpost-img\u002Fimages\u002FuU0qy728.webp",[16,102,103,104,107],{},"控制连接能登录、能列目录，但",[52,105,106],{},"传文件就超时","。这就要从 FTP 的协议设计说起了——FTP 分两条连接：",[109,110,111,127],"table",{},[112,113,114],"thead",{},[115,116,117,121,124],"tr",{},[118,119,120],"th",{},"类型",[118,122,123],{},"作用",[118,125,126],{},"特点",[128,129,130,144],"tbody",{},[115,131,132,138,141],{},[133,134,135],"td",{},[52,136,137],{},"控制连接",[133,139,140],{},"登录、发指令",[133,142,143],{},"固定端口，稳如老狗",[115,145,146,151,154],{},[133,147,148],{},[52,149,150],{},"数据连接",[133,152,153],{},"传输文件内容",[133,155,156],{},"另开端口，随缘连通",[16,158,159],{},"问题就出在这个数据连接上。FTP 有两种模式建立数据连接：",[161,162,163,170],"ul",{},[164,165,166,169],"li",{},[52,167,168],{},"被动模式（PASV）","：服务器开放一个随机端口，客户端主动连过去",[164,171,172,175],{},[52,173,174],{},"主动模式（PORT）","：客户端开放一个端口，服务器连回来",[16,177,178],{},"理论上 GitHub Actions 连服务器应该用被动模式，但我的服务器当时走的是 FRP 内网穿透，只转发了控制端口（21），数据端口被防火墙拦死了。被动模式连不上。",[16,180,181],{},"那换成主动模式试试？结果 GitHub Actions 的运行器也没有公网 IP 让服务器连回来。",[16,183,184,187,190,191,194],{},[28,185,186],{},"!WARNING",[52,188,189],{},"被动模式","客户端过不去，",[52,192,193],{},"主动模式","服务端过不来——逐文件传这条路走不通。",[33,196,198],{"id":197},"踩坑二打包上传","踩坑二：打包上传",[16,200,201,202,205],{},"既然传多文件要反复建立数据连接容易翻车，那",[52,203,204],{},"只传一个文件","不就完事了？",[74,207,212],{"className":208,"code":210,"language":211,"meta":80},[209],"language-bash","tar -czf dist.tar.gz -C dist .\ncurl -T dist.tar.gz \\\n  --connect-timeout 30 \\\n  --retry 3 \\\n  --user user:pass \\\n  ftp:\u002F\u002Fserver.com\u002Fdist.tar.gz\n","bash",[47,213,210],{"__ignoreMap":80},[16,215,216],{},"curl 自带的 FTP 客户端处理数据连接比那个 Action 稳得多，而且只建立一个数据连接，传完就断，完全没有之前的端口问题。",[16,218,219,220,223],{},"果然一次成功。但问题来了——",[52,221,222],{},"传上去的是个压缩包，网站没法直接跑","。所以还需要在服务器上配一个自动解压的定时任务。",[33,225,226],{"id":226},"正式教程",[228,229,231],"h3",{"id":230},"第一步创建工作流文件","第一步：创建工作流文件",[16,233,234,235,238],{},"在 GitHub 仓库中创建 ",[47,236,237],{"code":237},".github\u002Fworkflows\u002Fdeploy.yml","，路径如下：",[74,240,243],{"className":241,"code":242,"language":91},[89],"你的仓库\u002F\n├── .github\u002F\n│   └── workflows\u002F\n│       └── ftp-deploy.yml         ← 新建这个文件\n├── src\u002F\n├── public\u002F\n└── ...\n",[47,244,242],{"__ignoreMap":80},[16,246,247,249,250,253],{},[28,248,60],{},"\n如果 ",[47,251,252],{"code":252},".github\u002Fworkflows"," 目录不存在，手动创建即可。",[16,255,256],{},"将以下内容粘贴进去：",[74,258,261],{"className":259,"code":260,"language":79,"meta":80},[77],"name: FTP 部署\n\non:\n  push:\n    branches: [ master ]\n  workflow_dispatch:\n\nconcurrency:\n  group: \"ftp-deploy\"\n  cancel-in-progress: false\n\njobs:\n  deploy:\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions\u002Fcheckout@v4\n\n      - name: ⚡ 设置 Node.js\n        uses: actions\u002Fsetup-node@v4\n        with:\n          node-version: '22'\n\n      - name: 📦 设置 pnpm\n        uses: pnpm\u002Faction-setup@v4\n        with:\n          version: 9.14.4\n          run_install: false\n\n      - name: 🔗 安装依赖\n        run: pnpm install --frozen-lockfile\n\n      - name: 🔨 构建站点\n        run: pnpm run build\n\n      - name: 📦 打包并上传\n        run: |\n          tar -czf dist.tar.gz -C dist .\n          curl -T dist.tar.gz \\\n            --connect-timeout 30 \\\n            --retry 3 \\\n            --retry-delay 5 \\\n            ftp:\u002F\u002F${{ secrets.FTP_HOST }}:${{ secrets.FTP_PORT }}\u002Fdist.tar.gz \\\n            --insecure \\\n            --user ${{ secrets.FTP_USERNAME }}:${{ secrets.FTP_PASSWORD }}\n",[47,262,260],{"__ignoreMap":80},[228,264,266],{"id":265},"第二步配置-github-secrets","第二步：配置 GitHub Secrets",[16,268,269,270,273,274,277],{},"在 GitHub 仓库页面点击 ",[52,271,272],{},"Settings → Secrets and variables → Actions","，然后点击 ",[52,275,276],{},"New repository secret","，依次添加以下四个密钥：",[109,279,280,293],{},[112,281,282],{},[115,283,284,287,290],{},[118,285,286],{},"Secret 名称",[118,288,289],{},"说明",[118,291,292],{},"示例",[128,294,295,314,329,344],{},[115,296,297,302,305],{},[133,298,299],{},[47,300,301],{"code":301},"FTP_HOST",[133,303,304],{},"FTP 服务器地址",[133,306,307,310,311],{},[47,308,309],{"code":309},"192.168.1.1"," 或 ",[47,312,313],{"code":313},"ftp.example.com",[115,315,316,321,324],{},[133,317,318],{},[47,319,320],{"code":320},"FTP_PORT",[133,322,323],{},"FTP 服务器端口",[133,325,326],{},[47,327,328],{"code":328},"21",[115,330,331,336,339],{},[133,332,333],{},[47,334,335],{"code":335},"FTP_USERNAME",[133,337,338],{},"FTP 用户名",[133,340,341],{},[47,342,343],{"code":343},"blog_user",[115,345,346,351,354],{},[133,347,348],{},[47,349,350],{"code":350},"FTP_PASSWORD",[133,352,353],{},"FTP 密码",[133,355,356],{},[47,357,358],{"code":358},"your_password",[16,360,361],{},[97,362],{"alt":363,"src":364},"配置 GitHub Secrets","https:\u002F\u002Fimg.olinl.com\u002Ffile\u002Fpost-img\u002Fimages\u002Fp4hVhMyo.webp",[16,366,367,369],{},[28,368,60],{},"\n依次添加 FTP_HOST、FTP_PORT、FTP_USERNAME、FTP_PASSWORD 四个密钥",[228,371,373],{"id":372},"第三步安装并配置-ftp-服务端","第三步：安装并配置 FTP 服务端",[16,375,376,377,380],{},"以宝塔面板为例，进入 ",[52,378,379],{},"软件商店 → Pure-FTPd → 安装","。",[16,382,383],{},[97,384],{"alt":385,"src":386},"安装 Pure-FTPd","https:\u002F\u002Fimg.olinl.com\u002Ffile\u002Fpost-img\u002Fimages\u002Fv7Z4SP7X.webp",[16,388,389,390,393],{},"然后添加 FTP 账号：",[52,391,392],{},"宝塔 → FTP → 添加账号","，目录指向你的网站根目录。",[228,395,397],{"id":396},"第四步防火墙放行端口","第四步：防火墙放行端口",[16,399,400,72],{},[52,401,402],{},"宝塔面板 → 安全 → 系统防火墙 → 添加端口规则",[109,404,405,415],{},[112,406,407],{},[115,408,409,412],{},[118,410,411],{},"选项",[118,413,414],{},"值",[128,416,417,427,435],{},[115,418,419,422],{},[133,420,421],{},"端口",[133,423,424],{},[47,425,426],{"code":426},"39000-40000",[115,428,429,432],{},[133,430,431],{},"协议",[133,433,434],{},"TCP",[115,436,437,440],{},[133,438,439],{},"来源",[133,441,442],{},"全部",[16,444,445],{},[97,446],{"alt":447,"src":448},"防火墙放行端口","https:\u002F\u002Fimg.olinl.com\u002Ffile\u002Fpost-img\u002Fimages\u002FvbXCIAxy.webp",[16,450,451,453,454,457],{},[28,452,186],{},"\n宝塔放行后，如果用的是云服务器（阿里云\u002F腾讯云等），还需要去云厂商的控制台 ",[52,455,456],{},"安全组"," 中放行同样的端口。",[16,459,460],{},"完成后重启 Pure-FTPd：",[74,462,465],{"className":463,"code":464,"language":91},[89],"宝塔 → 软件商店 → Pure-FTPd → 重启\n",[47,466,464],{"__ignoreMap":80},[228,468,470],{"id":469},"第五步配置网站域名","第五步：配置网站域名",[16,472,473,476,477,480],{},[52,474,475],{},"宝塔 → 网站 → 添加站点","，输入你的域名，根目录选择一个目录（比如 ",[47,478,479],{"code":479},"\u002Fwww\u002Fwwwroot\u002Fblog","）。",[228,482,484],{"id":483},"第六步添加自动解压计划任务","第六步：添加自动解压计划任务",[16,486,487,488,72],{},"由于我们上传的是压缩包，服务器需要自动解压。",[52,489,490],{},"宝塔 → 计划任务 → 添加计划任务",[109,492,493,501],{},[112,494,495],{},[115,496,497,499],{},[118,498,411],{},[118,500,414],{},[128,502,503,511,519,527],{},[115,504,505,508],{},[133,506,507],{},"任务名称",[133,509,510],{},"自动部署",[115,512,513,516],{},[133,514,515],{},"任务类型",[133,517,518],{},"Shell脚本",[115,520,521,524],{},[133,522,523],{},"执行周期",[133,525,526],{},"N分钟 → 1",[115,528,529,532],{},[133,530,531],{},"脚本内容",[133,533,534],{},[47,535,536],{"code":536},"cd \u002Fwww\u002Fwwwroot\u002F你的网站目录 && [ -f dist.tar.gz ] && tar -xzf dist.tar.gz && rm -f dist.tar.gz",[16,538,539],{},[97,540],{"alt":541,"src":542},"添加计划任务","https:\u002F\u002Fimg.olinl.com\u002Ffile\u002Fpost-img\u002Fimages\u002F1p2PRfMR.webp",[16,544,545,548,549,552,553,380],{},[28,546,547],{},"!TIP","\n把 ",[47,550,551],{"code":551},"你的网站目录"," 替换成实际的路径，比如 ",[47,554,479],{"code":479},[228,556,558],{"id":557},"第七步运行工作流","第七步：运行工作流",[16,560,561],{},"将上述所有配置提交并推送到 GitHub：",[74,563,566],{"className":564,"code":565,"language":211,"meta":80},[209],"git add .\ngit commit -m \"添加 FTP 自动部署\"\ngit push\n",[47,567,565],{"__ignoreMap":80},[16,569,570,571,574],{},"推送后，进入 GitHub 仓库页面，点击 ",[52,572,573],{},"Actions"," 选项卡，就能看到正在运行的工作流：",[16,576,577],{},[97,578],{"alt":579,"src":580},"工作流运行","https:\u002F\u002Fimg.olinl.com\u002Ffile\u002Fpost-img\u002Fimages\u002FjOVYuXlV.webp",[16,582,583,584,587],{},"等待几分钟，构建 + 上传完成后，访问你的域名就能看到更新后的网站了。以后每次 ",[47,585,586],{"code":586},"git push"," 都会自动触发部署。",[33,589,590],{"id":590},"总结",[16,592,593],{},"回过头来看，不过是传一些静态文件到服务器这么简单的事，结果绕了一大圈。",[16,595,596],{},"主要教训就两条：",[598,599,600,606],"ol",{},[164,601,602,605],{},[52,603,604],{},"FTP 的两连接设计在今天的网络环境里真的很要命。"," NAT、防火墙、容器、CI 运行器……随便一个都能让数据连接翻车。",[164,607,608,611],{},[52,609,610],{},"能传一个文件就别传一万个。"," 单文件的 curl 上传比逐文件反复建连接稳定太多了。",[16,613,614,615,618],{},"最终方案：",[52,616,617],{},"tar 打包 + curl 上传 + 宝塔计划任务自动解压","，简单可靠。",{"title":80,"searchDepth":620,"depth":620,"links":621},4,[622,624,625,626,636],{"id":35,"depth":623,"text":35},2,{"id":64,"depth":623,"text":65},{"id":197,"depth":623,"text":198},{"id":226,"depth":623,"text":226,"children":627},[628,630,631,632,633,634,635],{"id":230,"depth":629,"text":231},3,{"id":265,"depth":629,"text":266},{"id":372,"depth":629,"text":373},{"id":396,"depth":629,"text":397},{"id":469,"depth":629,"text":470},{"id":483,"depth":629,"text":484},{"id":557,"depth":629,"text":558},{"id":590,"depth":623,"text":590},[638],"部署","2026-07-28","从踩坑到实战，教你用 GitHub Actions 自动构建博客并部署到自己的 FTP 服务器。",false,"md","https:\u002F\u002Fimg.olinl.com\u002Ffile\u002Fpost-img\u002Fcover\u002FlkqasBlA.webp",{"slots":645},{},true,"\u002Fposts\u002Fftp-deploy",null,{"text":650,"minutes":651,"time":652,"words":653},"8 min read",7.49,449400,1498,{"title":5,"description":640},{"loc":647},"posts\u002Fposts\u002Fftp-deploy",[658,659,660,661],"CI\u002FCD","FTP","GitHub Actions","教程","tech","0V6BWtl4Hareuv5of5lfv2aIL9cu6TeohrKmBEx8bNQ",[665,670],{"title":666,"path":667,"stem":668,"date":669,"type":662,"children":-1},"Firefly 博客魔改记","\u002Fposts\u002Fcustomization-log","posts\u002Fposts\u002Fcustomization-log","2026-07-22 19:30:00",{"title":671,"path":672,"stem":673,"date":674,"type":662,"children":-1},"欢迎加入 Olinl 的 QQ 交流群 🎉","\u002F2026\u002Fqq-group","posts\u002F2026\u002Fqq-group","2026-06-24 23:58:21",1788712221314]