本页目录

Linux、Docker 与 HTTPS:交付一个可维护的入口

理解 Linux 进程、容器、反向代理、健康检查与密钥边界,并在本机运行完整的代理部署练习。

L2 · 能交付约 20 分钟阅读含示例、练习与验收

建议先读:AI 应用安全:让模型只能建议被允许的动作

本页内容

用途、范围与前置#

部署的目标是让用户稳定访问服务,并让你能知道运行的是哪个版本、为什么失败、如何替换。本章适用于单机后端和中小规模 AI 应用的起步交付。L2 要求是独立解释端口、容器网络、进程生命周期、TLS 终止、健康检查和持久化,不要求立即掌握 Kubernetes。

前置是 Node HTTP 服务、环境变量和基本终端操作。本章完整练习运行在 Linux 容器 中:Linux 主机安装 Docker Engine 与 Compose 插件;Windows 可使用切换到 Linux containers 的 Docker Desktop。需要能拉取公开镜像;示例不会申请域名、配置云账号或调用付费 API。实际公网 HTTPS 的条件单独说明,不能把本机代理测试当作云部署已完成。

先把 Linux 的运行概念对齐#

你在终端执行 node app.mjs,启动的是进程。进程有用户身份、工作目录、环境变量、文件描述符和退出码。终端窗口关闭、进程崩溃、服务器重启,都可能让服务消失。生产需要进程托管;本章由容器运行时负责启动与重启,而不是靠 SSH 窗口常开。

文件权限决定进程能读写什么。容器内 root 仍应慎用,尤其不要把宿主机管理目录或 Docker socket 随意挂进应用。程序代码适合放进不可变镜像;上传、数据库和证书状态适合专门持久化。容器删掉重建很常见,写在容器可写层的数据不能视作可靠存储。

端口有三层:应用监听端口、容器网络端口、宿主机发布端口。容器里的 localhost 只指向本容器,不是另一服务,也不是宿主机。应用要让代理容器访问,需要监听 0.0.0.0,并通过 Compose 服务名互相连接。只在浏览器访问失败时修改 CORS,通常解决不了网络地址写错的问题。

镜像、容器与代理职责#

镜像像一次构建后的制品,容器是制品启动后的实例。Dockerfile 描述如何构建;Compose 描述服务怎么连接、配置和挂载。不要在运行中的容器里手工改源码作为发布方式,否则重建就丢失且无法追溯。示例镜像标签方便学习,生产应记录验证过的镜像摘要与版本。

反向代理接收公开连接,把请求转给内部服务。它承担 HTTPS、入口日志和路由,应用处理业务鉴权、输入校验和调用模型。代理转发的 IP、协议头只应在来自可信代理时被信任,否则客户端可以伪造。浏览器中的 API key 永远不是秘密,供应商密钥只能由后端持有。

健康检查分存活和就绪:进程是否响应是一回事,是否可以承接业务是另一回事。检查不应每次都调用昂贵模型,否则健康探测会制造费用。数据库暂时不可用时可以让就绪失败,但不要让存活检查触发无限重启。Compose 会标记 unhealthy;普通健康检查本身不会保证自动重启不健康容器,restart 策略主要针对进程退出。Compose 服务语义

完整本机示例:应用容器加代理容器#

建立一个空目录,保存下面四个完整文件。先安装对应系统的 Docker 与 Compose,执行 docker version 和 docker compose version 确认客户端、服务端和插件均可用。示例无需 npm 依赖。

app.mjs:

javascript
import http from "node:http";
const server = http.createServer((req, res) => {
  if (req.url !== "/healthz") {
    res.writeHead(404, { "content-type": "application/json" });
    res.end(JSON.stringify({ error: "NOT_FOUND" }));
    return;
  }
  res.writeHead(200, { "content-type": "application/json" });
  res.end(JSON.stringify({ ok: true, service: "ai-demo" }));
});
server.listen(3000, "0.0.0.0");
// 容器停止时给在途请求留出有限时间
process.on("SIGTERM", () => {
  server.close(() => process.exit(0));
  setTimeout(() => process.exit(1), 5000).unref();
});

