<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>Nginx on 老朱的赛博工位</title><link>https://blog.zjkun.top/tags/nginx/</link><description>Recent content in Nginx on 老朱的赛博工位</description><generator>Hugo</generator><language>zh-CN</language><lastBuildDate>Wed, 21 Oct 2026 23:52:00 +0800</lastBuildDate><atom:link href="https://blog.zjkun.top/tags/nginx/index.xml" rel="self" type="application/rss+xml"/><item><title>Docker 部署实战：一台 99 元 ECS 上的挂载、容器命名与 compose 行为总结</title><link>https://blog.zjkun.top/posts/docker-deploy-in-practice/</link><pubDate>Wed, 21 Oct 2026 23:52:00 +0800</pubDate><guid>https://blog.zjkun.top/posts/docker-deploy-in-practice/</guid><description>99 元/年阿里云 ECS 上 nginx + node-app 双容器跑起来之后的一天：按容器重组目录、给容器改名、固化部署流水线、新域名加限流。把 bind mount 两套门牌号、container_name 与网络别名的区别、compose up 不重建的坑、反代转发头这些概念一次讲透，全部基于当天实操记录。</description><content:encoded><![CDATA[<p>上一篇讲了 99 元 ECS 从装 Docker 到上线的踩坑单（见《99 元 ECS 上线踩坑单》），这篇讲点更底层的东西：<strong>容器跑起来之后</strong>，围绕 compose 文件、挂载、命名、重建行为发生的四件事。素材全是 10 月 21 日当晚的操作记录，改了什么、为什么改、怎么验收，都有据可查。</p>
<p>机器现状先交代：2 核 2G，<code>/root/app</code> 下 docker compose 起两个容器——<code>nginx</code>（80/443，反代 + 静态博客）和 <code>node-app</code>（Nuxt3 后端，容器内 3000）。</p>
<h2 id="一bind-mount冒号左右是两个世界">一、bind mount：冒号左右是两个世界</h2>
<p>compose 里最常被我忽略又最重要的一行：</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-yaml" data-lang="yaml"><span style="display:flex;"><span><span style="color:#f92672">volumes</span>:
</span></span><span style="display:flex;"><span>  - <span style="color:#ae81ff">./nginx/conf.d:/etc/nginx/conf.d:ro</span>
</span></span></code></pre></div><p><strong>冒号左边是宿主机路径（SSH 里 ls 得到的），冒号右边是容器内路径（docker exec 进去才存在的）。</strong> <code>/etc/nginx/conf.d</code> 这个目录在服务器上根本不存在——它只活在 nginx 容器那个独立的小文件系统里，物理内容就是宿主机的 <code>/root/app/nginx/conf.d</code> 被 bind mount 投射进去。同一块磁盘，两套门牌号。</p>
<p>nginx 配置里写 <code>root /usr/share/nginx/blog</code> 时，进程是按<strong>容器视角</strong>找文件，找到的实际是宿主机的博客产物目录。这解释了一个我此前觉得神奇的现象：<strong>scp 上传新博客后不用重启任何东西，刷新就是新内容</strong>——nginx 每次请求都现去容器内路径读文件，而那个路径实时映射到刚落盘的宿主机目录，中间没有任何复制或缓存环节。</p>
<p>两个配套习惯：</p>
<ul>
<li>挂载尾部加 <code>:ro</code>（只读）。我的证书目录、博客目录、conf.d 全是 ro——容器最多读到，写不了，误操作被内核直接挡下。</li>
<li>想确认映射关系，别猜，一条命令看全量表：</li>
</ul>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>docker inspect nginx --format <span style="color:#e6db74">&#39;{{range .Mounts}}{{.Source}} -&gt; {{.Destination}}{{&#34;\n&#34;}}{{end}}&#39;</span>
</span></span></code></pre></div><h2 id="二目录重组按容器分家顺手收一个安全事故">二、目录重组：按容器分家，顺手收一个安全事故</h2>
<p>当晚 22 点把 <code>/root/app</code> 从&quot;一锅炖&quot;重组成按容器分目录：<code>nginx/</code>（conf.d、blog、certbot-www、certbot）、<code>node-app/</code>（构建产物 output）、<code>scripts/</code>、<code>archive/</code>。动机一半是整洁，一半是安全——<strong>node-app 原来的挂载是 <code>.:/app</code>，等于把整个 /root/app 塞进容器，包括存着数据库密码和各种 API key 的 <code>.env</code></strong>。收窄成 <code>./node-app:/app</code> 后，容器只看得到自己的代码。</p>
<p>改挂载路径的连锁清单，少一个就断：compose 里 nginx 四个挂载同步改 <code>./nginx/*</code>；证书续期 cron 指向新位置的 <code>certbot_renew.sh</code>；脚本内的证书/webroot 路径 sed 换新前缀；本地部署脚本的 scp 目标目录也要改。</p>
<p>动手前留了两条退路：77M 全量 tar 到 <code>/root/backup/</code>，旧 compose 存进 <code>archive/</code>。验收不看&quot;容器 Up&quot;就完事，而是逐条打流量：IP:80 = 200、博客 https = 200、需鉴权接口 = 401（说明反代通了且鉴权逻辑正常）、检索预热日志正常。重组这种全量挪目录的操作，值得一次全量备份，成本 77M，买的是&quot;改崩了五分钟回滚&quot;。</p>
<h2 id="三容器命名服务名container_name网络别名是三回事">三、容器命名：服务名、container_name、网络别名是三回事</h2>
<p>compose 不写 <code>container_name</code> 时，容器默认名是 <code>&lt;项目&gt;-&lt;服务&gt;-&lt;序号&gt;</code>——项目名取目录名，所以我那台机器上 nginx 容器叫 <code>app-nginx-1</code>。能跑，但丑，而且脚本里引用它就得背下这个拼装规则。</p>
<p>修法就是显式钉死：</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-yaml" data-lang="yaml"><span style="display:flex;"><span><span style="color:#f92672">services</span>:
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">nginx</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">container_name</span>: <span style="color:#ae81ff">nginx  </span> <span style="color:#75715e"># docker ps 里就叫 nginx</span>
</span></span></code></pre></div><p>当晚改名踩到的最有价值的坑在这里：<strong>容器名会被硬编码进外围脚本</strong>。nginx 从 <code>app-nginx-1</code> 改成 <code>nginx</code> 之前，先 grep 了全服务器的引用——果然在证书续期脚本里躺着一行 <code>docker exec app-nginx-1 nginx -s reload</code>。不同步改掉，下次 cron 续期就静默失败。所以我的动作顺序是：备份 compose → 加 container_name → 全局 grep 旧容器名逐个改 → <code>docker compose up -d nginx</code>（改 compose 本身会触发重建，无需 force）→ 三站点 200 验收。</p>
<p>另一个容易混的点：<strong>容器间互相访问用的是服务名，不是容器名</strong>。nginx 反代配置里写 <code>proxy_pass http://node-app:3000</code>，这个 <code>node-app</code> 是 services 下的键名，compose 网络会自动把它解析成容器 IP；<code>container_name</code> 只影响 <code>docker exec</code>/<code>docker ps</code> 这些宿主机动词。分清这两层，改名才改得不慌。</p>
<h2 id="四compose-up-的懒得动行为改了代码不等于改了容器">四、compose up 的&quot;懒得动&quot;行为：改了代码不等于改了容器</h2>
<p>固化部署流水线（本地构建 → tar → scp → 服务器解压 → <code>docker compose up -d --force-recreate node-app</code>）时撞的坑：不加 <code>--force-recreate</code> 的 <code>up -d</code>，如果 compose 配置本身没变，它看到容器已经是 Running 就直接跳过——<strong>新代码解压在挂载目录里，但容器内存里跑的还是旧进程</strong>。表现是页面 title 改了、线上没变化，一度以为构建出错。</p>
<ul>
<li>挂载卷改&quot;文件内容&quot;（博客 html、配置）：不用动容器，实时生效；</li>
<li>改 compose 配置（镜像/环境变量/挂载）：<code>up -d</code> 会自动重建对应容器；</li>
<li>改了代码要重启进程但 compose 没变：<code>up -d --force-recreate</code> 或 <code>restart</code>。</li>
</ul>
<p>三种&quot;变更&quot;对应三种生效方式，混着记就乱。我的流程里干脆把 <code>--force-recreate</code> 写死进部署脚本，并在同一步里抓线上 HTML 的 <code>&lt;title&gt;</code> 和探一个鉴权接口返回 401，作为&quot;新构建真的在服务&quot;的证据——entry hash 变了才算数。</p>
<h2 id="五反代与限流两域名一台机的最终形态">五、反代与限流：两域名一台机的最终形态</h2>
<p>22 点半给 node 应用单独开了 <code>node.zjkun.top</code>，A 记录指到同一台 ECS，nginx 按 Host 分流。新域名的 80 只留 ACME 验证路径、其余 301 上 https；443 反代 <code>node-app:3000</code>。几个关键配置都有明确理由：</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-nginx" data-lang="nginx"><span style="display:flex;"><span><span style="color:#66d9ef">proxy_http_version</span> <span style="color:#ae81ff">1</span><span style="color:#e6db74">.1</span>;
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">proxy_set_header</span> <span style="color:#e6db74">X-Real-IP</span> $remote_addr;
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">proxy_set_header</span> <span style="color:#e6db74">X-Forwarded-For</span> $proxy_add_x_forwarded_for;
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">proxy_set_header</span> <span style="color:#e6db74">X-Forwarded-Proto</span> $scheme;
</span></span></code></pre></div><p>反代天然会把访客身份弄丢——后端日志里只见 docker 网关 IP、生成的回调 URL 拼成 http。这四行就是把真实 IP 和协议原样递进去。</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-nginx" data-lang="nginx"><span style="display:flex;"><span><span style="color:#66d9ef">proxy_read_timeout</span> <span style="color:#e6db74">300s</span>;
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">proxy_buffering</span> <span style="color:#66d9ef">off</span>;
</span></span></code></pre></div><p>这两行是为 SSE 流式接口准备的：默认 60 秒收不到字节 nginx 就掐线报 504，AI 应用动辄生成一两分钟；默认攒块转发则打字机效果出不来，关缓冲来一点推一点。</p>
<p>限流用两个 <code>limit_req</code> zone：登录类接口（正则覆盖 <code>/api/login</code>、注册、token 等）1r/s、burst=5，防爆破；全站 10r/s、burst=20、nodelay，兜刷量；<code>limit_req_status 429</code>。实测连打 12 发登录，第 6 发起全部 429，同时首页照常 200——严区和宽区各管各的。证书方面新域名由既有的每日 04:30 certbot cron 自动覆盖（renew 本来就扫全部证书），不用新加任务。</p>
<h2 id="验收清单当晚全绿">验收清单（当晚全绿）</h2>
<table>
	<thead>
			<tr>
					<th>项</th>
					<th>结果</th>
			</tr>
	</thead>
	<tbody>
			<tr>
					<td>容器</td>
					<td><code>nginx</code>、<code>node-app</code> 双 Up，命名已钉死</td>
			</tr>
			<tr>
					<td>IP:80 / 博客 https / node https</td>
					<td>全部 200</td>
			</tr>
			<tr>
					<td>需鉴权接口</td>
					<td>401（反代通、鉴权在位）</td>
			</tr>
			<tr>
					<td>限流</td>
					<td>登录连打 12 发，第 6 发起 429</td>
			</tr>
			<tr>
					<td>引用同步</td>
					<td>续期脚本、cron、本地部署脚本无残留旧路径/旧容器名</td>
			</tr>
			<tr>
					<td>回滚</td>
					<td>全量 tar + 旧 compose 存档</td>
			</tr>
	</tbody>
</table>
<p>一天下来最大的体会：<strong>Docker 的很多&quot;玄学&quot;，拆开都是三层问题——文件在哪（挂载的两套门牌号）、谁在引用它（硬编码的容器名和路径）、变更怎么生效（文件级/配置级/进程级）。</strong> 改前先备份、grep 引用，改后按流量验收，基本就不会翻车。</p>
<hr>
<p><em>本文基于 2026-10-21 当晚服务器实操记录整理，操作明细与回滚存档位置见文中对应小节。</em></p>
]]></content:encoded></item></channel></rss>