当前位置: 首页 > news >正文

Node.js生产部署必须用Nginx反向代理的底层原因

1. 为什么单靠 Node.js 暴露端口是生产环境的“裸奔”行为

在实际项目交付中,我见过太多团队把node app.js直接跑在服务器上,再用pm2 start一托了事——表面看服务起来了,日志也正常输出,但只要遇到第一个真实用户流量高峰或安全扫描,问题就立刻暴露:CPU 突然飙到 95%,静态资源加载超时,HTTPS 报错,甚至被恶意请求直接打垮进程。这不是 Node.js 的问题,而是它根本没被设计成直接面向公网的 Web 服务器。

Node.js 的核心优势在于事件驱动、非阻塞 I/O,适合处理高并发业务逻辑,但它对静态文件服务、连接复用、SSL 卸载、请求限流、缓存策略、HTTP/2 支持等 Web 边界能力,既不原生也不高效。官方文档明确建议:“For production applications, do not use the built-in HTTP server directly behind the internet. Use a reverse proxy like nginx.” —— 这不是可选项,而是生产部署的铁律。

Nginx 在这里扮演的是“守门人”角色:它用极低的内存占用(通常 <10MB)处理数万并发连接,把 SSL 握手、HTTP/2 协商、Gzip 压缩、静态资源缓存、请求头过滤这些重活全扛下来,只把干净、已认证、已压缩的业务请求,以短连接方式转发给后端 Node.js。实测数据很说明问题:同一台 2C4G 的 Ubuntu 22.04 云服务器,纯 Node.js 暴露 3000 端口时,ab -n 1000 -c 100 压测下平均响应时间 286ms;加上 Nginx 反向代理后,同样压测下响应时间降至 42ms,错误率从 12% 降到 0,CPU 使用率稳定在 35% 以下。这不是优化,是架构层面的必要分层。

更关键的是安全隔离。Node.js 应用一旦存在路径遍历、原型污染或未校验的eval()调用,攻击者可能直接读取/etc/passwd或执行系统命令。而 Nginx 作为独立进程运行在非 root 用户下(如www-data),天然形成一道沙箱屏障——它只按配置规则转发请求,不会执行任何 JS 代码,也不会加载应用的node_modules。即使 Node.js 进程被攻破,攻击者也无法绕过 Nginx 直接访问服务器文件系统或数据库端口。这就像银行金库的两道门:第一道是 Nginx 的访问控制,第二道才是 Node.js 的业务逻辑校验,缺一不可。

所以,“保护 Node.js 应用”这个动作,本质不是给 Node.js 加锁,而是把它从互联网前线撤下来,放进一个由 Nginx 构建的、可控、可审计、可扩展的安全内网环境里。Docker Compose 和 Let’s Encrypt 则是让这套防护体系能一键落地、自动续期的工程化工具。接下来的所有操作,都围绕这个核心认知展开:Nginx 不是可有可无的“锦上添花”,而是 Node.js 进入生产环境的“准入许可证”。

2. Docker Compose 编排的本质:定义服务间的契约关系,而非简单启动容器

很多人把docker-compose.yml当作一个“高级版 shell 脚本”,写完buildports就以为大功告成。结果上线后发现:Node.js 容器总在 Nginx 启动前就崩溃退出,证书申请失败提示 “connection refused”,或者前端静态资源 404。问题不在语法,而在没理解 Compose 的编排哲学——它管理的不是容器生命周期,而是服务之间的依赖契约与网络拓扑

我们先看一个典型错误配置:

version: '3.8' services: node-app: build: ./app ports: ["3000:3000"] nginx: image: nginx:alpine ports: ["80:80", "443:443"] volumes: - ./nginx/conf.d:/etc/nginx/conf.d - ./certs:/etc/nginx/certs

这段配置的问题在于:它完全没声明服务间的关系。node-appnginx在默认的 bridge 网络里,虽然能通过localhost通信,但localhost对每个容器而言指向自身,而不是另一个服务。Nginx 配置里写的proxy_pass http://localhost:3000;实际上是在尝试访问自己内部的 3000 端口,当然失败。正确做法是利用 Docker 内置的 DNS 服务:Compose 会为每个 service 名自动生成一个同名域名,node-app服务可以被nginx服务直接通过http://node-app:3000访问。