Dockerfile:

dockerfile
FROM node:22-alpine
WORKDIR /app
COPY --chown=node:node app.mjs ./
USER node
EXPOSE 3000
CMD ["node", "app.mjs"]

Caddyfile:

text
:80 {
  reverse_proxy app:3000
}

compose.yaml:

yaml
services:
  app:
    build: .
    restart: unless-stopped
    read_only: true
    init: true
    healthcheck:
      test: ["CMD", "node", "-e", "fetch('http://127.0.0.1:3000/healthz').then(r=>process.exit(r.ok?0:1)).catch(()=>process.exit(1))"]
      interval: 10s
      timeout: 3s
      retries: 3
    # 应用只在容器网络可见,没有向宿主机发布端口
  proxy:
    image: caddy:2-alpine
    restart: unless-stopped
    ports:
      - "127.0.0.1:8080:80"
    volumes:
      - ./Caddyfile:/etc/caddy/Caddyfile:ro
    depends_on:
      app:
        condition: service_healthy

另存 .dockerignore,防止构建上下文把无关内容发送给构建器:

text
.git
node_modules
.env
.env.*
secrets/
*.pem
*.key

在这些文件所在目录执行:

bash
docker compose config
docker compose up -d --build
docker compose ps
curl http://127.0.0.1:8080/healthz
docker compose logs --tail 30 app proxy

Windows PowerShell 若 curl 被映射成其他命令,使用 curl.exe。预期响应为 {"ok":true,"service":"ai-demo"};Compose 中 app 最终为 healthy,proxy 处于运行状态。第一次拉镜像需要网络和时间。停止练习使用 docker compose down;此练习没有用户数据库,也没有业务持久化卷。

逐段解析:应用只提供一个健康路由,避免把教学示例伪装成完整 AI 后端;非健康路径明确返回 404。Dockerfile 使用非 root 用户和直接 exec 形式 CMD,让停止信号能到达应用。read_only 禁止应用任意改容器根文件系统;后续需要临时文件时应显式增加受限 tmpfs 或工作卷。代理通过 app:3000 寻址,宿主机只暴露回环地址的 8080,所以局域网其他机器不能直接访问。

healthcheck 的 interval 是检查周期,timeout 是单次时限,retries 是连续失败阈值。depends_on 的 service_healthy 只解决启动依赖等待,不代表运行中依赖故障会自动恢复业务,也不代表集群调度。应用自己仍需处理依赖超时和错误。

迁移到公网 HTTPS 的具体边界#

生产需要你拥有并可控制的域名、正确 DNS、入口网络与防火墙策略,以及能持续保存证书状态的目录。Caddy 在合适域名和挑战条件下自动申请及续期证书;是否成功必须查看实际证书与日志,不能只看配置文件存在。HTTPS 条件

下面只是生产替换片段,不是另一份可直接运行的完整 Compose。将 Caddyfile 的 :80 改成真实域名,例如 app.example.com;把 proxy.ports 改成 80:80 和 443:443;为 Caddy 的 /data 和 /config 添加持久化卷,并在顶层声明卷。域名示例必须替换为自己的域名,不能照抄申请证书。部署后确认 HTTP 重定向、HTTPS 证书域名、续期状态和实际公网访问。

模型密钥不要 COPY 进镜像、不要写到前端构建变量。可以通过运行时密钥服务或 Compose secrets 文件挂载给指定服务,并让应用读取 /run/secrets 下的文件。Compose 文件式 secret 仍依赖宿主机文件保护,不自动等于加密密钥管理平台;密钥文件权限、备份范围和轮换必须设计。Compose secrets

AI 流式输出还要确认代理没有把内容攒到结束才发送。使用实际 SSE 接口测首内容时间和中断行为;不要把代理超时设置得无限长来掩盖业务卡死。后端要在浏览器断开时传播取消,并设置独立的上游总时限。

