<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/">
    <channel>
        <title>个人编程分享</title>
        <link>https://www.anderslane.cn/</link>
        <description>个人技术博客</description>
        <lastBuildDate>Fri, 20 Jun 2025 00:00:00 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <language>zh-Hans</language>
        <copyright>© 2025 Anders Lane</copyright>
        <item>
            <title><![CDATA[用 Docker 多阶段构建部署 Docusaurus 静态站点]]></title>
            <link>https://www.anderslane.cn/deploy-docusaurus-with-docker</link>
            <guid>https://www.anderslane.cn/deploy-docusaurus-with-docker</guid>
            <pubDate>Fri, 20 Jun 2025 00:00:00 GMT</pubDate>
            <description><![CDATA[记录把 Docusaurus 静态站点用 Docker 多阶段构建打包、用 nginx 托管的完整流程，附 Dockerfile 与踩坑经验。]]></description>
            <content:encoded><![CDATA[<p>Docusaurus 构建出来是一堆纯静态文件，部署本质上就是「找个能托管静态文件的服务」。但直接把 build 产物丢到服务器上手动同步，既不优雅也容易出错。用 Docker 把构建和托管封装到一个镜像里，换机器部署只要 <code>docker compose up</code>，省心很多。</p>
<p>下面是我这个博客站点实际的部署方式。</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="整体思路">整体思路<a href="https://www.anderslane.cn/deploy-docusaurus-with-docker#%E6%95%B4%E4%BD%93%E6%80%9D%E8%B7%AF" class="hash-link" aria-label="整体思路的直接链接" title="整体思路的直接链接" translate="no">​</a></h2>
<p>两阶段构建：</p>
<ol>
<li class=""><strong>builder 阶段</strong>：用 <code>node:22-alpine</code> 跑 <code>npm ci &amp;&amp; npm run build</code>，产出 <code>build/</code> 静态文件。</li>
<li class=""><strong>serve 阶段</strong>：把 <code>build/</code> 拷进 <code>nginx:alpine</code>，用 nginx 托管。</li>
</ol>
<p>最终镜像里没有 Node、没有源码、没有 node_modules，只有 nginx + 静态文件，体积大概 50MB 出头。</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="dockerfile">Dockerfile<a href="https://www.anderslane.cn/deploy-docusaurus-with-docker#dockerfile" class="hash-link" aria-label="Dockerfile的直接链接" title="Dockerfile的直接链接" translate="no">​</a></h2>
<div class="language-dockerfile codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-dockerfile codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#393A34"><span class="token plain"># ==================== Stage 1: Build ====================</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">FROM node:22-alpine AS builder</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">WORKDIR /app</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">COPY package.json package-lock.json ./</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">RUN npm ci</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">COPY . .</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">RUN npm run build</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"># ==================== Stage 2: Serve ====================</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">FROM nginx:alpine</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">RUN rm /etc/nginx/conf.d/default.conf</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">COPY nginx.conf /etc/nginx/conf.d/default.conf</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">COPY --from=builder /app/build /usr/share/nginx/html</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">EXPOSE 80</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">CMD ["nginx", "-g", "daemon off;"]</span><br></span></code></pre></div></div>
<p>几个细节：</p>
<ul>
<li class=""><strong>先 <code>COPY package.json package-lock.json</code> 再 <code>npm ci</code>，最后才 <code>COPY . .</code></strong>。这样只要依赖没变，<code>npm ci</code> 那一层就命中缓存，改文章重新构建只要几秒。</li>
<li class="">用 <code>npm ci</code> 而不是 <code>npm install</code>，保证依赖按 lockfile 精确安装，CI 里可复现。</li>
<li class=""><code>package-lock.json</code> 必须提交进仓库，否则 <code>npm ci</code> 直接报错。</li>
</ul>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="nginx-配置要点">nginx 配置要点<a href="https://www.anderslane.cn/deploy-docusaurus-with-docker#nginx-%E9%85%8D%E7%BD%AE%E8%A6%81%E7%82%B9" class="hash-link" aria-label="nginx 配置要点的直接链接" title="nginx 配置要点的直接链接" translate="no">​</a></h2>
<p>Docusaurus 是 SSG（静态站点生成），不是 SPA。这意味着每个路由在 <code>build/</code> 里都有对应的 <code>index.html</code>，<strong>不需要 SPA 那种「所有路径回退到 index.html」的逻辑</strong>。</p>
<div class="language-nginx codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-nginx codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#393A34"><span class="token plain">server {</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    listen 80;</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    server_name _;</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    root /usr/share/nginx/html;</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    index index.html;</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    absolute_redirect off;</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    gzip on;</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    gzip_vary on;</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    gzip_proxied any;</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    gzip_comp_level 6;</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    gzip_types text/plain text/css text/xml application/json</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">               application/javascript application/xml+rss</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">               application/atom+xml image/svg+xml;</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    location / {</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">        try_files $uri $uri/ =404;</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    }</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    location ~* \.(?:js|css|woff2?|ttf|eot|png|jpg|jpeg|gif|ico|svg|webp)$ {</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">        expires 30d;</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">        add_header Cache-Control "public, immutable";</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">        access_log off;</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    }</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    location ~* \.html$ {</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">        add_header Cache-Control "no-cache, no-store, must-revalidate";</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    }</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    error_page 404 /404.html;</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">}</span><br></span></code></pre></div></div>
<p>关键点：</p>
<ul>
<li class=""><strong><code>try_files $uri $uri/ =404</code></strong>：先找精确文件，再找目录（落到 <code>index.html</code>），都没有就 404。千万别写成 <code>try_files $uri $uri/ /index.html</code>，那是 SPA 回退，会让 Docusaurus 的 404 页失效、所有错误路径都返回首页。</li>
<li class=""><strong>HTML 不缓存</strong>：文章更新后能立刻生效；而带 hash 的 JS/CSS 设置长期不可变缓存，再访问直接走浏览器缓存。</li>
<li class=""><strong><code>absolute_redirect off</code></strong>：nginx 目录重定向默认会用绝对地址，开了这个更可控。</li>
<li class=""><strong>gzip</strong>：把 <code>application/atom+xml</code> 也加上，RSS feed 才会被压缩。</li>
</ul>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="踩过的坑">踩过的坑<a href="https://www.anderslane.cn/deploy-docusaurus-with-docker#%E8%B8%A9%E8%BF%87%E7%9A%84%E5%9D%91" class="hash-link" aria-label="踩过的坑的直接链接" title="踩过的坑的直接链接" translate="no">​</a></h2>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="1-npm-ci-报缺-package-lockjson">1. <code>npm ci</code> 报缺 <code>package-lock.json</code><a href="https://www.anderslane.cn/deploy-docusaurus-with-docker#1-npm-ci-%E6%8A%A5%E7%BC%BA-package-lockjson" class="hash-link" aria-label="1-npm-ci-报缺-package-lockjson的直接链接" title="1-npm-ci-报缺-package-lockjson的直接链接" translate="no">​</a></h3>
<p>最初我图省事没提交 lockfile，CI 里 <code>npm ci</code> 直接挂了。<code>npm ci</code> 是严格模式，没有 lockfile 不干活。<strong>lockfile 必须进版本控制。</strong></p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="2-容器里访问-404-但本地-docusaurus-serve-正常">2. 容器里访问 404 但本地 <code>docusaurus serve</code> 正常<a href="https://www.anderslane.cn/deploy-docusaurus-with-docker#2-%E5%AE%B9%E5%99%A8%E9%87%8C%E8%AE%BF%E9%97%AE-404-%E4%BD%86%E6%9C%AC%E5%9C%B0-docusaurus-serve-%E6%AD%A3%E5%B8%B8" class="hash-link" aria-label="2-容器里访问-404-但本地-docusaurus-serve-正常的直接链接" title="2-容器里访问-404-但本地-docusaurus-serve-正常的直接链接" translate="no">​</a></h3>
<p>本地 <code>npm run serve</code> 用的是 Docusaurus 内置服务，带 SPA 回退；而 nginx 是纯静态。如果构建时有 broken link，本地可能看不出来，nginx 下就是 404。所以 <code>docusaurus.config.ts</code> 里我把 <code>onBrokenLinks</code> 设成 <code>'throw'</code>，构建阶段就直接挂掉，不留到部署。</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="3-构建产物路径">3. 构建产物路径<a href="https://www.anderslane.cn/deploy-docusaurus-with-docker#3-%E6%9E%84%E5%BB%BA%E4%BA%A7%E7%89%A9%E8%B7%AF%E5%BE%84" class="hash-link" aria-label="3. 构建产物路径的直接链接" title="3. 构建产物路径的直接链接" translate="no">​</a></h3>
<p>Docusaurus 默认输出到 <code>build/</code>，<code>baseUrl</code> 决定了产物里的子路径。我这个站点 <code>baseUrl: '/'</code>，所以 <code>build/</code> 直接就是 web root，<code>COPY --from=builder /app/build /usr/share/nginx/html</code> 刚好对上。如果 <code>baseUrl</code> 不是 <code>/</code>，nginx 的 <code>root</code> 和 location 都要相应调整。</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="4-前置反代偶发-504">4. 前置反代偶发 504<a href="https://www.anderslane.cn/deploy-docusaurus-with-docker#4-%E5%89%8D%E7%BD%AE%E5%8F%8D%E4%BB%A3%E5%81%B6%E5%8F%91-504" class="hash-link" aria-label="4. 前置反代偶发 504的直接链接" title="4. 前置反代偶发 504的直接链接" translate="no">​</a></h3>
<p>容器跑起来后，外面套一层 nginx-ui 反向代理做 TLS。一开始偶发 504，排查下来是容器 <code>mem_limit</code> 太紧、峰值时 nginx worker 被挤掉，反代侧表现为超时。把内存上限放宽、给静态资源开长缓存之后就好了。反代那侧 <code>proxy_read_timeout</code> 设 30s，足够了。详见我另一篇 <a class="" href="https://www.anderslane.cn/troubleshooting-nginx-502-504">排查 nginx 反向代理 502/504 错误</a>。</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="构建与运行">构建与运行<a href="https://www.anderslane.cn/deploy-docusaurus-with-docker#%E6%9E%84%E5%BB%BA%E4%B8%8E%E8%BF%90%E8%A1%8C" class="hash-link" aria-label="构建与运行的直接链接" title="构建与运行的直接链接" translate="no">​</a></h2>
<div class="language-bash codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-bash codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#393A34"><span class="token plain">docker compose up -d --build</span><br></span></code></pre></div></div>
<p><code>docker-compose.yml</code> 里把容器 80 映射到宿主机 8080，再由 192.168.1.3 上的 nginx-ui 反代到 <code>https://www.anderslane.cn</code>。整条链路：浏览器 → nginx-ui (TLS) → 容器 nginx (静态文件)。</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="小结">小结<a href="https://www.anderslane.cn/deploy-docusaurus-with-docker#%E5%B0%8F%E7%BB%93" class="hash-link" aria-label="小结的直接链接" title="小结的直接链接" translate="no">​</a></h2>
<p>多阶段构建 + nginx 托管静态文件，是 SSG 站点最干净的部署方式之一：构建环境和运行环境彻底分离，镜像小、启动快、无状态。唯一要记住的是 <strong>SSG 不等于 SPA，别用 SPA 的回退逻辑</strong>。</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[排查 nginx 反向代理 502/504 错误]]></title>
            <link>https://www.anderslane.cn/troubleshooting-nginx-502-504</link>
            <guid>https://www.anderslane.cn/troubleshooting-nginx-502-504</guid>
            <pubDate>Thu, 12 Jun 2025 00:00:00 GMT</pubDate>
            <description><![CDATA[一次真实的 nginx 反代偶发 502/504 排查记录，从现象到根因，附排查命令和思路。]]></description>
            <content:encoded><![CDATA[<p>把这个博客部署起来之后，访问偶发 502、504。前端就一个静态站点，后端就一个 nginx 容器，链路很简单，但越是简单的链路，问题越容易被忽略。记一下这次排查的过程。</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="环境">环境<a href="https://www.anderslane.cn/troubleshooting-nginx-502-504#%E7%8E%AF%E5%A2%83" class="hash-link" aria-label="环境的直接链接" title="环境的直接链接" translate="no">​</a></h2>
<ul>
<li class="">浏览器 → <code>192.168.1.3</code> 上的 nginx-ui（反代，终止 TLS）</li>
<li class="">nginx-ui → <code>192.168.1.5:8080</code> 上的容器（nginx 托管静态文件）</li>
</ul>
<p>偶发，不是必现，大概十次里一两次。</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="现象">现象<a href="https://www.anderslane.cn/troubleshooting-nginx-502-504#%E7%8E%B0%E8%B1%A1" class="hash-link" aria-label="现象的直接链接" title="现象的直接链接" translate="no">​</a></h2>
<ul>
<li class="">大部分请求正常 200。</li>
<li class="">偶发 502 Bad Gateway，或 504 Gateway Timeout。</li>
<li class="">刷新一下往往就好了。</li>
</ul>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="排查思路">排查思路<a href="https://www.anderslane.cn/troubleshooting-nginx-502-504#%E6%8E%92%E6%9F%A5%E6%80%9D%E8%B7%AF" class="hash-link" aria-label="排查思路的直接链接" title="排查思路的直接链接" translate="no">​</a></h2>
<p>502/504 的本质是「反代拿不到上游的有效响应」。要么上游没响应（504 超时），要么上游拒绝了连接 / 连接被重置（502）。所以排查分两条线：</p>
<ol>
<li class=""><strong>上游本身是不是活的</strong>——容器 nginx 在不在、监听对不对。</li>
<li class=""><strong>反代到上游的网络通不通</strong>——连接能不能建立、建立后能不能在超时内完成。</li>
</ol>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="第一步确认上游存活">第一步：确认上游存活<a href="https://www.anderslane.cn/troubleshooting-nginx-502-504#%E7%AC%AC%E4%B8%80%E6%AD%A5%E7%A1%AE%E8%AE%A4%E4%B8%8A%E6%B8%B8%E5%AD%98%E6%B4%BB" class="hash-link" aria-label="第一步：确认上游存活的直接链接" title="第一步：确认上游存活的直接链接" translate="no">​</a></h2>
<div class="language-bash codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-bash codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#393A34"><span class="token plain"># 容器状态</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">docker ps | grep www-anderslane</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"># 容器内 nginx 监听</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">docker exec www-anderslane wget -qO- http://localhost:80/ | head -5</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"># 从反代机直接打上游</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">curl -v -H 'Host: www.anderslane.cn' http://192.168.1.5:8080/</span><br></span></code></pre></div></div>
<p>结果：容器在，监听正常，从反代机 <code>curl</code> 上游也 200。说明上游是活的，问题出在「偶发」上。</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="第二步看反代错误日志">第二步：看反代错误日志<a href="https://www.anderslane.cn/troubleshooting-nginx-502-504#%E7%AC%AC%E4%BA%8C%E6%AD%A5%E7%9C%8B%E5%8F%8D%E4%BB%A3%E9%94%99%E8%AF%AF%E6%97%A5%E5%BF%97" class="hash-link" aria-label="第二步：看反代错误日志的直接链接" title="第二步：看反代错误日志的直接链接" translate="no">​</a></h2>
<div class="language-bash codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-bash codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#393A34"><span class="token plain"># nginx-ui 容器的错误日志</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">docker logs &lt;nginx-ui-container&gt; 2&gt;&amp;1 | grep -E 'upstream|502|504'</span><br></span></code></pre></div></div>
<p>抓到关键几行：</p>
<div class="language-text codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-text codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#393A34"><span class="token plain">upstream timed out (110: Connection timed out) while reading response header from upstream</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">upstream prematurely closed connection while reading response header from upstream</span><br></span></code></pre></div></div>
<p>两类错误：</p>
<ul>
<li class=""><code>Connection timed out</code>：TCP 连上了，但上游迟迟不回响应头 → 504。</li>
<li class=""><code>upstream prematurely closed connection</code>：连接建立后被上游主动关了 → 502。</li>
</ul>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="第三步上游资源排查">第三步：上游资源排查<a href="https://www.anderslane.cn/troubleshooting-nginx-502-504#%E7%AC%AC%E4%B8%89%E6%AD%A5%E4%B8%8A%E6%B8%B8%E8%B5%84%E6%BA%90%E6%8E%92%E6%9F%A5" class="hash-link" aria-label="第三步：上游资源排查的直接链接" title="第三步：上游资源排查的直接链接" translate="no">​</a></h2>
<p>容器是静态文件服务，怎么会超时？看容器资源：</p>
<div class="language-bash codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-bash codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#393A34"><span class="token plain">docker stats --no-stream www-anderslane</span><br></span></code></pre></div></div>
<p>发现偶发时段 CPU 飙到接近 100%，内存也接近上限（<code>docker-compose.yml</code> 里限了 256m）。再看进程：</p>
<div class="language-bash codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-bash codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#393A34"><span class="token plain">docker exec www-anderslane ps aux</span><br></span></code></pre></div></div>
<p>nginx worker 之外，多了一堆 <code>wget --spider</code> 进程——那是 healthcheck。</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="根因">根因<a href="https://www.anderslane.cn/troubleshooting-nginx-502-504#%E6%A0%B9%E5%9B%A0" class="hash-link" aria-label="根因的直接链接" title="根因的直接链接" translate="no">​</a></h2>
<p><code>docker-compose.yml</code> 的健康检查：</p>
<div class="language-yaml codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-yaml codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#393A34"><span class="token key atrule" style="color:#00a4db">healthcheck</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token key atrule" style="color:#00a4db">test</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">[</span><span class="token string" style="color:#e3116c">"CMD"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"wget"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"--spider"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"-q"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"http://localhost:80/"</span><span class="token punctuation" style="color:#393A34">]</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token key atrule" style="color:#00a4db">interval</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"> 30s</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token key atrule" style="color:#00a4db">timeout</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"> 5s</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token key atrule" style="color:#00a4db">retries</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">3</span><br></span></code></pre></div></div>
<p><code>wget --spider</code> 在 alpine 上实际会发起请求并解析响应，30s 一次本不算频繁。但 <code>mem_limit: 256m</code> 偏紧，nginx 在高并发或被健康检查挤占时，worker 偶发 OOM 被杀、连接被重置 → 反代侧表现就是 502/504。</p>
<p>两条因素叠加：</p>
<ol>
<li class="">内存上限太紧，峰值时 worker 被杀。</li>
<li class="">健康检查和正常请求抢资源，放大了峰值。</li>
</ol>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="处理">处理<a href="https://www.anderslane.cn/troubleshooting-nginx-502-504#%E5%A4%84%E7%90%86" class="hash-link" aria-label="处理的直接链接" title="处理的直接链接" translate="no">​</a></h2>
<ol>
<li class=""><strong>放宽内存上限</strong>到 512m，给 nginx worker 留余量。</li>
<li class=""><strong>健康检查换成更轻的探测</strong>，直接打一个静态小文件，少解析。</li>
<li class=""><strong>反代侧 <code>proxy_read_timeout</code> 设 30s</strong>（本来就是），<code>proxy_connect_timeout</code> 设 5s 快速失败。</li>
<li class="">给静态资源开 <code>expires</code> 长缓存，减少回源压力。</li>
</ol>
<p>改完观察两天，502/504 消失。</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="复盘">复盘<a href="https://www.anderslane.cn/troubleshooting-nginx-502-504#%E5%A4%8D%E7%9B%98" class="hash-link" aria-label="复盘的直接链接" title="复盘的直接链接" translate="no">​</a></h2>
<p>几条经验：</p>
<ul>
<li class=""><strong>偶发 502/504 先看上游资源</strong>，别一上来就调反代超时。超时只是症状，根因往往是上游扛不住。</li>
<li class=""><strong>容器资源限制要留余量</strong>，256m 对 alpine + nginx 平时够用，但峰值（OOM 边缘）会偶发杀进程，很难复现。</li>
<li class=""><strong>健康检查本身也是负载</strong>，在资源紧张的容器里别用重检查。</li>
<li class=""><strong>日志是最快的线索</strong>：<code>upstream timed out</code> vs <code>prematurely closed</code> 区分了超时和重置，直接指向不同方向。</li>
</ul>
<p>排查这类问题的顺序我习惯是：<strong>上游存活 → 上游资源 → 网络/超时 → 反代配置</strong>。前两步排掉，剩下的基本就是配置调优了。</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[为什么我选择 Docusaurus 写技术博客]]></title>
            <link>https://www.anderslane.cn/why-docusaurus</link>
            <guid>https://www.anderslane.cn/why-docusaurus</guid>
            <pubDate>Wed, 28 May 2025 00:00:00 GMT</pubDate>
            <description><![CDATA[对比 Hugo、Astro、Docusaurus 三个 SSG，讲讲我为什么最后选了 Docusaurus 写技术博客。]]></description>
            <content:encoded><![CDATA[<p>折腾技术博客这事我干过好几轮了。最早用 WordPress，后来用 Hexo，中间还试过自己拿 Go 写一个。每次都是写了几篇就荒废，原因不外乎：要么写起来麻烦，要么主题丑，要么改起来痛。</p>
<p>这次重新搭博客，我认真对比了当下三个主流的静态站点生成器（SSG）：Hugo、Astro、Docusaurus。结论是选了 Docusaurus。不是因为它最好，而是因为它最契合我「写技术博客」这个具体场景。</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="三个候选">三个候选<a href="https://www.anderslane.cn/why-docusaurus#%E4%B8%89%E4%B8%AA%E5%80%99%E9%80%89" class="hash-link" aria-label="三个候选的直接链接" title="三个候选的直接链接" translate="no">​</a></h2>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="hugo">Hugo<a href="https://www.anderslane.cn/why-docusaurus#hugo" class="hash-link" aria-label="Hugo的直接链接" title="Hugo的直接链接" translate="no">​</a></h3>
<p>Go 写的，主打一个快。构建几千篇文章只要几百毫秒，单二进制部署，没有依赖地狱。</p>
<ul>
<li class="">优点：构建飞快，单文件，主题生态成熟。</li>
<li class="">缺点：模板是 Go template，写复杂逻辑很拧巴；默认主题多偏「文档站」或老派博客；要做出自己想要的样子，改主题的成本不低。</li>
</ul>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="astro">Astro<a href="https://www.anderslane.cn/why-docusaurus#astro" class="hash-link" aria-label="Astro的直接链接" title="Astro的直接链接" translate="no">​</a></h3>
<p>新生代，主打 Islands Architecture，默认尽量少发 JS 到前端。可以混用 React / Vue / Svelte。</p>
<ul>
<li class="">优点：现代、灵活，性能好，组件化，能做出很精致的页面。</li>
<li class="">缺点：对「纯文字博客」来说有点重；要配置的东西多；生态相对新，内容插件（比如 RSS、归档）要自己搭。</li>
</ul>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="docusaurus">Docusaurus<a href="https://www.anderslane.cn/why-docusaurus#docusaurus" class="hash-link" aria-label="Docusaurus的直接链接" title="Docusaurus的直接链接" translate="no">​</a></h3>
<p>React + MDX，Meta 出品。原本定位是文档站，但 blog 插件很完整。</p>
<ul>
<li class="">优点：MDX 写作、代码块高亮开箱即用、RSS / 归档 / TOC 都现成、React 生态可复用、TypeScript 支持好。</li>
<li class="">缺点：构建产物带一定量 JS（比 Hugo 重）；定位偏文档，做博客要稍微「裁掉」一些默认元素。</li>
</ul>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="我的取舍">我的取舍<a href="https://www.anderslane.cn/why-docusaurus#%E6%88%91%E7%9A%84%E5%8F%96%E8%88%8D" class="hash-link" aria-label="我的取舍的直接链接" title="我的取舍的直接链接" translate="no">​</a></h2>
<p>我的需求很具体：</p>
<ol>
<li class=""><strong>以文章时间流为主</strong>，不要分类树、不要复杂的文档结构。</li>
<li class=""><strong>写技术文章</strong>，代码块、TOC、MDX 组件是高频需求。</li>
<li class=""><strong>能精细控制主题</strong>，想要一个克制的极简风，而不是套现成主题。</li>
<li class=""><strong>TypeScript + React</strong>，跟我日常技术栈一致，改起来顺手。</li>
</ol>
<p>逐条对：</p>
<ul>
<li class="">需求 1：三个都能做。Hugo 默认就是博客起家；Astro 和 Docusaurus 都能裁成纯时间流。</li>
<li class="">需求 2：<strong>Docusaurus 完胜</strong>。MDX + Prism 代码高亮 + 自动 TOC + 阅读体验，开箱即用。Hugo 要自己配高亮和 shortcodes，Astro 要自己接插件。</li>
<li class="">需求 3：三个都能改主题，但 Docusaurus 的 Infima 体系 + CSS 变量改起来很线性，覆盖一个 <code>custom.css</code> 就能大改。Hugo 主题是一大坨模板，魔改成本高。Astro 灵活但要自己搭整套布局。</li>
<li class="">需求 4：只有 Docusaurus 是 React / TS 原生。Hugo 是 Go template，Astro 虽支持 React 但本体是 <code>.astro</code>。</li>
</ul>
<p>所以选 Docusaurus。代价是构建产物比 Hugo 重一点、带些 JS，但对我来说这点体积完全可接受——技术博客又不追求极致 LCP，可读性才是第一位。</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="实际怎么裁">实际怎么裁<a href="https://www.anderslane.cn/why-docusaurus#%E5%AE%9E%E9%99%85%E6%80%8E%E4%B9%88%E8%A3%81" class="hash-link" aria-label="实际怎么裁的直接链接" title="实际怎么裁的直接链接" translate="no">​</a></h2>
<p>Docusaurus 默认是个「文档站 + 博客」的形态，要做纯博客得裁掉一些东西：</p>
<ul>
<li class=""><strong>关掉 docs 插件</strong>：<code>preset-classic</code> 里 <code>docs: false</code>，只留 <code>blog</code>。</li>
<li class=""><strong>博客当首页</strong>：blog 插件设 <code>routeBasePath: '/'</code>，主页直接是文章列表。</li>
<li class=""><strong>去掉标签</strong>：我个人不喜欢标签云，文章不写 tags，保持纯时间流。</li>
<li class=""><strong>保留归档和 RSS</strong>：这两个是博客的基本盘，Docusaurus blog 插件默认就给。</li>
<li class=""><strong>极简主题</strong>：覆盖 <code>custom.css</code>，黑白为主 + 单一强调色，正文限宽 680px，行高 1.7，参考 overreacted.io 那种阅读体验。</li>
</ul>
<p>裁完之后，它就是一个干净的博客，没有文档站的 sidebar、没有 hero banner、没有多余卡片。</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="会再换吗">会再换吗<a href="https://www.anderslane.cn/why-docusaurus#%E4%BC%9A%E5%86%8D%E6%8D%A2%E5%90%97" class="hash-link" aria-label="会再换吗的直接链接" title="会再换吗的直接链接" translate="no">​</a></h2>
<p>大概率不会。SSG 这东西，迁移成本主要在内容和主题。内容是 Markdown / MDX，可移植；主题是自己写的 CSS，也清楚。真要换，文章搬走不难。但 Docusaurus 目前满足我所有需求，没有换的理由。</p>
<p>选工具的真相是：<strong>没有最好的，只有最契合当前场景的</strong>。对我这个「会 React / TS、想精细控制样式、写技术文章为主」的人来说，Docusaurus 就是当前的最优解。</p>]]></content:encoded>
        </item>
    </channel>
</rss>