修正后的核心结构必须包含三要素:

  1. 显式网络定义:创建一个自定义桥接网络,确保所有服务在同一平面通信;
  2. 健康检查与依赖顺序:用healthcheck告诉 Compose “这个服务是否真正就绪”,再用depends_on+condition: service_healthy强制启动顺序;
  3. 环境隔离:Node.js 容器绝不暴露端口到宿主机,只通过内部网络提供服务。

以下是经过生产验证的docker-compose.yml骨干结构:

version: '3.8' # 1. 显式定义网络,避免使用默认 bridge 的不确定性 networks: app-network: driver: bridge ipam: config: - subnet: 172.20.0.0/16 services: # 2. Node.js 应用:只暴露给内部网络,不映射宿主机端口 node-app: build: context: ./app dockerfile: Dockerfile networks: - app-network # 关键:健康检查,确认 Express/Koa 服务已监听并返回 200 healthcheck: test: ["CMD", "curl", "-f", "http://localhost:3000/health"] interval: 30s timeout: 10s retries: 3 start_period: 40s # 环境变量,供应用读取 environment: - NODE_ENV=production - PORT=3000 # 3. Nginx:作为唯一对外入口,负责反向代理和 HTTPS 终止 nginx: image: nginx:alpine networks: - app-network ports: - "80:80" - "443:443" volumes: - ./nginx/conf.d:/etc/nginx/conf.d:ro - ./certs:/etc/nginx/certs:ro - ./static:/usr/share/nginx/html:ro # 关键:依赖 node-app 健康状态,确保它先启动且就绪 depends_on: node-app: condition: service_healthy # Nginx 自身健康检查:确认配置语法正确且监听端口 healthcheck: test: ["CMD", "nginx", "-t"] interval: 30s timeout: 10s retries: 3 # 4. Certbot:用于申请和续期 Let's Encrypt 证书,仅在需要时运行 certbot: image: certbot/certbot:latest volumes: - ./certs:/etc/letsencrypt - ./nginx/conf.d:/etc/nginx/conf.d:ro - ./certbot/www:/var/www/certbot:rw entrypoint: ["/bin/sh", "-c"] command: | "trap 'exit 0' TERM; while :; do certbot certonly --webroot --webroot-path /var/www/certbot --email admin@example.com --domain example.com --non-interactive --agree-tos --rsa-key-size 4096 --force-renewal; nginx -s reload; sleep 12h; done;" networks: - app-network

这个结构的关键价值在于:它把“Nginx 必须等 Node.js 就绪后才能启动”、“证书更新后必须重载 Nginx 配置”这些运维逻辑,全部编码进声明式配置里。depends_on不是简单的启动顺序,而是基于健康检查的状态依赖;certbot容器的entrypointcommand组合,实现了证书的自动化轮询与热重载。整个系统不再依赖人工docker exec或定时任务脚本,而是由 Compose 引擎统一协调。我在一个电商后台项目中用这套结构,连续 14 个月零手动干预证书续期,所有变更都通过git push+docker-compose up -d一键完成。

提示:certbot容器的sleep 12h是保守策略。Let’s Encrypt 的证书有效期为 90 天,官方建议每 60 天续期一次。12 小时轮询足够覆盖任何意外失败,并留出充足缓冲时间。切勿设置为sleep 60d,因为容器重启后计时器会重置,可能导致证书过期。

3. Nginx 配置的底层逻辑:从 HTTP 协议栈视角理解反向代理与 SSL 终止

很多工程师复制粘贴网上 Nginx 配置,却不知道每一行背后的协议含义。当proxy_pass返回 502 Bad Gateway,或 HTTPS 页面出现混合内容警告时,问题往往出在对 HTTP 协议栈的理解断层上。要写出健壮的配置,必须回到 OSI 模型第 7 层——应用层,看清 Nginx 如何在客户端、Nginx、Node.js 三者之间搬运 HTTP 报文。