常见错误与排障#

502 通常从代理到应用这段查起:查看 app 是否 healthy、监听地址是否 0.0.0.0、服务名和端口是否一致。容器启动后立刻退出,查看 logs 和退出码,而不是无限重建。证书申请失败时查 DNS 的 A 与 AAAA、端口可达性、挑战日志和系统时间。

“本机能访问,服务器不能”需要区分回环地址、防火墙和发布端口。本例故意绑定 127.0.0.1;公开部署需要明确改动并重新核对暴露面。不要为省事发布数据库端口或关闭 TLS 证书校验。

把一个 HTTP 请求沿部署链路走完整#

理解部署时,最有效的方法不是背命令,而是跟踪一条请求。浏览器先把域名解析成地址,与服务器建立 TCP 和 TLS 连接,代理根据域名及路径选择上游,应用验证身份并执行业务,随后把响应沿原路返回。任何一层失败都会表现成“页面打不开”,但责任和修复方式完全不同。

域名解析错误时,请求可能根本没有到达你的服务器。TLS 证书域名不匹配时,浏览器在业务 HTTP 之前就会拒绝连接。代理返回五百零二时,公开入口一般已经工作,问题更可能在上游连接或响应。应用返回四百零三时,继续改服务器防火墙通常没有意义,应该检查身份、权限和业务策略。

不要把 localhost 当成一个全局名字。Windows 浏览器里的回环地址指宿主系统;Linux 容器里的回环地址指容器自己的网络空间;远程服务器浏览器或终端里的回环地址又指那台服务器。你在不同位置执行 curl,相当于从链路不同节点发起探测,必须记录命令执行位置,否则“我这里通了”缺少解释价值。

Docker 的服务名解析只在对应容器网络中生效,宿主浏览器通常不能直接访问 http://app:3000。反向代理容器通过 app 找服务,浏览器通过宿主发布的端口找代理,这两条连接不共享同一个名字体系。将应用监听地址从 127.0.0.1 改为 0.0.0.0,只是让容器网络接口能够接受连接;是否向公网开放仍由发布端口及网络策略决定。

配置不是越多越安全,要理解它改变了什么#

第一个 Dockerfile 的 FROM 选择基础运行环境,标签方便阅读但可能被更新,因此可重复生产发布应记录镜像摘要。WORKDIR 同时影响后续构建命令和程序相对路径,不能假设应用总从仓库根目录启动。COPY 只复制需要的文件,既减少镜像内容,也使构建缓存失效范围更明确。

USER node 让业务进程使用普通用户。它减少误操作范围,但不代替宿主机隔离,也不意味着所有挂载文件自动可读。宿主目录若归其他用户且权限过窄,容器会收到 EACCES;修复应针对目标文件的所有者和所需权限,而不是递归赋予所有人写权限。Windows 与 Linux 文件权限表现不同,交付前要在目标 Linux 环境核对。

CMD 的数组形式直接运行进程,避免额外 shell 改变信号转发和参数解释。EXPOSE 只是镜像提供端口信息,既不修改应用的监听端口,也不代替 Compose 的 ports。init: true 让容器使用一个小型初始化进程帮助转发信号并回收子进程;如果应用会启动文档转换器等子进程,这比只看主 Node 进程更重要。

restart 未设置时默认不自动重启;unless-stopped 适合需要持续运行的本机服务,但人为停止应被尊重。它不是业务恢复算法,错误配置导致程序持续退出时,只会不断重启。read_only 默认为可写根文件系统,设为 true 后程序应把必要临时写入放到明确挂载路径。init 默认不开启,因此教材显式写出意图。

资源限制是为了让一个异常请求不能把整台机器拖垮。mem_limit 限制容器可用内存,cpus 限制 CPU 使用份额对应的配额,pids_limit 限制进程数量。过小的内存限制会让 Node 或解析器被终止,表现成连接突然断开;这时需要结合容器状态和内存记录判断,不能简单归为模型网络不稳定。

