本文首发于 2023 年,2026-04 根据最新版 NPM(UI 改了、SSL 流程也更严格了)完整重写。如果你就是按旧版步骤卡在
Internal Error上的小伙伴,可以直接拉到下面「常见坑」那一节。
前言
咕咕之前写过一篇 在 NPM 上部署静态网站 的短教程,那会儿流程特别简单,两句话就能讲完:建个文件夹、Advanced 里写一行 root,保存完事。
结果最近几年 NPM 迭代了好几个大版本,不少小伙伴在评论和 Telegram 群里反馈:
- Advanced 标签找不到了,点哪都没反应
- 按老教程配完,申请 SSL 一直弹
Internal Error - 保存 Proxy Host 成功但访问直接 403 或 404
这周正好给 bwg.laoda.de 部署静态页,顺手把踩过的坑都记下来,把老文章整个重写一遍。这版本适用于 2026 年最新的 NPM,流程跑熟了五分钟就能上线一个新站。
什么情况下适合这样部署
- 只托管一些简单的 HTML 单页:博客首页、落地页、工具页,甚至只放一个
robots.txt和sitemap.xml - 服务器上已经有 NPM 在跑,不想再起一个 nginx 或 caddy 容器
- 域名 DNS 已经指到服务器,80 和 443 端口都开着
如果你要跑的是 WordPress、Halo 这种动态博客,或者 Next.js SSR 应用,就不适合这个方案了,还是老老实实起个应用容器让 NPM 反代。
开始之前
1、NPM 已经在跑,80 和 443 端口正常映射到宿主机
2、域名 A 记录指向服务器公网 IP,且已经生效(dig 能查到返回正确 IP)
3、知道 NPM 的 data/ 挂载路径。忘了就进服务器跑一下:
docker inspect <你的NPM容器名> --format '{{json .Mounts}}' | jq
本文用 /root/data/docker_data/npm/data 做例子,这个宿主机目录在容器里对应 /data。
目录怎么放比较合理
老版本是把静态文件直接丢 data/<域名>/ 下面,和 NPM 自己的 nginx/、letsencrypt-acme-challenge/、custom_ssl/ 平级,看起来特别乱。这次重新规划,推荐统一放到 data/custom/www/ 下面:
/root/data/docker_data/npm/data/
├── nginx/
├── letsencrypt-acme-challenge/
├── custom_ssl/
├── database.sqlite
└── custom/
└── www/
├── bwg.laoda.de/
├── demo.example.com/
└── ...
好处是以后备份或者迁移,只要打包 custom/ 一个目录就行,和 NPM 本身解耦得很干净,升级 NPM 也不用担心碰坏你的内容。
部署步骤
1、在服务器上建目录
mkdir -p /root/data/docker_data/npm/data/custom/www/bwg
2、把本地文件传上去
# Mac / Linux 本地执行
scp -P 22 index.html robots.txt sitemap.xml root@你的服务器IP:/root/data/docker_data/npm/data/custom/www/bwg/
SSH 端口不是默认 22 就改成你自己的,比如 -P 222。
3、给 nginx 读文件的权限
chmod -R a+rX /root/data/docker_data/npm/data/custom/www/bwg
咕咕第一次部署就漏了这步,打开直接 403 Forbidden,卡了半天才反应过来。注意参数是大写 X,只给目录和已有执行位的文件加 x,别无脑 +x,不然 html 文件也会被标成可执行的。
4、进容器里确认一下文件能看到
docker exec -it <你的NPM容器名> ls -la /data/custom/www/bwg/
能看到 index.html 就对了,看不到说明宿主机路径没对齐容器挂载,回头检查。
5、NPM 里新建 Proxy Host(SSL 先别开)
登进 NPM 后台,Hosts → Proxy Hosts → Add Proxy Host。
Details 标签:
- Domain Names:填你自己的域名,比如
bwg.laoda.de - Scheme:
http - Forward Hostname / IP:
127.0.0.1 - Forward Port:
80 - Cache Assets:开
- Block Common Exploits:开
- Websockets Support:关
Forward Hostname 和 Port 这里随便填个占位就行,因为我们根本不会反代到别的服务,全靠下面 Advanced 里的 root 直接读文件。但 NPM 表单必填,空着保存不了。
SSL 标签: 暂时保持 None,Force SSL、HSTS 全都先别开。咕咕踩过的坑是 SSL 和 Advanced 一起提交经常互相干扰,先把 HTTP 链路跑通,SSL 留到最后一步。
Advanced 标签: 新版 NPM 最容易迷路的地方来了。Advanced 不在顶部标签栏里,而是标签栏右上角那个齿轮图标 ⚙️(跟 Details / Custom Locations / SSL 并排的那一排)。点齿轮才会出来 Custom Nginx Configuration 的大文本框。
粘贴这段进去:
location / {
root /data/custom/www/bwg;
index index.html;
try_files $uri $uri/ =404;
}
location = /robots.txt {
root /data/custom/www/bwg;
access_log off;
log_not_found off;
}
location = /sitemap.xml {
root /data/custom/www/bwg;
add_header Content-Type application/xml;
}
和老教程相比,多了几个细节:
index index.html:显式指定默认首页,别依赖默认行为try_files $uri $uri/ =404:访问不存在的路径直接返回 404,不会错误落到反代默认逻辑- 单独给
robots.txt和sitemap.xml写 location:一个关 404 日志污染,一个补 XML MIME 头,爬虫读起来更舒服
粘完 Save,绿色成功提示。
6、验证 HTTP 是否通
curl -I http://bwg.laoda.de/
curl -s http://bwg.laoda.de/robots.txt | head -5
返回 200 OK 就说明 HTTP 链路没问题。这一步如果不通,就别急着往下走申请 SSL,LE 的验证也一定会失败。
7、回头申请 SSL
编辑刚才保存的 Proxy Host,切到 SSL 标签:
- SSL Certificate:Request a new SSL Certificate
- Force SSL:开
- HTTP/2 Support:开
- HSTS Enabled:开
- Email Address:你自己的邮箱(证书到期时 LE 发通知用)
- 底下的 I Agree 勾上
Save,等个十几秒,证书就下来了。成功之后域名会显示 Online 绿色小标。
浏览器打开 https://你的域名/,地址栏有小锁头、页面正常加载,搞定!
常见坑
Internal Error(申请 SSL 时报错)
九成九都是 Let’s Encrypt 的 HTTP-01 挑战没通过,跟你 nginx 配置本身没关系。LE 要从公网访问 http://你的域名/.well-known/acme-challenge/xxx,哪一环断了都会炸。按顺序排查一下:
# 1. DNS 是否已经生效
dig +short 你的域名
# 2. 80 端口从外面是否能通
curl -I http://你的域名/
# 3. 看 NPM 容器里 certbot 的真实报错
docker logs --tail 100 <你的NPM容器名> 2>&1 | grep -iE "error|certbot|letsencrypt"
最常见的四种情况:
- DNS 还没生效(刚改完解析的等 5-30 分钟再试)
- 服务器防火墙或云厂商安全组只放了 443,没放 80
- 同一个域名之前申请失败过太多次,被 LE 限频了(一周最多 5 次失败,触发后等一周)
- NPM 的 SSL 证书列表里有同域名的僵尸证书,先删掉再来
403 Forbidden
文件权限不够。chmod -R a+rX 再跑一遍。一定是大写 X,小写的 x 会把 html 也标成可执行的。
404 Not Found
Advanced 里 root 路径写错了,或者容器内根本看不到文件。进容器 ls 一下立马就能定位。
保存 Advanced 就报 Internal Error
先把 SSL 标签选回 None 保存一次,再单独回去填 Advanced。NPM 偶尔会把两边的错误搅在一起,分开提交就好了。
证书下来了但浏览器报 SSL_ERROR_BAD_CERT_DOMAIN
Domain Names 里填的和证书实际签发的域名对不上,或者想上泛域名证书但没开 DNS Challenge。按实际情况改一下再重新申请。
日常怎么维护
改页面内容:直接替换服务器上对应文件就行,nginx 读静态文件不需要 reload:
scp -P 22 index.html root@你的服务器IP:/root/data/docker_data/npm/data/custom/www/bwg/
改了 Advanced 配置之后:NPM 会自动重新生成 conf 并 reload,不用手动操作。想强制刷新一下的话:
docker exec <你的NPM容器名> nginx -s reload
备份:打包 data/custom/ 整个目录,再加上 NPM 自己的 data/database.sqlite,就是一份完整的静态站 + 反代配置备份,迁机器时丢到新服务器同路径下就能复原。
进阶:一个 root 挂多个域名
如果几个域名想共用同一套静态资源,比如 www.example.com 和 example.com,Domain Names 里一次性填上就行,NPM 会在同一条 server block 里生成多个 server_name,不需要为每个域名都建一份 Proxy Host。
进阶:对 AI 爬虫友好一点
2026 年做内容站,搜索流量越来越少是大趋势,除了 Google,还要考虑 ChatGPT、Perplexity、Claude、豆包这些 AI 检索能不能抓到你的页面、能不能在回答里引用你的内容。robots.txt 里显式放行主流 AI 爬虫:
User-agent: *
Allow: /
User-agent: GPTBot
Allow: /
User-agent: OAI-SearchBot
Allow: /
User-agent: ChatGPT-User
Allow: /
User-agent: PerplexityBot
Allow: /
User-agent: ClaudeBot
Allow: /
User-agent: Google-Extended
Allow: /
Sitemap: https://你的域名/sitemap.xml
再配合 sitemap.xml 和结构化数据(JSON-LD),AI 在回答相关问题时引用到你的概率会高不少。
搞定!
和三年前那版比,这次主要升级了这几点:
- 目录统一收到
custom/www/下,和 NPM 自身结构解耦 - Advanced 配置从一行裸
root扩展到完整的index/try_files/ robots / sitemap 模板 - 明确"先 HTTP 通,再 SSL"的顺序,彻底避开
Internal Error - 补齐 403 / 404 / SSL 限频的具体排查命令
- 标注了新版 NPM 里 Advanced 被收进齿轮图标的位置
流程跑熟了之后,从传文件到 HTTPS 上线,五分钟就能闭环一个新静态站。
推荐阅读
- 实战案例:搬瓦工 CN2 GIA-E VPS 选购指南 2026,这篇教程的真实落地
- Nginx Proxy Manager(NPM)的安装与使用
---------------
如何觉得文章内容不错,欢迎点击一下广告,支持一下咕咕😍😍😍
原创文章,作者:Roy,如若转载,请注明出处:https://iwanlab.com/host-static-sites-on-npm/
评论列表(1条)
那css,js,image这些资源怎么办呢,跟博主一样配置,但是这些文件无法加载