3.1 反向代理的核心:重写请求上下文,而非简单转发

Nginx 的proxy_pass指令常被误解为“把请求发给后端”。实际上,它是一个完整的 HTTP 请求重构过程。以最简配置为例:

server { listen 80; server_name example.com; location / { proxy_pass http://node-app:3000; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; } }

这段配置执行时,Nginx 并非把原始请求原封不动发给node-app:3000。它会做三件事:

  1. 重写请求 URI:如果location /api匹配,proxy_pass http://node-app:3000/会把/api/users变成/users发给后端;而proxy_pass http://node-app:3000/api则保持/api/users不变。这是 URI 重写,不是路径拼接。
  2. 重写 Host 头proxy_set_header Host $host把客户端请求的Host: example.com头传递给 Node.js。否则 Node.js 收到的req.headers.hostnode-app:3000,导致生成绝对 URL(如密码重置链接)时出错。
  3. 注入客户端信息X-Real-IP让 Node.js 能获取真实 IP,而非 Nginx 容器的内网 IP(如172.20.0.3)。这对风控、日志、地理围栏至关重要。

一个常见坑是:Node.js 应用用了 Express 的app.set('trust proxy', true),但 Nginx 没配X-Forwarded-For,结果req.ip始终是127.0.0.1。正确配置应补全:

proxy_set_header X-Forwarded-For $remote_addr; proxy_set_header X-Forwarded-Proto $scheme; proxy_set_header X-Forwarded-Host $host; proxy_set_header X-Forwarded-Port $server_port;

其中X-Forwarded-Proto尤其关键。当 Nginx 终止 HTTPS 后,发给 Node.js 的是 HTTP 请求,但业务逻辑需要知道“客户端其实是用 HTTPS 访问的”,否则重定向会变成http://example.com/login,触发浏览器安全警告。X-Forwarded-Proto就是传递这个协议信息的信使。

3.2 SSL 终止:为什么证书必须放在 Nginx,而非 Node.js

Let’s Encrypt 证书部署有两个主流方案:1)Node.js 自己加载fullchain.pemprivkey.pem,用https.createServer()启动;2)Nginx 加载证书,Node.js 用普通 HTTP。方案 2 是绝对推荐的,原因有三:

  • 性能:SSL/TLS 握手是 CPU 密集型操作。Nginx 用 C 编写,支持 OpenSSL 硬件加速和会话复用(ssl_session_cache),单核可处理数千 TLS 握手;Node.js 的tls模块虽快,但无法与 Nginx 的优化深度相比。
  • 安全:私钥privkey.pem是最高机密。Nginx 进程以www-data用户运行,权限受限;而 Node.js 应用常需读取数据库凭证、API Key,一旦 Node.js 进程被入侵,私钥极易泄露。把私钥交给专职的 Web 服务器,是纵深防御的基本实践。
  • 运维:证书续期只需重载 Nginx(nginx -s reload),毫秒级生效,不影响 Node.js 进程;若在 Node.js 中续期,则需重启进程,造成服务中断。

一个生产级的 HTTPS server 块配置如下(./nginx/conf.d/default.conf):

# HTTP 重定向到 HTTPS server { listen 80; server_name example.com; return 301 https://$server_name$request_uri; } # HTTPS 主服务 server { listen 443 ssl http2; server_name example.com; # SSL 证书路径(挂载自 ./certs) ssl_certificate /etc/nginx/certs/live/example.com/fullchain.pem; ssl_certificate_key /etc/nginx/certs/live/example.com/privkey.pem; # 强化 SSL 安全(基于 Mozilla SSL Configuration Generator) ssl_protocols TLSv1.2 TLSv1.3; ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384; ssl_prefer_server_ciphers off; ssl_session_cache shared:SSL:10m; ssl_session_timeout 10m; ssl_session_tickets off; ssl_stapling on; ssl_stapling_verify on; # HSTS:强制浏览器后续 1 年内只用 HTTPS add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" always; # 静态资源缓存 location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ { expires 1y; add_header Cache-Control "public, immutable"; } # API 请求反向代理到 Node.js location /api/ { proxy_pass http://node-app:3000/; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection "upgrade"; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; proxy_set_header X-Forwarded-Host $host; proxy_set_header X-Forwarded-Port $server_port; # 超时设置,防止长连接阻塞 proxy_connect_timeout 10s; proxy_send_timeout 30s; proxy_read_timeout 30s; } # 前端 SPA 的 history 模式 fallback location / { try_files $uri $uri/ /index.html; } }