cap_drop: [ALL] 删除应用不需要的 Linux capabilities,no-new-privileges 阻止进程通过某些机制获得新特权。它们不能补救把 Docker socket 或宿主敏感目录挂进去的设计。安全配置应与实际任务验证:只读查询服务一般不需要管理网络设备;文档转换服务可能需要额外工作目录,但也不因此需要完整特权容器。

日志同样占磁盘。json-file 驱动配合 max-size 和 max-file 可以限制单容器日志轮转规模,避免持续错误把宿主磁盘写满。轮转不是集中日志保存,更不是备份;你还需要决定哪些事件送到受控日志系统、保留多久。日志一旦塞进完整提示词或密钥,轮转也不会解决泄漏问题。

第二个完整例子:启动就绪与优雅退出#

建立独立目录 readiness-demo,保存以下 app.mjs。它不调用数据库或模型,而是用可配置启动延迟模拟“进程已启动但依赖尚未就绪”。这比始终返回成功的健康接口更接近真实应用生命周期。

javascript
// app.mjs:Node.js 22,无第三方依赖
import http from "node:http";
function parseStartupDelay(raw) {
  if (raw === undefined) return 1500; // 仅缺失变量使用默认值,空字符串是配置错误。
  if (typeof raw !== "string" || raw.length === 0 || /[^0-9]/u.test(raw)) {
    throw new Error("STARTUP_DELAY_MS 必须是不含空白或符号的十进制整数字符串");
  }
  const value = Number(raw); // 先验证原文,拒绝 Number 能转换的十六进制、空白等写法。
  if (!Number.isInteger(value) || value > 30000) {
    throw new Error("STARTUP_DELAY_MS 必须在 0 到 30000 之间");
  }
  return value;
}
const delay = parseStartupDelay(process.env.STARTUP_DELAY_MS);
if (process.argv.includes("--check-config")) {
  console.log(JSON.stringify({ startupDelayMs: delay }));
  process.exit(0); // 配置检查模式到此结束,不创建定时器,也不监听端口。
}
let ready = false;
let stopping = false;
setTimeout(() => { if (!stopping) ready = true; }, delay);
const server = http.createServer((req, res) => {
  const path = new URL(req.url, "http://localhost").pathname;
  const send = (status, data) => {
    res.writeHead(status, { "content-type": "application/json" });
    res.end(JSON.stringify(data));
  };
  if (path === "/livez") return send(200, { alive: true });
  if (path === "/readyz") return send(ready ? 200 : 503, { ready });
  if (path === "/version") return send(200, { version: "readiness-demo-v1" });
  if (!ready) return send(503, { error: "NOT_READY" });
  return send(404, { error: "NOT_FOUND" });
});
server.listen(3000, "0.0.0.0");
process.on("SIGTERM", () => {
  stopping = true;
  ready = false; // 先停止接收新业务,再排空已有连接
  server.close(() => process.exit(0));
  setTimeout(() => process.exit(1), 5000).unref();
});

在同一目录保存第一个例子的 Dockerfile 和 Caddyfile,再保存下面完整 compose.yaml。它使用 8081,避免与前面的练习端口冲突;不需要任何真实密钥。

yaml
services:
  app:
    build: .
    init: true
    restart: unless-stopped
    environment:
      STARTUP_DELAY_MS: "1500"
    read_only: true
    tmpfs:
      - /tmp:size=16m
    mem_limit: 256m
    cpus: 0.5
    pids_limit: 128
    cap_drop:
      - ALL
    security_opt:
      - no-new-privileges:true
    stop_grace_period: 10s
    healthcheck:
      test: ["CMD", "node", "-e", "fetch('http://127.0.0.1:3000/readyz').then(r=>process.exit(r.ok?0:1)).catch(()=>process.exit(1))"]
      interval: 2s
      timeout: 1s
      start_period: 5s
      retries: 3
    logging:
      driver: json-file
      options:
        max-size: "10m"
        max-file: "3"
  proxy:
    image: caddy:2-alpine
    ports:
      - "127.0.0.1:8081:80"
    volumes:
      - ./Caddyfile:/etc/caddy/Caddyfile:ro
    depends_on:
      app:
        condition: service_healthy

