[{"data":1,"prerenderedAt":1338},["ShallowReactive",2],{"page-/post/nuxt/cloud/use-github-actions-deloy-nuxt-blog":3,"surrounding-page":1329},{"id":4,"title":5,"author":6,"body":7,"date":1317,"description":1318,"extension":1319,"group":6,"lastmod":1317,"meta":1320,"navigation":164,"path":1321,"rawbody":1322,"seo":1323,"showTitle":6,"stem":1324,"tags":1325,"versions":6,"__hash__":1328},"content/post/nuxt/cloud/use-github-actions-deloy-nuxt-blog.md","白嫖一下 Github Actions 打包部署博客",null,{"type":8,"value":9,"toc":1315},"minimark",[10,30,44,51,54,77,100,116,124,131,141,148,1259,1270,1281,1286,1291,1305,1308,1311],[11,12,13,14,18,19,21,22,25,26,29],"p",{},"之前我的博客一直使用 ",[15,16,17],"code",{},"Gitea"," 来管理代码，然后顺势也配好了 ",[15,20,17],{}," 的 ",[15,23,24],{},"actions","，推送了指定 ",[15,27,28],{},"commit msg"," 时就会自动打包部署代码。",[11,31,32,33,36,37,40,41],{},"但代价就是服务器内存从 ",[15,34,35],{},"4G"," 升到了 ",[15,38,39],{},"8G","，因为打包时峰值内存占用要到 ",[15,42,43],{},"6G",[11,45,46],{},[47,48],"img",{"alt":49,"src":50},"","https://img.zzao.club/article/202508261410524.png",[11,52,53],{},"最近想把博客相关的环境容器化",[11,55,56,57,60,61,64,65,68,69,72,73,76],{},"但是和 ",[15,58,59],{},"GTP5"," 一番讨论后，问题还是出在 ",[15,62,63],{},"Nuxt/Content"," 上，",[15,66,67],{},"Content"," 主动拉取 ",[15,70,71],{},"Github repo"," 的行为依赖于 ",[15,74,75],{},"nuxt build","，所以如果要单独发布一篇文章，不得不重新上传一个镜像。",[11,78,79,80,83,84,88,89,92,93,96,97],{},"所以最后还是决定博客不用 ",[15,81,82],{},"docker"," 了， ",[85,86,87],"strong",{},"mysql + redis"," 使用 ",[15,90,91],{},"docker compose"," 管理，博客还是用 ",[15,94,95],{},"pm2","  + ",[15,98,99],{},"envfile",[11,101,102,103,106,107,109,110,113,114],{},"如果迁移服务器的话，就需要自己全局安装 ",[15,104,105],{},"node"," 、",[15,108,95],{},"，然后使用现有的 ",[15,111,112],{},"docker-compose.yml"," 启动数据库环境，以及迁移现有的生产环境的 ",[15,115,99],{},[11,117,118,119,121,122],{},"然后继续走 Github Actions 构建、打包、ssh 传输到目标服务器，运行 ",[15,120,95],{}," 命令，加载 ",[15,123,99],{},[11,125,126,127,130],{},"等 ",[15,128,129],{},"NuxtContent"," 支持主动拉取新的仓库文件或者定时拉取后，再进行调整",[11,132,133,134],{},"然后分享一下，",[135,136,140],"a",{"href":137,"rel":138},"https://github.com/aatrooox/blog.zzao.club",[139],"nofollow","博客开源地址",[11,142,143,144,147],{},"以及 ",[15,145,146],{},"action"," 脚本",[149,150,155],"pre",{"className":151,"code":152,"filename":153,"language":154,"meta":49,"style":49},"language-yaml shiki shiki-themes github-light","\nname: Deploy via GitHub Actions (SSH + PM2 Prod)\n\non:\n  push:\n    branches:\n      - main\n\nconcurrency:\n  group: deploy-main\n  cancel-in-progress: true\n\njobs:\n  deploy:\n    runs-on: ubuntu-latest\n    environment:\n      name: zzaoclub\n    if: contains(github.event.head_commit.message, 'chore(release)')\n    steps:\n      - name: Checkout\n        uses: actions/checkout@v4\n\n      - name: Setup PNPM\n        uses: pnpm/action-setup@v4\n\n      - name: Setup Node\n        uses: actions/setup-node@v4\n        with:\n          node-version: 20\n          cache: pnpm\n\n      - name: Install deps\n        run: pnpm install --no-frozen-lockfile\n\n      - name: Build\n        env:\n          CONTENT_REPO_TOKEN: ${{ secrets.CONTENT_REPO_TOKEN }}\n          DATABASE_URL: ${{ secrets.DATABASE_URL }}\n          NUXT_FEISHU_WEBHOOK: ${{ secrets.NUXT_FEISHU_WEBHOOK }}\n          NUXT_FEISHU_USER_ID: ${{ secrets.NUXT_FEISHU_USER_ID }}\n          NUXT_JWT_SECRET: ${{ secrets.NUXT_JWT_SECRET }}\n          NUXT_NODEMAILER_HOST: ${{ secrets.NUXT_NODEMAILER_HOST }}\n          NUXT_NODEMAILER_PORT: ${{ secrets.NUXT_NODEMAILER_PORT }}\n          NUXT_NODEMAILER_AUTH_USER: ${{ secrets.NUXT_NODEMAILER_AUTH_USER }}\n          NUXT_NODEMAILER_AUTH_PASS: ${{ secrets.NUXT_NODEMAILER_AUTH_PASS }}\n          NUXT_UMAMI_HOST: ${{ secrets.NUXT_UMAMI_HOST }}\n          NUXT_UMAMI_USER: ${{ secrets.NUXT_UMAMI_USER }}\n          NUXT_UMAMI_PASS: ${{ secrets.NUXT_UMAMI_PASS }}\n          NUXT_SESSION_PASSWORD: ${{ secrets.NUXT_SESSION_PASSWORD }}\n          NUXT_OAUTH_GITHUB_CLIENT_ID: ${{ secrets.NUXT_OAUTH_GITHUB_CLIENT_ID }}\n          NUXT_OAUTH_GITHUB_CLIENT_SECRET: ${{ secrets.NUXT_OAUTH_GITHUB_CLIENT_SECRET }}\n          NUXT_COS_SECRET_ID: ${{ secrets.NUXT_COS_SECRET_ID }}\n          NUXT_COS_SECRET_KEY: ${{ secrets.NUXT_COS_SECRET_KEY }}\n          NUXT_COS_BUCKET: ${{ secrets.NUXT_COS_BUCKET }}\n          NUXT_COS_REGION: ${{ secrets.NUXT_COS_REGION }}\n        run: |\n          NODE_OPTIONS=\"--max-old-space-size=4080\" pnpm build\n\n      - name: Pack artifact (flatten .output)\n        run: |\n          rm -rf distpkg && mkdir -p distpkg\n          cp -R .output/* distpkg/\n          cp -f pm2.config.json distpkg/\n          cp -f pm2.preload.cjs distpkg/\n          # Include Drizzle migrations & config for server-side migrate\n          mkdir -p distpkg/lib/drizzle\n          if [ -d lib/drizzle/migrations ]; then cp -R lib/drizzle/migrations distpkg/lib/drizzle/; fi\n          if [ -f drizzle.config.ts ]; then cp -f drizzle.config.ts distpkg/; fi\n          tar -C distpkg -czf artifact.tgz .\n          du -h artifact.tgz\n\n      - name: Prepare SSH key\n        run: |\n          mkdir -p ~/.ssh\n          chmod 700 ~/.ssh\n          echo \"${{ secrets.SSH_PRIVATE_KEY }}\" | tr -d '\\r' > ~/.ssh/id_rsa\n          chmod 600 ~/.ssh/id_rsa\n\n      - name: Upload artifact via scp\n        env:\n          SSH_HOST: ${{ secrets.SSH_HOST }}\n          SSH_USER: ${{ secrets.SSH_USER }}\n          SSH_PORT: ${{ secrets.SSH_PORT }}\n        run: |\n          ssh -o StrictHostKeyChecking=no -p \"${SSH_PORT}\" \"${SSH_USER}@${SSH_HOST}\" \"mkdir -p /root/web/blog\"\n          scp -P \"${SSH_PORT}\" artifact.tgz \"${SSH_USER}@${SSH_HOST}:/root/web/blog/artifact.tgz\"\n\n      - name: Deploy prod & start with PM2\n        env:\n          SSH_HOST: ${{ secrets.SSH_HOST }}\n          SSH_USER: ${{ secrets.SSH_USER }}\n          SSH_PORT: ${{ secrets.SSH_PORT }}\n        run: |\n          ssh -o StrictHostKeyChecking=no -p \"${SSH_PORT}\" \"${SSH_USER}@${SSH_HOST}\" \u003C\u003C 'EOSSH'\n          set -e\n          APP_DIR=/root/web/blog\n          ENVFILE=/root/envs/blog/.env\n          mkdir -p \"$APP_DIR\"\n          cd \"$APP_DIR\"\n          # Clean app dir but keep artifact\n          find \"$APP_DIR\" -mindepth 1 -maxdepth 1 ! -name artifact.tgz -exec rm -rf {} +\n          tar -xzf artifact.tgz -C \"$APP_DIR\"\n          rm -f artifact.tgz\n          # Prefer globally installed dotenv-cli; fallback to npx dotenv-cli; else source fallback\n          if command -v dotenv >/dev/null 2>&1; then\n            DOTENV=\"dotenv -e \\\"$ENVFILE\\\" --\"\n          elif command -v npx >/dev/null 2>&1; then\n            DOTENV=\"npx -y dotenv-cli -e \\\"$ENVFILE\\\" --\"\n          else\n            DOTENV=\"\"\n          fi\n\n          # Apply Drizzle migrations on server\n          if [ -n \"$DOTENV\" ]; then\n            eval \"$DOTENV\" npx -y drizzle-kit@0.31.4 migrate || true\n          else\n            echo \"dotenv-cli not available; using source fallback for migrations\"\n            if [ -f \"$ENVFILE\" ]; then set -a; . \"$ENVFILE\"; set +a; fi\n            if command -v npx >/dev/null 2>&1; then\n              npx -y drizzle-kit@0.31.4 migrate || true\n            else\n              echo \"npx not found; skipping migrations\"\n            fi\n          fi\n\n          # Start cleanly: use prod pm2.config.json\n          pm2 delete Blog >/dev/null 2>&1 || true\n          pm2 start pm2.config.json --update-env\n          pm2 save\n          EOSSH\n\n      - name: Notify (Feishu)\n        if: always()\n        run: |\n          curl -X POST -H \"Content-Type: application/json\" \\\n            -d '{\"msg_type\":\"text\",\"content\":{\"text\":\"'\"${{ github.repository }}\"' - GH canary deploy ['\"'\"${{ job.status }}\"'\"']\"}}' \\\n            \"${{ secrets.NUXT_FEISHU_WEBHOOK }}\"\n\n",".github/workflows/deploy-ssh.yml","yaml",[15,156,157,166,181,186,196,204,212,221,226,234,245,256,261,269,277,288,296,307,318,326,338,349,354,366,376,381,393,403,411,422,433,438,450,461,466,478,486,497,508,519,530,541,552,563,574,585,596,607,618,629,640,651,662,673,684,695,706,712,717,729,738,744,750,756,762,768,774,780,786,792,798,803,815,824,830,836,842,848,853,865,872,883,894,905,914,920,926,931,943,950,959,968,977,986,992,998,1004,1010,1016,1022,1028,1034,1040,1046,1052,1058,1064,1070,1076,1082,1088,1094,1099,1105,1111,1117,1122,1128,1134,1140,1146,1152,1158,1164,1169,1174,1180,1186,1192,1198,1204,1209,1221,1232,1241,1247,1253],{"__ignoreMap":49},[158,159,162],"span",{"class":160,"line":161},"line",1,[158,163,165],{"emptyLinePlaceholder":164},true,"\n",[158,167,169,173,177],{"class":160,"line":168},2,[158,170,172],{"class":171},"shJU0","name",[158,174,176],{"class":175},"sgsFI",": ",[158,178,180],{"class":179},"sYBdl","Deploy via GitHub Actions (SSH + PM2 Prod)\n",[158,182,184],{"class":160,"line":183},3,[158,185,165],{"emptyLinePlaceholder":164},[158,187,189,193],{"class":160,"line":188},4,[158,190,192],{"class":191},"sYu0t","on",[158,194,195],{"class":175},":\n",[158,197,199,202],{"class":160,"line":198},5,[158,200,201],{"class":171},"  push",[158,203,195],{"class":175},[158,205,207,210],{"class":160,"line":206},6,[158,208,209],{"class":171},"    branches",[158,211,195],{"class":175},[158,213,215,218],{"class":160,"line":214},7,[158,216,217],{"class":175},"      - ",[158,219,220],{"class":179},"main\n",[158,222,224],{"class":160,"line":223},8,[158,225,165],{"emptyLinePlaceholder":164},[158,227,229,232],{"class":160,"line":228},9,[158,230,231],{"class":171},"concurrency",[158,233,195],{"class":175},[158,235,237,240,242],{"class":160,"line":236},10,[158,238,239],{"class":171},"  group",[158,241,176],{"class":175},[158,243,244],{"class":179},"deploy-main\n",[158,246,248,251,253],{"class":160,"line":247},11,[158,249,250],{"class":171},"  cancel-in-progress",[158,252,176],{"class":175},[158,254,255],{"class":191},"true\n",[158,257,259],{"class":160,"line":258},12,[158,260,165],{"emptyLinePlaceholder":164},[158,262,264,267],{"class":160,"line":263},13,[158,265,266],{"class":171},"jobs",[158,268,195],{"class":175},[158,270,272,275],{"class":160,"line":271},14,[158,273,274],{"class":171},"  deploy",[158,276,195],{"class":175},[158,278,280,283,285],{"class":160,"line":279},15,[158,281,282],{"class":171},"    runs-on",[158,284,176],{"class":175},[158,286,287],{"class":179},"ubuntu-latest\n",[158,289,291,294],{"class":160,"line":290},16,[158,292,293],{"class":171},"    environment",[158,295,195],{"class":175},[158,297,299,302,304],{"class":160,"line":298},17,[158,300,301],{"class":171},"      name",[158,303,176],{"class":175},[158,305,306],{"class":179},"zzaoclub\n",[158,308,310,313,315],{"class":160,"line":309},18,[158,311,312],{"class":171},"    if",[158,314,176],{"class":175},[158,316,317],{"class":179},"contains(github.event.head_commit.message, 'chore(release)')\n",[158,319,321,324],{"class":160,"line":320},19,[158,322,323],{"class":171},"    steps",[158,325,195],{"class":175},[158,327,329,331,333,335],{"class":160,"line":328},20,[158,330,217],{"class":175},[158,332,172],{"class":171},[158,334,176],{"class":175},[158,336,337],{"class":179},"Checkout\n",[158,339,341,344,346],{"class":160,"line":340},21,[158,342,343],{"class":171},"        uses",[158,345,176],{"class":175},[158,347,348],{"class":179},"actions/checkout@v4\n",[158,350,352],{"class":160,"line":351},22,[158,353,165],{"emptyLinePlaceholder":164},[158,355,357,359,361,363],{"class":160,"line":356},23,[158,358,217],{"class":175},[158,360,172],{"class":171},[158,362,176],{"class":175},[158,364,365],{"class":179},"Setup PNPM\n",[158,367,369,371,373],{"class":160,"line":368},24,[158,370,343],{"class":171},[158,372,176],{"class":175},[158,374,375],{"class":179},"pnpm/action-setup@v4\n",[158,377,379],{"class":160,"line":378},25,[158,380,165],{"emptyLinePlaceholder":164},[158,382,384,386,388,390],{"class":160,"line":383},26,[158,385,217],{"class":175},[158,387,172],{"class":171},[158,389,176],{"class":175},[158,391,392],{"class":179},"Setup Node\n",[158,394,396,398,400],{"class":160,"line":395},27,[158,397,343],{"class":171},[158,399,176],{"class":175},[158,401,402],{"class":179},"actions/setup-node@v4\n",[158,404,406,409],{"class":160,"line":405},28,[158,407,408],{"class":171},"        with",[158,410,195],{"class":175},[158,412,414,417,419],{"class":160,"line":413},29,[158,415,416],{"class":171},"          node-version",[158,418,176],{"class":175},[158,420,421],{"class":191},"20\n",[158,423,425,428,430],{"class":160,"line":424},30,[158,426,427],{"class":171},"          cache",[158,429,176],{"class":175},[158,431,432],{"class":179},"pnpm\n",[158,434,436],{"class":160,"line":435},31,[158,437,165],{"emptyLinePlaceholder":164},[158,439,441,443,445,447],{"class":160,"line":440},32,[158,442,217],{"class":175},[158,444,172],{"class":171},[158,446,176],{"class":175},[158,448,449],{"class":179},"Install deps\n",[158,451,453,456,458],{"class":160,"line":452},33,[158,454,455],{"class":171},"        run",[158,457,176],{"class":175},[158,459,460],{"class":179},"pnpm install --no-frozen-lockfile\n",[158,462,464],{"class":160,"line":463},34,[158,465,165],{"emptyLinePlaceholder":164},[158,467,469,471,473,475],{"class":160,"line":468},35,[158,470,217],{"class":175},[158,472,172],{"class":171},[158,474,176],{"class":175},[158,476,477],{"class":179},"Build\n",[158,479,481,484],{"class":160,"line":480},36,[158,482,483],{"class":171},"        env",[158,485,195],{"class":175},[158,487,489,492,494],{"class":160,"line":488},37,[158,490,491],{"class":171},"          CONTENT_REPO_TOKEN",[158,493,176],{"class":175},[158,495,496],{"class":179},"${{ secrets.CONTENT_REPO_TOKEN }}\n",[158,498,500,503,505],{"class":160,"line":499},38,[158,501,502],{"class":171},"          DATABASE_URL",[158,504,176],{"class":175},[158,506,507],{"class":179},"${{ secrets.DATABASE_URL }}\n",[158,509,511,514,516],{"class":160,"line":510},39,[158,512,513],{"class":171},"          NUXT_FEISHU_WEBHOOK",[158,515,176],{"class":175},[158,517,518],{"class":179},"${{ secrets.NUXT_FEISHU_WEBHOOK }}\n",[158,520,522,525,527],{"class":160,"line":521},40,[158,523,524],{"class":171},"          NUXT_FEISHU_USER_ID",[158,526,176],{"class":175},[158,528,529],{"class":179},"${{ secrets.NUXT_FEISHU_USER_ID }}\n",[158,531,533,536,538],{"class":160,"line":532},41,[158,534,535],{"class":171},"          NUXT_JWT_SECRET",[158,537,176],{"class":175},[158,539,540],{"class":179},"${{ secrets.NUXT_JWT_SECRET }}\n",[158,542,544,547,549],{"class":160,"line":543},42,[158,545,546],{"class":171},"          NUXT_NODEMAILER_HOST",[158,548,176],{"class":175},[158,550,551],{"class":179},"${{ secrets.NUXT_NODEMAILER_HOST }}\n",[158,553,555,558,560],{"class":160,"line":554},43,[158,556,557],{"class":171},"          NUXT_NODEMAILER_PORT",[158,559,176],{"class":175},[158,561,562],{"class":179},"${{ secrets.NUXT_NODEMAILER_PORT }}\n",[158,564,566,569,571],{"class":160,"line":565},44,[158,567,568],{"class":171},"          NUXT_NODEMAILER_AUTH_USER",[158,570,176],{"class":175},[158,572,573],{"class":179},"${{ secrets.NUXT_NODEMAILER_AUTH_USER }}\n",[158,575,577,580,582],{"class":160,"line":576},45,[158,578,579],{"class":171},"          NUXT_NODEMAILER_AUTH_PASS",[158,581,176],{"class":175},[158,583,584],{"class":179},"${{ secrets.NUXT_NODEMAILER_AUTH_PASS }}\n",[158,586,588,591,593],{"class":160,"line":587},46,[158,589,590],{"class":171},"          NUXT_UMAMI_HOST",[158,592,176],{"class":175},[158,594,595],{"class":179},"${{ secrets.NUXT_UMAMI_HOST }}\n",[158,597,599,602,604],{"class":160,"line":598},47,[158,600,601],{"class":171},"          NUXT_UMAMI_USER",[158,603,176],{"class":175},[158,605,606],{"class":179},"${{ secrets.NUXT_UMAMI_USER }}\n",[158,608,610,613,615],{"class":160,"line":609},48,[158,611,612],{"class":171},"          NUXT_UMAMI_PASS",[158,614,176],{"class":175},[158,616,617],{"class":179},"${{ secrets.NUXT_UMAMI_PASS }}\n",[158,619,621,624,626],{"class":160,"line":620},49,[158,622,623],{"class":171},"          NUXT_SESSION_PASSWORD",[158,625,176],{"class":175},[158,627,628],{"class":179},"${{ secrets.NUXT_SESSION_PASSWORD }}\n",[158,630,632,635,637],{"class":160,"line":631},50,[158,633,634],{"class":171},"          NUXT_OAUTH_GITHUB_CLIENT_ID",[158,636,176],{"class":175},[158,638,639],{"class":179},"${{ secrets.NUXT_OAUTH_GITHUB_CLIENT_ID }}\n",[158,641,643,646,648],{"class":160,"line":642},51,[158,644,645],{"class":171},"          NUXT_OAUTH_GITHUB_CLIENT_SECRET",[158,647,176],{"class":175},[158,649,650],{"class":179},"${{ secrets.NUXT_OAUTH_GITHUB_CLIENT_SECRET }}\n",[158,652,654,657,659],{"class":160,"line":653},52,[158,655,656],{"class":171},"          NUXT_COS_SECRET_ID",[158,658,176],{"class":175},[158,660,661],{"class":179},"${{ secrets.NUXT_COS_SECRET_ID }}\n",[158,663,665,668,670],{"class":160,"line":664},53,[158,666,667],{"class":171},"          NUXT_COS_SECRET_KEY",[158,669,176],{"class":175},[158,671,672],{"class":179},"${{ secrets.NUXT_COS_SECRET_KEY }}\n",[158,674,676,679,681],{"class":160,"line":675},54,[158,677,678],{"class":171},"          NUXT_COS_BUCKET",[158,680,176],{"class":175},[158,682,683],{"class":179},"${{ secrets.NUXT_COS_BUCKET }}\n",[158,685,687,690,692],{"class":160,"line":686},55,[158,688,689],{"class":171},"          NUXT_COS_REGION",[158,691,176],{"class":175},[158,693,694],{"class":179},"${{ secrets.NUXT_COS_REGION }}\n",[158,696,698,700,702],{"class":160,"line":697},56,[158,699,455],{"class":171},[158,701,176],{"class":175},[158,703,705],{"class":704},"sD7c4","|\n",[158,707,709],{"class":160,"line":708},57,[158,710,711],{"class":179},"          NODE_OPTIONS=\"--max-old-space-size=4080\" pnpm build\n",[158,713,715],{"class":160,"line":714},58,[158,716,165],{"emptyLinePlaceholder":164},[158,718,720,722,724,726],{"class":160,"line":719},59,[158,721,217],{"class":175},[158,723,172],{"class":171},[158,725,176],{"class":175},[158,727,728],{"class":179},"Pack artifact (flatten .output)\n",[158,730,732,734,736],{"class":160,"line":731},60,[158,733,455],{"class":171},[158,735,176],{"class":175},[158,737,705],{"class":704},[158,739,741],{"class":160,"line":740},61,[158,742,743],{"class":179},"          rm -rf distpkg && mkdir -p distpkg\n",[158,745,747],{"class":160,"line":746},62,[158,748,749],{"class":179},"          cp -R .output/* distpkg/\n",[158,751,753],{"class":160,"line":752},63,[158,754,755],{"class":179},"          cp -f pm2.config.json distpkg/\n",[158,757,759],{"class":160,"line":758},64,[158,760,761],{"class":179},"          cp -f pm2.preload.cjs distpkg/\n",[158,763,765],{"class":160,"line":764},65,[158,766,767],{"class":179},"          # Include Drizzle migrations & config for server-side migrate\n",[158,769,771],{"class":160,"line":770},66,[158,772,773],{"class":179},"          mkdir -p distpkg/lib/drizzle\n",[158,775,777],{"class":160,"line":776},67,[158,778,779],{"class":179},"          if [ -d lib/drizzle/migrations ]; then cp -R lib/drizzle/migrations distpkg/lib/drizzle/; fi\n",[158,781,783],{"class":160,"line":782},68,[158,784,785],{"class":179},"          if [ -f drizzle.config.ts ]; then cp -f drizzle.config.ts distpkg/; fi\n",[158,787,789],{"class":160,"line":788},69,[158,790,791],{"class":179},"          tar -C distpkg -czf artifact.tgz .\n",[158,793,795],{"class":160,"line":794},70,[158,796,797],{"class":179},"          du -h artifact.tgz\n",[158,799,801],{"class":160,"line":800},71,[158,802,165],{"emptyLinePlaceholder":164},[158,804,806,808,810,812],{"class":160,"line":805},72,[158,807,217],{"class":175},[158,809,172],{"class":171},[158,811,176],{"class":175},[158,813,814],{"class":179},"Prepare SSH key\n",[158,816,818,820,822],{"class":160,"line":817},73,[158,819,455],{"class":171},[158,821,176],{"class":175},[158,823,705],{"class":704},[158,825,827],{"class":160,"line":826},74,[158,828,829],{"class":179},"          mkdir -p ~/.ssh\n",[158,831,833],{"class":160,"line":832},75,[158,834,835],{"class":179},"          chmod 700 ~/.ssh\n",[158,837,839],{"class":160,"line":838},76,[158,840,841],{"class":179},"          echo \"${{ secrets.SSH_PRIVATE_KEY }}\" | tr -d '\\r' > ~/.ssh/id_rsa\n",[158,843,845],{"class":160,"line":844},77,[158,846,847],{"class":179},"          chmod 600 ~/.ssh/id_rsa\n",[158,849,851],{"class":160,"line":850},78,[158,852,165],{"emptyLinePlaceholder":164},[158,854,856,858,860,862],{"class":160,"line":855},79,[158,857,217],{"class":175},[158,859,172],{"class":171},[158,861,176],{"class":175},[158,863,864],{"class":179},"Upload artifact via scp\n",[158,866,868,870],{"class":160,"line":867},80,[158,869,483],{"class":171},[158,871,195],{"class":175},[158,873,875,878,880],{"class":160,"line":874},81,[158,876,877],{"class":171},"          SSH_HOST",[158,879,176],{"class":175},[158,881,882],{"class":179},"${{ secrets.SSH_HOST }}\n",[158,884,886,889,891],{"class":160,"line":885},82,[158,887,888],{"class":171},"          SSH_USER",[158,890,176],{"class":175},[158,892,893],{"class":179},"${{ secrets.SSH_USER }}\n",[158,895,897,900,902],{"class":160,"line":896},83,[158,898,899],{"class":171},"          SSH_PORT",[158,901,176],{"class":175},[158,903,904],{"class":179},"${{ secrets.SSH_PORT }}\n",[158,906,908,910,912],{"class":160,"line":907},84,[158,909,455],{"class":171},[158,911,176],{"class":175},[158,913,705],{"class":704},[158,915,917],{"class":160,"line":916},85,[158,918,919],{"class":179},"          ssh -o StrictHostKeyChecking=no -p \"${SSH_PORT}\" \"${SSH_USER}@${SSH_HOST}\" \"mkdir -p /root/web/blog\"\n",[158,921,923],{"class":160,"line":922},86,[158,924,925],{"class":179},"          scp -P \"${SSH_PORT}\" artifact.tgz \"${SSH_USER}@${SSH_HOST}:/root/web/blog/artifact.tgz\"\n",[158,927,929],{"class":160,"line":928},87,[158,930,165],{"emptyLinePlaceholder":164},[158,932,934,936,938,940],{"class":160,"line":933},88,[158,935,217],{"class":175},[158,937,172],{"class":171},[158,939,176],{"class":175},[158,941,942],{"class":179},"Deploy prod & start with PM2\n",[158,944,946,948],{"class":160,"line":945},89,[158,947,483],{"class":171},[158,949,195],{"class":175},[158,951,953,955,957],{"class":160,"line":952},90,[158,954,877],{"class":171},[158,956,176],{"class":175},[158,958,882],{"class":179},[158,960,962,964,966],{"class":160,"line":961},91,[158,963,888],{"class":171},[158,965,176],{"class":175},[158,967,893],{"class":179},[158,969,971,973,975],{"class":160,"line":970},92,[158,972,899],{"class":171},[158,974,176],{"class":175},[158,976,904],{"class":179},[158,978,980,982,984],{"class":160,"line":979},93,[158,981,455],{"class":171},[158,983,176],{"class":175},[158,985,705],{"class":704},[158,987,989],{"class":160,"line":988},94,[158,990,991],{"class":179},"          ssh -o StrictHostKeyChecking=no -p \"${SSH_PORT}\" \"${SSH_USER}@${SSH_HOST}\" \u003C\u003C 'EOSSH'\n",[158,993,995],{"class":160,"line":994},95,[158,996,997],{"class":179},"          set -e\n",[158,999,1001],{"class":160,"line":1000},96,[158,1002,1003],{"class":179},"          APP_DIR=/root/web/blog\n",[158,1005,1007],{"class":160,"line":1006},97,[158,1008,1009],{"class":179},"          ENVFILE=/root/envs/blog/.env\n",[158,1011,1013],{"class":160,"line":1012},98,[158,1014,1015],{"class":179},"          mkdir -p \"$APP_DIR\"\n",[158,1017,1019],{"class":160,"line":1018},99,[158,1020,1021],{"class":179},"          cd \"$APP_DIR\"\n",[158,1023,1025],{"class":160,"line":1024},100,[158,1026,1027],{"class":179},"          # Clean app dir but keep artifact\n",[158,1029,1031],{"class":160,"line":1030},101,[158,1032,1033],{"class":179},"          find \"$APP_DIR\" -mindepth 1 -maxdepth 1 ! -name artifact.tgz -exec rm -rf {} +\n",[158,1035,1037],{"class":160,"line":1036},102,[158,1038,1039],{"class":179},"          tar -xzf artifact.tgz -C \"$APP_DIR\"\n",[158,1041,1043],{"class":160,"line":1042},103,[158,1044,1045],{"class":179},"          rm -f artifact.tgz\n",[158,1047,1049],{"class":160,"line":1048},104,[158,1050,1051],{"class":179},"          # Prefer globally installed dotenv-cli; fallback to npx dotenv-cli; else source fallback\n",[158,1053,1055],{"class":160,"line":1054},105,[158,1056,1057],{"class":179},"          if command -v dotenv >/dev/null 2>&1; then\n",[158,1059,1061],{"class":160,"line":1060},106,[158,1062,1063],{"class":179},"            DOTENV=\"dotenv -e \\\"$ENVFILE\\\" --\"\n",[158,1065,1067],{"class":160,"line":1066},107,[158,1068,1069],{"class":179},"          elif command -v npx >/dev/null 2>&1; then\n",[158,1071,1073],{"class":160,"line":1072},108,[158,1074,1075],{"class":179},"            DOTENV=\"npx -y dotenv-cli -e \\\"$ENVFILE\\\" --\"\n",[158,1077,1079],{"class":160,"line":1078},109,[158,1080,1081],{"class":179},"          else\n",[158,1083,1085],{"class":160,"line":1084},110,[158,1086,1087],{"class":179},"            DOTENV=\"\"\n",[158,1089,1091],{"class":160,"line":1090},111,[158,1092,1093],{"class":179},"          fi\n",[158,1095,1097],{"class":160,"line":1096},112,[158,1098,165],{"emptyLinePlaceholder":164},[158,1100,1102],{"class":160,"line":1101},113,[158,1103,1104],{"class":179},"          # Apply Drizzle migrations on server\n",[158,1106,1108],{"class":160,"line":1107},114,[158,1109,1110],{"class":179},"          if [ -n \"$DOTENV\" ]; then\n",[158,1112,1114],{"class":160,"line":1113},115,[158,1115,1116],{"class":179},"            eval \"$DOTENV\" npx -y drizzle-kit@0.31.4 migrate || true\n",[158,1118,1120],{"class":160,"line":1119},116,[158,1121,1081],{"class":179},[158,1123,1125],{"class":160,"line":1124},117,[158,1126,1127],{"class":179},"            echo \"dotenv-cli not available; using source fallback for migrations\"\n",[158,1129,1131],{"class":160,"line":1130},118,[158,1132,1133],{"class":179},"            if [ -f \"$ENVFILE\" ]; then set -a; . \"$ENVFILE\"; set +a; fi\n",[158,1135,1137],{"class":160,"line":1136},119,[158,1138,1139],{"class":179},"            if command -v npx >/dev/null 2>&1; then\n",[158,1141,1143],{"class":160,"line":1142},120,[158,1144,1145],{"class":179},"              npx -y drizzle-kit@0.31.4 migrate || true\n",[158,1147,1149],{"class":160,"line":1148},121,[158,1150,1151],{"class":179},"            else\n",[158,1153,1155],{"class":160,"line":1154},122,[158,1156,1157],{"class":179},"              echo \"npx not found; skipping migrations\"\n",[158,1159,1161],{"class":160,"line":1160},123,[158,1162,1163],{"class":179},"            fi\n",[158,1165,1167],{"class":160,"line":1166},124,[158,1168,1093],{"class":179},[158,1170,1172],{"class":160,"line":1171},125,[158,1173,165],{"emptyLinePlaceholder":164},[158,1175,1177],{"class":160,"line":1176},126,[158,1178,1179],{"class":179},"          # Start cleanly: use prod pm2.config.json\n",[158,1181,1183],{"class":160,"line":1182},127,[158,1184,1185],{"class":179},"          pm2 delete Blog >/dev/null 2>&1 || true\n",[158,1187,1189],{"class":160,"line":1188},128,[158,1190,1191],{"class":179},"          pm2 start pm2.config.json --update-env\n",[158,1193,1195],{"class":160,"line":1194},129,[158,1196,1197],{"class":179},"          pm2 save\n",[158,1199,1201],{"class":160,"line":1200},130,[158,1202,1203],{"class":179},"          EOSSH\n",[158,1205,1207],{"class":160,"line":1206},131,[158,1208,165],{"emptyLinePlaceholder":164},[158,1210,1212,1214,1216,1218],{"class":160,"line":1211},132,[158,1213,217],{"class":175},[158,1215,172],{"class":171},[158,1217,176],{"class":175},[158,1219,1220],{"class":179},"Notify (Feishu)\n",[158,1222,1224,1227,1229],{"class":160,"line":1223},133,[158,1225,1226],{"class":171},"        if",[158,1228,176],{"class":175},[158,1230,1231],{"class":179},"always()\n",[158,1233,1235,1237,1239],{"class":160,"line":1234},134,[158,1236,455],{"class":171},[158,1238,176],{"class":175},[158,1240,705],{"class":704},[158,1242,1244],{"class":160,"line":1243},135,[158,1245,1246],{"class":179},"          curl -X POST -H \"Content-Type: application/json\" \\\n",[158,1248,1250],{"class":160,"line":1249},136,[158,1251,1252],{"class":179},"            -d '{\"msg_type\":\"text\",\"content\":{\"text\":\"'\"${{ github.repository }}\"' - GH canary deploy ['\"'\"${{ job.status }}\"'\"']\"}}' \\\n",[158,1254,1256],{"class":160,"line":1255},137,[158,1257,1258],{"class":179},"            \"${{ secrets.NUXT_FEISHU_WEBHOOK }}\"\n",[11,1260,1261,1262,1265,1266,1269],{},"代码中的 ",[15,1263,1264],{},"env","，全部要在 ",[15,1267,1268],{},"github"," 配置一遍",[11,1271,1272,1273,1276,1277,1280],{},"先建一个 ",[15,1274,1275],{},"Environment"," ，然后在其下配 ",[15,1278,1279],{},"secrets"," 即可",[11,1282,1283],{},[47,1284],{"alt":49,"src":1285},"https://img.zzao.club/article/202508261410526.png",[11,1287,1288],{},[85,1289,1290],{},"迁移时，先用一个临时目录进行迁移。测试没问题后再覆盖原来的目录",[11,1292,1293,1294,1296,1297,1300,1301,1304],{},"使用 ",[15,1295,1268],{}," 的服务器，打包速度也快了，总流程 ",[85,1298,1299],{},"3m20s"," 。 用自己的服务器，",[15,1302,1303],{},"5m30s","，快了不少。",[11,1306,1307],{},"主要是在没有大量 IO 的情况下，服务器内存占用就很稳定，下一年也不用再续费 8G 的服务器了",[11,1309,1310],{},"不过趁着内存够用，多上一些应用试试水。",[1312,1313,1314],"style",{},"html pre.shiki code .shJU0, html code.shiki .shJU0{--shiki-default:#22863A}html pre.shiki code .sgsFI, html code.shiki .sgsFI{--shiki-default:#24292E}html pre.shiki code .sYBdl, html code.shiki .sYBdl{--shiki-default:#032F62}html pre.shiki code .sYu0t, html code.shiki .sYu0t{--shiki-default:#005CC5}html pre.shiki code .sD7c4, html code.shiki .sD7c4{--shiki-default:#D73A49}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}",{"title":49,"searchDepth":168,"depth":168,"links":1316},[],"2025-08-26T00:00:00.000Z","之前我的博客一直使用 Gitea 来管理代码，然后顺势也配好了 Gitea 的 actions，推送了指定 commit msg 时就会自动打包部署代码。","md",{},"/post/nuxt/cloud/use-github-actions-deloy-nuxt-blog","---\ntitle: 白嫖一下 Github Actions 打包部署博客\ndate: 2025-08-26\nlastmod: 2025-08-26\ntags: [\"博客\", \"Nuxt\"]\n\n---\n之前我的博客一直使用 `Gitea` 来管理代码，然后顺势也配好了 `Gitea` 的 `actions`，推送了指定 `commit msg` 时就会自动打包部署代码。\n\n但代价就是服务器内存从 `4G` 升到了 `8G`，因为打包时峰值内存占用要到 `6G`\n\n![](https://img.zzao.club/article/202508261410524.png)\n\n最近想把博客相关的环境容器化\n\n但是和 `GTP5` 一番讨论后，问题还是出在 `Nuxt/Content` 上，`Content` 主动拉取 `Github repo` 的行为依赖于 `nuxt build`，所以如果要单独发布一篇文章，不得不重新上传一个镜像。\n\n所以最后还是决定博客不用 `docker` 了， **mysql + redis** 使用 `docker compose` 管理，博客还是用 `pm2`  + `envfile`\n\n如果迁移服务器的话，就需要自己全局安装 `node` 、`pm2`，然后使用现有的 `docker-compose.yml` 启动数据库环境，以及迁移现有的生产环境的 `envfile`\n\n然后继续走 Github Actions 构建、打包、ssh 传输到目标服务器，运行 `pm2` 命令，加载 `envfile`\n\n等 `NuxtContent` 支持主动拉取新的仓库文件或者定时拉取后，再进行调整\n\n然后分享一下，[博客开源地址](https://github.com/aatrooox/blog.zzao.club)\n\n以及 `action` 脚本\n\n```yaml [.github/workflows/deploy-ssh.yml]\n\nname: Deploy via GitHub Actions (SSH + PM2 Prod)\n\non:\n  push:\n    branches:\n      - main\n\nconcurrency:\n  group: deploy-main\n  cancel-in-progress: true\n\njobs:\n  deploy:\n    runs-on: ubuntu-latest\n    environment:\n      name: zzaoclub\n    if: contains(github.event.head_commit.message, 'chore(release)')\n    steps:\n      - name: Checkout\n        uses: actions/checkout@v4\n\n      - name: Setup PNPM\n        uses: pnpm/action-setup@v4\n\n      - name: Setup Node\n        uses: actions/setup-node@v4\n        with:\n          node-version: 20\n          cache: pnpm\n\n      - name: Install deps\n        run: pnpm install --no-frozen-lockfile\n\n      - name: Build\n        env:\n          CONTENT_REPO_TOKEN: ${{ secrets.CONTENT_REPO_TOKEN }}\n          DATABASE_URL: ${{ secrets.DATABASE_URL }}\n          NUXT_FEISHU_WEBHOOK: ${{ secrets.NUXT_FEISHU_WEBHOOK }}\n          NUXT_FEISHU_USER_ID: ${{ secrets.NUXT_FEISHU_USER_ID }}\n          NUXT_JWT_SECRET: ${{ secrets.NUXT_JWT_SECRET }}\n          NUXT_NODEMAILER_HOST: ${{ secrets.NUXT_NODEMAILER_HOST }}\n          NUXT_NODEMAILER_PORT: ${{ secrets.NUXT_NODEMAILER_PORT }}\n          NUXT_NODEMAILER_AUTH_USER: ${{ secrets.NUXT_NODEMAILER_AUTH_USER }}\n          NUXT_NODEMAILER_AUTH_PASS: ${{ secrets.NUXT_NODEMAILER_AUTH_PASS }}\n          NUXT_UMAMI_HOST: ${{ secrets.NUXT_UMAMI_HOST }}\n          NUXT_UMAMI_USER: ${{ secrets.NUXT_UMAMI_USER }}\n          NUXT_UMAMI_PASS: ${{ secrets.NUXT_UMAMI_PASS }}\n          NUXT_SESSION_PASSWORD: ${{ secrets.NUXT_SESSION_PASSWORD }}\n          NUXT_OAUTH_GITHUB_CLIENT_ID: ${{ secrets.NUXT_OAUTH_GITHUB_CLIENT_ID }}\n          NUXT_OAUTH_GITHUB_CLIENT_SECRET: ${{ secrets.NUXT_OAUTH_GITHUB_CLIENT_SECRET }}\n          NUXT_COS_SECRET_ID: ${{ secrets.NUXT_COS_SECRET_ID }}\n          NUXT_COS_SECRET_KEY: ${{ secrets.NUXT_COS_SECRET_KEY }}\n          NUXT_COS_BUCKET: ${{ secrets.NUXT_COS_BUCKET }}\n          NUXT_COS_REGION: ${{ secrets.NUXT_COS_REGION }}\n        run: |\n          NODE_OPTIONS=\"--max-old-space-size=4080\" pnpm build\n\n      - name: Pack artifact (flatten .output)\n        run: |\n          rm -rf distpkg && mkdir -p distpkg\n          cp -R .output/* distpkg/\n          cp -f pm2.config.json distpkg/\n          cp -f pm2.preload.cjs distpkg/\n          # Include Drizzle migrations & config for server-side migrate\n          mkdir -p distpkg/lib/drizzle\n          if [ -d lib/drizzle/migrations ]; then cp -R lib/drizzle/migrations distpkg/lib/drizzle/; fi\n          if [ -f drizzle.config.ts ]; then cp -f drizzle.config.ts distpkg/; fi\n          tar -C distpkg -czf artifact.tgz .\n          du -h artifact.tgz\n\n      - name: Prepare SSH key\n        run: |\n          mkdir -p ~/.ssh\n          chmod 700 ~/.ssh\n          echo \"${{ secrets.SSH_PRIVATE_KEY }}\" | tr -d '\\r' > ~/.ssh/id_rsa\n          chmod 600 ~/.ssh/id_rsa\n\n      - name: Upload artifact via scp\n        env:\n          SSH_HOST: ${{ secrets.SSH_HOST }}\n          SSH_USER: ${{ secrets.SSH_USER }}\n          SSH_PORT: ${{ secrets.SSH_PORT }}\n        run: |\n          ssh -o StrictHostKeyChecking=no -p \"${SSH_PORT}\" \"${SSH_USER}@${SSH_HOST}\" \"mkdir -p /root/web/blog\"\n          scp -P \"${SSH_PORT}\" artifact.tgz \"${SSH_USER}@${SSH_HOST}:/root/web/blog/artifact.tgz\"\n\n      - name: Deploy prod & start with PM2\n        env:\n          SSH_HOST: ${{ secrets.SSH_HOST }}\n          SSH_USER: ${{ secrets.SSH_USER }}\n          SSH_PORT: ${{ secrets.SSH_PORT }}\n        run: |\n          ssh -o StrictHostKeyChecking=no -p \"${SSH_PORT}\" \"${SSH_USER}@${SSH_HOST}\" \u003C\u003C 'EOSSH'\n          set -e\n          APP_DIR=/root/web/blog\n          ENVFILE=/root/envs/blog/.env\n          mkdir -p \"$APP_DIR\"\n          cd \"$APP_DIR\"\n          # Clean app dir but keep artifact\n          find \"$APP_DIR\" -mindepth 1 -maxdepth 1 ! -name artifact.tgz -exec rm -rf {} +\n          tar -xzf artifact.tgz -C \"$APP_DIR\"\n          rm -f artifact.tgz\n          # Prefer globally installed dotenv-cli; fallback to npx dotenv-cli; else source fallback\n          if command -v dotenv >/dev/null 2>&1; then\n            DOTENV=\"dotenv -e \\\"$ENVFILE\\\" --\"\n          elif command -v npx >/dev/null 2>&1; then\n            DOTENV=\"npx -y dotenv-cli -e \\\"$ENVFILE\\\" --\"\n          else\n            DOTENV=\"\"\n          fi\n\n          # Apply Drizzle migrations on server\n          if [ -n \"$DOTENV\" ]; then\n            eval \"$DOTENV\" npx -y drizzle-kit@0.31.4 migrate || true\n          else\n            echo \"dotenv-cli not available; using source fallback for migrations\"\n            if [ -f \"$ENVFILE\" ]; then set -a; . \"$ENVFILE\"; set +a; fi\n            if command -v npx >/dev/null 2>&1; then\n              npx -y drizzle-kit@0.31.4 migrate || true\n            else\n              echo \"npx not found; skipping migrations\"\n            fi\n          fi\n\n          # Start cleanly: use prod pm2.config.json\n          pm2 delete Blog >/dev/null 2>&1 || true\n          pm2 start pm2.config.json --update-env\n          pm2 save\n          EOSSH\n\n      - name: Notify (Feishu)\n        if: always()\n        run: |\n          curl -X POST -H \"Content-Type: application/json\" \\\n            -d '{\"msg_type\":\"text\",\"content\":{\"text\":\"'\"${{ github.repository }}\"' - GH canary deploy ['\"'\"${{ job.status }}\"'\"']\"}}' \\\n            \"${{ secrets.NUXT_FEISHU_WEBHOOK }}\"\n\n```\n\n代码中的 `env`，全部要在 `github` 配置一遍\n\n先建一个 `Environment` ，然后在其下配 `secrets` 即可\n\n![](https://img.zzao.club/article/202508261410526.png)\n\n**迁移时，先用一个临时目录进行迁移。测试没问题后再覆盖原来的目录**\n\n使用 `github` 的服务器，打包速度也快了，总流程 **3m20s** 。 用自己的服务器，`5m30s`，快了不少。\n\n主要是在没有大量 IO 的情况下，服务器内存占用就很稳定，下一年也不用再续费 8G 的服务器了\n\n不过趁着内存够用，多上一些应用试试水。\n",{"title":5,"description":1318},"post/nuxt/cloud/use-github-actions-deloy-nuxt-blog",[1326,1327],"博客","Nuxt","jfHOlr_QwSNYXjAIhoh6gRUmSrOtRANdEDZVKgIEGHI",[1330,1334],{"title":1331,"path":1332,"stem":1333},"OpenClaw 安装入门（Windows）","/post/zzao/openclaw/openclaw-install-windows","post/zzao/openclaw/openclaw-install-windows",{"title":1335,"path":1336,"stem":1337},"假设你是AI，你的Skill应该是什么样的","/post/zzao/ai-skill-structure","post/zzao/ai-skill-structure",1779005085305]