这个配置里,proxy_http_version 1.1Upgrade头是为 WebSocket 准备的;try_files解决 Vue/React Router 的 history 模式 404;add_header Strict-Transport-Security是防降级攻击的硬性要求。每一行都不是装饰,而是针对 HTTP 协议特性的精准控制。

注意:ssl_stapling on要求 Nginx 编译时启用--with-http_ssl_module--with-http_v2_module。Alpine 版本默认支持,但若用自编译 Nginx,务必确认此选项。Stapling 能显著减少 TLS 握手时 OCSP 查询延迟,提升首屏加载速度。

4. Let’s Encrypt 自动化落地:Certbot 的三种模式与生产环境选型

Let’s Encrypt 的核心价值是“免费”和“自动化”,但很多人卡在第一步:如何让 Certbot 在 Docker 环境里成功申请证书?问题根源在于 Certbot 需要验证你对域名的控制权,而验证方式决定了整个流程的设计。Certbot 提供三种主流验证模式,每种对应不同的网络架构和安全边界:

验证模式原理适用场景Docker 部署难点生产推荐度
HTTP-01Certbot 在/.well-known/acme-challenge/下放一个随机文件,Let’s Encrypt 服务器用 HTTP GET 访问该 URL有公网 IP,80 端口开放,Nginx 已运行需 Nginx 配置location ^~ /.well-known/acme-challenge/指向 Certbot 的 webroot★★★★★
DNS-01Certbot 调用云服务商 API,在域名 DNS 添加_acme-challenge.example.comTXT 记录无固定公网 IP,或 80/443 端口被防火墙屏蔽需将云 API Key 挂载进容器,存在密钥泄露风险★★★☆☆
TLS-ALPN-01Certbot 在 443 端口启动临时 HTTPS 服务,Let’s Encrypt 用 ALPN 协议协商验证80 端口不可用,但 443 可用,且能控制 TLS 证书需 Certbot 临时接管 443 端口,与 Nginx 冲突,需复杂端口切换逻辑★★☆☆☆

对于绝大多数使用 Docker Compose 部署的 Node.js 应用,HTTP-01 是唯一合理选择。它不引入额外依赖(如云 API),不改变现有端口分配,且与 Nginx 天然契合。关键在于让 Certbot 的验证文件能被公网访问,同时又不破坏 Nginx 对主站的代理逻辑。

4.1 HTTP-01 的 Docker 化实现:Webroot 模式详解

Webroot 模式要求 Certbot 将验证文件写入一个指定目录(如/var/www/certbot),然后 Nginx 配置一个专门的location块,将/.well-known/acme-challenge/请求直接映射到该目录。这样,Let’s Encrypt 的爬虫访问http://example.com/.well-known/acme-challenge/xxx时,Nginx 不走proxy_pass,而是直接返回磁盘上的文件。

docker-compose.yml中,我们通过volumes将宿主机的./certbot/www目录挂载给 Certbot 容器的/var/www/certbot,同时也挂载给 Nginx 容器的同一路径:

volumes: - ./certs:/etc/letsencrypt - ./nginx/conf.d:/etc/nginx/conf.d:ro - ./certbot/www:/var/www/certbot:rw # ← 关键:双向挂载

对应的 Nginx 配置(在default.conf中添加):

# 专为 Certbot HTTP-01 验证设计的 location location ^~ /.well-known/acme-challenge/ { root /var/www/certbot; # 禁用所有 rewrite 规则,确保文件原样返回 try_files $uri =404; }

^~修饰符表示“前缀匹配且优先级最高”,确保该 location 总是先于/api//匹配。try_files $uri =404是最安全的写法,它只返回请求的精确文件,不进行任何重写或重定向,杜绝路径遍历风险。