先在本机直接运行 node app.mjs,可以观察启动前约一点五秒 readyz 返回五百零三,随后返回二百;livez 一直返回二百。也可在装好 Docker 的 Linux 容器环境执行 docker compose config、docker compose up -d --build,再访问 http://127.0.0.1:8081/version。预期版本字段为 readiness-demo-v1。容器命令在教材编写时未执行,读者需在自己的练习环境观察实际状态。

STARTUP_DELAY_MS 的合同是:变量缺失时默认一千五百毫秒;提供时必须是非空、只含 ASCII 数字的十进制字符串,数值范围为零到三万,包含边界。零、100 和前导零字符串如 00100 合法;空串、任何空白、负号、正号、小数、指数形式与 0x10 均不合法。不能先 trim 再解析,因为那会悄悄接受本合同明确拒绝的输入。格式检查先于 Number 转换,所以不会把空字符串当成零或把十六进制当成十进制配置。

运行 node app.mjs --check-config 只校验配置并输出 JSON,例如未设置变量时得到 {"startupDelayMs":1500},随后退出零,不监听端口;错误配置抛出明确包含变量名的异常,退出非零。正常 node app.mjs 仍按照校验后的延迟启动就绪状态。这个入口适合在容器运行前验证有效配置,不能代替存活、就绪或代理联调。

tmpfs 为临时写入提供有限空间,容器消失后内容不保留,不应存数据库。stop_grace_period 为应用退出留十秒,长于代码五秒兜底,让应用先尝试完成清理,容器运行时再采取强制措施。

在第二个 app.mjs 所在目录另存以下完整文件 check-startup-config.mjs,执行 node check-startup-config.mjs。它启动独立 Node 子进程,分别传入环境变量,检查真实应用的退出码与输出,而不是复制一份解析函数来验证自己。无需 Docker、网络或额外依赖。

javascript
// check-startup-config.mjs:必须和第二个 app.mjs 放在同一目录。
import assert from "node:assert/strict";
import { spawnSync } from "node:child_process";
import { fileURLToPath } from "node:url";
const appPath = fileURLToPath(new URL("./app.mjs", import.meta.url));
const cases = [
  { label: "missing", raw: undefined, value: 1500 },
  { label: "zero", raw: "0", value: 0 },
  { label: "hundred", raw: "100", value: 100 },
  { label: "upper-bound", raw: "30000", value: 30000 },
  { label: "leading-zeros", raw: "00100", value: 100 },
  { label: "empty", raw: "" },
  { label: "whitespace", raw: "   " },
  { label: "surrounding-space", raw: " 100 " },
  { label: "newline", raw: "100\n" },
  { label: "hex", raw: "0x10" },
  { label: "negative", raw: "-1" },
  { label: "over-range", raw: "30001" },
  { label: "fraction", raw: "1.5" },
  { label: "exponent", raw: "1e2" },
  { label: "plus-sign", raw: "+100" },
];
for (const item of cases) {
  const env = { ...process.env };
  if (item.raw === undefined) delete env.STARTUP_DELAY_MS;
  else env.STARTUP_DELAY_MS = item.raw;
  const result = spawnSync(process.execPath, [appPath, "--check-config"], {
    env, encoding: "utf8", timeout: 5000,
  });
  assert.ifError(result.error);
  assert.equal(result.signal, null, item.label);
  if ("value" in item) {
    assert.equal(result.status, 0, item.label + ": " + result.stderr);
    assert.deepEqual(JSON.parse(result.stdout), { startupDelayMs: item.value });
  } else {
    assert.notEqual(result.status, 0, item.label);
    assert.match(result.stderr, /STARTUP_DELAY_MS/u, item.label);
  }
}
console.log("startup configuration checks passed: " + cases.length);

预期输出 startup configuration checks passed: 15。教材修订时已实际执行这十五组子进程检查,涵盖缺失默认、合法零和一百、边界、空白、空串、十六进制、负数及越界等;没有在这次检查中启动 HTTP 服务、执行 Docker 或验证代理与证书。

start_period 给初始化一个宽限窗口,期间失败不会按正常方式累计到不健康阈值;成功检查仍有意义。它不是固定等待五秒才检查,也不会让业务请求自动排队。depends_on 等待服务健康后才启动代理,因此通过代理观察启动中状态不一定方便;想观察就绪变化,可以直接在应用容器内部请求 readyz。健康检查与退出配置

第三个例子:故意失败,按证据定位配置层#

将 STARTUP_DELAY_MS 改成 "wrong",运行构建启动命令后查看 docker compose logs --tail 30 app。预期错误明确指向环境变量,进程不会以错误配置继续运行。此时代理可能因为依赖始终不健康而不启动。不要将 depends_on 删掉当成修复,因为根因是应用配置无效。

恢复变量,再把 Caddyfile 的上游端口改成 3001。应用健康检查依然能成功,代理却无法取得业务响应。依次检查应用状态、代理日志、容器内地址和端口,可以把问题缩到代理到应用这一跳。这个实验说明健康检查成功只覆盖它实际探测的路径,不代表整个公开入口都可用。

进一步将应用的 read_only 关闭,并不应该改变网络错误;修改资源限制也不应该修复错误端口。排障时一次只改变与当前证据相关的一层,并保留前后观测,才能理解为何恢复。盲目一起重启所有服务、换端口、清镜像和修改权限,会消灭定位线索,甚至引入第二个问题。

操作观察应形成稳定顺序:先看 docker compose ps 确认容器状态,再看最近日志与退出信息,再从合适网络位置发起请求。需要了解有效配置时用 docker compose config,确认环境变量替换后究竟是什么。命令可能展开敏感值,因此不要把完整输出公开粘贴到工单;只截取非敏感字段说明问题。

生产入口的责任边界与发布前检查#

HTTPS 终止点决定哪一段连接被加密。代理到同机私有容器网络使用 HTTP 是一种部署选择;跨主机或跨不受控网络时,需要重新评估上游 TLS。不能因为浏览器地址栏有锁,就断定内部所有链路都已加密。TLS 校验失败应修正确书、域名或信任链,不应通过永久跳过校验解决。

静态前端和 API 同域部署可以减少跨域配置复杂度,但身份与 CSRF 等问题仍然需要应用处理。如果使用 cookie,Secure、HttpOnly 和 SameSite 的含义应与登录流程一致;如果代理前还有 CDN,应只信任明确配置的代理来源。外部客户端自己写的 X-Forwarded-For 不能直接当真实用户地址做安全决策。

密钥应通过运行时注入,并限制能读取它的服务。更换密钥不仅是改文件:应用是否缓存、是否需要重启、旧密钥何时失效、失败怎样回退都需要明确。日志和镜像扫描可以发现部分意外泄漏,但不能保证从未泄漏;关键是减少进入构建上下文、前端包和广泛日志的机会。

数据卷和镜像更新为什么要分开考虑#

应用镜像可以随时重建,业务数据却必须跨版本延续。命名卷把数据生命周期从容器中分离出来,但“卷还存在”不等于“有备份”。宿主磁盘损坏、误删除、勒索或逻辑写错都可能影响同一份卷。把数据库写进卷解决的是容器替换问题,把快照和备份保存到独立位置解决的是恢复问题,两者不能互相代替。