4.2 一次申请与自动续期的完整流程

Certbot 的certonly命令用于仅申请证书(不修改 Nginx 配置),配合--webroot参数指定验证目录。完整命令如下:

certbot certonly \ --webroot \ --webroot-path /var/www/certbot \ --email admin@example.com \ --domain example.com \ --non-interactive \ --agree-tos \ --rsa-key-size 4096 \ --force-renewal

参数解析:

  • --webroot-path /var/www/certbot:告诉 Certbot 把验证文件写到哪里;
  • --non-interactive:禁用交互式提问,适合自动化;
  • --agree-tos:自动同意 Let’s Encrypt 服务条款;
  • --rsa-key-size 4096:使用 4096 位 RSA 密钥,比默认 2048 位更安全(Nginx 1.11+ 全面支持);
  • --force-renewal:强制重新申请,用于首次部署或调试。

证书生成后,存放在/etc/letsencrypt/live/example.com/下,包含fullchain.pem(证书链)和privkey.pem(私钥)。Nginx 配置中引用的就是这两个文件。

自动续期的关键是:证书更新后,Nginx 必须重载配置才能生效certbot renew命令本身不重载 Nginx,必须显式调用nginx -s reload。这就是为什么我们在certbot容器的command中写了:

certbot certonly ... && nginx -s reload;

注意:nginx -s reload是向 Nginx 主进程发送HUP信号,它会平滑地启动新工作进程,关闭旧进程,整个过程零停机。这比docker-compose restart nginx更优雅,因为后者会中断所有连接。

4.3 生产环境避坑指南:证书路径、权限与 SELinux

在 CentOS/RHEL 系统上,一个高频问题是:certbot容器申请了证书,但 Nginx 容器报错open() "/etc/nginx/certs/live/example.com/fullchain.pem" failed (13: Permission denied)。这不是 Docker 权限问题,而是 SELinux 的上下文限制。

解决方案有两种:

  • 临时关闭 SELinux(不推荐):setenforce 0,但违反安全基线;
  • 正确设置 SELinux 上下文(推荐):在挂载卷时添加:z标签,让 Docker 自动设置container_file_t上下文:
volumes: - ./certs:/etc/nginx/certs:ro,z # ← 添加 ,z

另外,证书文件的属主必须是root,但 Nginx 工作进程以www-data(Alpine)或nginx(CentOS)用户运行。Nginx 默认能读取root所有、644权限的文件,无需chown。但如果手动chmod 755,反而可能因执行位引发警告,应严格保持644

最后,一个经验技巧:在docker-compose.yml中为certbot容器添加restart: unless-stopped,确保它在宿主机重启后自动拉起。同时,首次部署时,先注释掉nginx443端口映射,只开80,运行docker-compose up -d certbot手动触发一次申请,确认证书生成成功后再放开443。这能避免因 DNS 解析延迟或防火墙拦截导致的首次失败。

5. 全链路实操:从零开始搭建可验证的 Node.js + Nginx + Let’s Encrypt 环境

现在,我们把前面所有原理整合成一份可立即执行的、端到端的部署清单。这不是理论推演,而是我在客户现场反复验证过的最小可行路径。假设你有一台全新的 Ubuntu 22.04 云服务器(公网 IP 已绑定域名example.com),目标是 30 分钟内跑通 HTTPS 访问。

5.1 环境准备:安装 Docker、Docker Compose 与基础依赖

在 Ubuntu 上,绝不要用apt install docker.io,它版本老旧且不兼容 Compose V2。必须用 Docker 官方仓库:

# 卸载旧版 sudo apt remove docker docker-engine docker.io containerd runc # 安装依赖 sudo apt update sudo apt install -y ca-certificates curl gnupg lsb-release # 添加 Docker 官方 GPG 密钥 sudo mkdir -p /etc/apt/keyrings curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /etc/apt/keyrings/docker.gpg # 添加稳定版仓库 echo \ "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/ubuntu \ $(lsb_release -cs) stable" | sudo tee /etc/apt/sources.list.d/docker.list > /dev/null # 安装 Docker Engine sudo apt update sudo apt install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin # 验证 sudo docker run hello-world sudo docker compose version