绑定挂载直接使用宿主目录,定位文件方便,但容易把宿主权限、路径和内容变化带进运行环境。生产代码通常放进镜像,避免有人修改目录后线上代码悄悄改变。需要挂载配置时可以只读;需要挂载上传目录时则限制到最小范围,避免把整个项目或用户目录交给容器。

更新镜像前先确认数据格式兼容性。新版本写入的数据如果旧版本无法读取,回滚镜像并不会自动恢复服务。应用迁移、队列消息和检索索引都要纳入版本说明。停止练习时也要理解命令影响:删除容器与删除卷是不同操作,带有删除卷含义的选项只能在明确不需要这些数据时使用,不能当作日常“重启修复”手段。

综合练习:把配置错误变成可理解的启动失败#

练习是在第二个应用中增加 PORT 配置,默认三千,只接受一到六万五千五百三十五的整数。所有服务端监听、健康检查与代理引用必须一致。然后故意输入负数、非数字和超范围值,确认错误出现在启动阶段,而不是用户发请求时。

参考实现与观察步骤

在 app.mjs 的 server.listen 之前定义并验证 port,随后用它替换监听端口。下面是完整可独立执行的配置解析练习,保存为 parse-config.mjs。

javascript
// parse-config.mjs
function readPort(value = "3000") {
  if (!/^[0-9]+$/.test(value)) throw new Error("PORT 必须是十进制整数");
  const port = Number(value);
  if (!Number.isInteger(port) || port < 1 || port > 65535) {
    throw new Error("PORT 超出允许范围");
  }
  return port;
}
for (const input of [undefined, "8080", "-1", "abc", "70000"]) {
  try { console.log(String(input), "=>", readPort(input)); }
  catch (error) { console.log(String(input), "=>", error.message); }
}

执行 node parse-config.mjs,前两项得到三千与八千零八十,后三项给出错误。集成到容器时,若改为三千零一,还要同步健康检查和 Caddy 上游。可以避免随意配置内部端口而固定三千,只让宿主发布端口可变;这是减少配置组合的一种合理设计,关键是把可配置范围说清楚。

部署完成的证据应包括有效版本、端到端业务请求、失败路径、日志定位、停止恢复和数据持久化行为。容器运行、端口开放或证书存在都只是其中一部分。本章提供本机练习和配置解释,真正的公网 DNS、证书签发、云防火墙与长期运行必须在获授权的目标环境验证。

练习、提示与参考解答#

练习:把应用监听端口改成 3001,仅修改应用文件,观察故障;修复所有引用后重新构建。再停止 app,观察代理响应与日志。

提示:Dockerfile 的 EXPOSE 只是描述,不会自动改应用端口;健康检查和 reverse_proxy 必须一致。

参考答案

应用改为 3001 后,健康检查仍访问 3000,app 会变为 unhealthy,首次启动的 proxy 也可能因依赖不健康而未启动。同步修改 healthcheck URL、Caddy 上游与 EXPOSE,运行 docker compose up -d --build,再访问健康接口。停止 app 后代理通常返回上游连接错误;恢复 app 后再次验证,不能把容器“Running”当成业务可用。

验收与自测#

验收本机健康接口、404 行为、非 root 运行、停止信号和错误端口排障。记录镜像版本与配置。公网证书、真实域名、云安全组以及付费 API 均需在真实环境另行验收,本练习不宣称完成这些动作。

  1. 容器中的 localhost 是宿主机吗? 不是,它通常只指本容器的网络空间。
  2. EXPOSE 会自动开放公网端口吗? 不会,实际发布由端口映射和网络策略决定。
  3. HTTPS 解决业务越权吗? 不解决。它保护传输,业务鉴权仍由后端实施。
原有课程整理于 2026-09-10;Node / Electron 扩充于 2026-09-11。示例环境与验证范围以正文为准。
原创中文学习手册,阅读结构参考 Vue 文档;非 Vue 官方教材。
下载本章 Markdown

支持中文和英文全文搜索 · ↑ ↓ 选择 · Enter 打开 · Esc 关闭