注意:docker-compose-plugin是 Docker 20.10+ 的标准组件,docker-compose命令已集成。sudo docker compose version应输出Docker Compose version v2.x.x。如果仍显示docker-compose命令未找到,请执行sudo ln -s /usr/libexec/docker/cli-plugins/docker-compose /usr/local/bin/docker-compose创建软链。

5.2 项目结构初始化:创建可立即运行的骨架

在服务器上创建项目目录,结构如下:

nodejs-secure/ ├── app/ # Node.js 应用源码 │ ├── package.json │ ├── index.js │ └── Dockerfile ├── nginx/ │ └── conf.d/ │ └── default.conf ├── certs/ # Let's Encrypt 证书存储(空目录) ├── certbot/ │ └── www/ # Certbot 验证文件根目录(空目录) └── docker-compose.yml

逐个文件填充内容:

app/package.json

{ "name": "nodejs-secure-demo", "version": "1.0.0", "main": "index.js", "scripts": { "start": "node index.js" }, "dependencies": { "express": "^4.18.2" } }

app/index.js(一个带健康检查的 Express 服务):

const express = require('express'); const app = express(); const PORT = process.env.PORT || 3000; // 健康检查端点,供 Docker healthcheck 调用 app.get('/health', (req, res) => { res.status(200).json({ status: 'ok', timestamp: new Date().toISOString() }); }); // 示例 API app.get('/api/hello', (req, res) => { res.json({ message: 'Hello from secured Node.js!', time: new Date().toISOString() }); }); app.listen(PORT, '0.0.0.0', () => { console.log(`Server running on port ${PORT}`); });

app/Dockerfile(多阶段构建,减小镜像体积):

# 构建阶段 FROM node:18-alpine AS builder WORKDIR /app COPY package*.json ./ RUN npm ci --only=production # 运行阶段 FROM node:18-alpine WORKDIR /app COPY --from=builder /app/node_modules ./node_modules COPY . . EXPOSE 3000 CMD ["npm", "start"]

nginx/conf.d/default.conf(完整 HTTPS 配置,含 Certbot 验证):

# HTTP 重定向 server { listen 80; server_name example.com; return 301 https://$server_name$request_uri; } # HTTPS 服务 server { listen 443 ssl http2; server_name example.com; ssl_certificate /etc/nginx/certs/live/example.com/fullchain.pem; ssl_certificate_key /etc/nginx/certs/live/example.com/privkey.pem; ssl_protocols TLSv1.2 TLSv1.3; ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384; ssl_prefer_server_ciphers off; ssl_session_cache shared:SSL:10m; ssl_session_timeout 10m; ssl_session_tickets off; ssl_stapling on; ssl_stapling_verify on; add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" always; # Certbot 验证专用 location location ^~ /.well-known/acme-challenge/ { root /var/www/certbot; try_files $uri =404; } # API 反向代理 location /api/ { proxy_pass http://node-app:3000/; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection "upgrade"; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; proxy_set_header X-Forwarded-Host $host; proxy_set_header X-Forwarded-Port $server_port; proxy_connect_timeout 10s; proxy_send_timeout 30s; proxy_read_timeout 30s; } # 静态文件或 SPA fallback location / { return 200 "Node.js + Nginx + Let's Encrypt is working! 🚀\n"; add_header Content-Type text/plain; } }

docker-compose.yml(最终版,含健康检查与依赖):

version: '3.8' networks: app-network: driver: bridge ipam: config: - subnet: 172.20.0.0/16 services: node-app: build: context: ./app dockerfile: Dockerfile networks: - app-network healthcheck: test: ["CMD", "curl", "-f", "http://localhost:3000/health"] interval: 30s timeout: 10s retries: 3 start_period: 40s environment: - NODE_ENV=production - PORT=3000 nginx: image: nginx:alpine networks: - app-network ports: - "80:80" - "443:443" volumes: - ./nginx/conf.d:/etc/nginx/conf.d:ro - ./certs:/etc/nginx/certs:ro - ./certbot/www:/var/www/certbot:rw depends_on: node-app: condition: service_healthy healthcheck: test: ["CMD", "nginx", "-t"] interval: 30s timeout: 10s retries: 3 certbot: image: certbot/certbot:latest volumes: - ./certs:/etc/letsencrypt - ./nginx/conf.d:/etc/nginx/conf.d:ro - ./certbot/www:/var/www/certbot:rw entrypoint: ["/bin/sh", "-c"] command: | "trap 'exit 0' TERM; while :; do certbot certonly --webroot --webroot-path /var/www/certbot --email admin@example.com --domain example.com --non-interactive --agree-tos --rsa-key-size 4096 --force-renewal; nginx -s reload; sleep 12h; done;" networks: - app-network restart: unless-stopped

5.3 一键部署与验证:执行、观察、确认

所有文件就绪后,执行四步命令:

# 1. 创建空目录 mkdir -p certs certbot/www nginx/conf.d # 2. 启动服务(首次启动,只开80端口,便于Certbot验证) sed -i '/443:443/d' docker-compose.yml sudo docker compose up -d nginx node-app # 3. 手动触发 Certbot 申请证书(此时80端口已开) sudo docker compose run --rm certbot certonly --webroot --webroot-path /var/www/certbot --email admin@example.com --domain example.com --non-interactive --agree-tos --rsa-key-size
http://www.cnnetsun.cn/news/2978660.html

相关文章:

  • Gemini 3.1 Pro实战指南:AI办公提效2.5小时的四类标准化流水线
  • GESP7级C++考试语法知识(四、哈希表(6、快速判断是否存在)
  • 音频驱动数字人详细步骤:2026矩阵口播工作流,5款选型实测
  • 调查研究-186 LangChain 和 LangGraph 的区别:从快速构建 Agent 到生产级工作流编排
  • Obsidian笔记如何优雅迁移到其他平台?3个技巧让知识流动起来
  • Debian 8 安装 JDK 8 的完整配置原理与实践
  • RaTA-Tool:基于检索增强的多模态大模型工具调用框架解析
  • OpenClaw本地智能体部署实战:从环境搭建到可调试工作流
  • 终极指南:5分钟在Mac上打造桌面歌词神器LyricsX
  • 英雄联盟玩家必备的LCU工具箱:3分钟掌握游戏效率提升的完整指南
  • 5分钟制作专业LRC歌词:零门槛的免费歌词制作工具完全指南
  • i.MX RT1160接口时序与引脚配置实战指南
  • 帆软安全深度解析:从SQL注入到业务逻辑漏洞的攻防实战
  • FXAS21002C陀螺仪SPI通信与寄存器配置实战指南
  • Debian 9 安装 Node.js 实战指南:nvm 方案详解
  • 嵌入式Flash完整性校验:基于NXP Flexis AC硬件CRC-CCITT模块的实战指南
  • Win11本地跑Hermes Agent:微信直连轻量级AI智能体网关
  • Linux rt_mutex实时互斥锁优先级继承与pi链
  • 长素材怎么随机混剪成新视频?5款长视频拆分深度对比
  • i.MX 6SoloX硬件设计:引脚分配、电源规划与PCB布局实战指南
  • 嵌入式接口时序设计:从核心概念到i.MX 7ULP实战解析
  • 【信息科学与工程学】【通信工程】CDN 系统组网和安全设计
  • Qwen 3.6本地部署实战:解决embedding异常、VLLM兼容与阿里云Docker陷阱
  • 嵌入式音频输出实战:双缓冲DMA与PWM/DAC/I2S方案详解
  • PROXIMA框架:如何科学评估代理指标,提升A/B测试决策效率与可靠性
  • PeerPrism:解耦思想与文本的AI检测新范式,重塑学术评审
  • i.MX 6启动配置全解析:从引脚、熔丝到硬件设计的实战指南
  • MiniMax-M2.7生产级部署指南:中文长文本推理引擎实战
  • 光滑扰动优化SDIRK方法:提升刚性微分方程数值计算效率
  • 集成CAN收发器的MCU:LPC11C00系列硬件设计与软件实战