部署入门:从本地到线上——Nginx + 服务器
你写了个网站,在本地跑得很开心,localhost:4321 里什么都有。然后问题来了:怎么让其他人也看到?
部署这件事,入门门槛其实不高。你不需要 DevOps 认证,不需要 K8s 集群。你需要的就是一台服务器加一个反向代理。白物集现在每天服务几百次访问,底层就是这套方案。
一台服务器就够了
阿里云 ECS、腾讯云 Lighthouse、AWS EC2——选一个最便宜的入门套餐就行。1 核 2G 的配置够跑两个 Node 进程加一个 Nginx,再加一个 Cloudflare Tunnel 做內网穿透。装 Ubuntu 24.04,SSH 进去就是你的战场。
白物集服务器上跑了四个东西:
- Nginx — 接收所有外部请求,按 URL 路径分发给不同的后端进程
- Express API — 处理数据读写,连接 Supabase 数据库
- Astro SSR — 渲染页面,从 API 拿数据拼成 HTML 返回给浏览器
- Cloudflare Tunnel — 免配 HTTPS,替代直接暴露端口
架构图画出来就是:
用户浏览器 → Cloudflare → Nginx (:80)
├── /api/* → Express API → Supabase
├── /_astro/* → 静态资源(一年缓存)
└── /* → Astro SSR → Express API
这不是纸上谈兵,白物集正式环境就是跑这个结构。Nginx 在中间扮演交通警察的角色,所有流量在 80 端口统一入口,按路径分流。
Nginx 是什么,为什么要用它
Nginx 是反向代理服务器。核心职能是:你的 Node 进程监听某个内部端口(比如 3001),Nginx 监听 80 端口(HTTP 默认端口),当用户的请求到达 80,Nginx 根据 URL 路径来决定交给哪个进程处理。
为什么不用 Node 直接监听 80 端口?三个原因。
第一,多个服务需要共用一个 80 端口。 白物集有 Express API 和 Astro SSR 两个 Node 进程,不可能同时占 80。Nginx 在 80 收口,按路径分流:/api/ 开头的交给 Express,剩下的交给 Astro。
第二,Nginx 处理静态文件比 Node 快很多。 Astro 构建产出的 CSS 和 JS 文件经过 hash 命名,适合长期缓存。Nginx 直接读磁盘返回,不走 Node 的事件循环,延迟低一个数量级。白物集配了一年的 Cache-Control,用户浏览器第二次访问直接命中本地缓存。
第三,Nginx 自带限流、HTTPS 终端、gzip 压缩。 白物集的 Express API 源码里单独配了限流中间件:
import rateLimit from 'express-rate-limit';
const apiLimiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15分钟窗口
max: 100, // 最多100次请求
standardHeaders: true,
legacyHeaders: false,
handler: (req, res) => rateLimited(res,
'Too many requests, please try again in 15 minutes'),
});
但 Nginx 层面的 limit_req 是第一道防线——请求在进 Node 之前就被挡住了,CPU 不用浪费在应用层面的拒绝响应上。
Express API 怎么和 Nginx 配合
看一眼白物集 API 的入口文件就知道了:
import express from 'express';
import cors from 'cors';
import logger from './lib/logger.js';
import healthRouter from './routes/health.js';
import articlesRouter from './routes/articles.js';
import ingestRouter from './routes/ingest.js';
// ... 其他路由
const app = express();
const PORT = parseInt(process.env.PORT || '3001');
app.use(cors());
app.use(express.json({ limit: '10mb' }));
// API v1 路由挂载
const v1 = express.Router();
v1.use('/articles', apiLimiter, articlesRouter);
v1.use('/content/ingest', ingestLimiter, ingestRouter);
v1.use('/health', healthRouter);
// ...
app.use('/api/v1', v1);
app.listen(PORT, '127.0.0.1', () => {
logger.info({ port: PORT }, 'baiwuji-api started');
});
注意 listen(PORT, '127.0.0.1')——绑定在本机回环地址。外部无法直接访问 :3001,所有请求必须经过 Nginx 转发。这是安全第一道关:即使 Nginx 被攻破,API 也只在回环接口上暴露。
Astro SSR 的配置类似:
// astro.config.mjs
import { defineConfig } from 'astro/config';
import node from '@astrojs/node';
import tailwindcss from '@tailwindcss/vite';
export default defineConfig({
output: 'server',
adapter: node({ mode: 'standalone' }),
site: 'https://baiwuji.top',
vite: { plugins: [tailwindcss()] },
});
output: 'server' + adapter: node({ mode: 'standalone' }) 告诉 Astro 编译成一个独立 Node 服务器。构建产物在 dist/server/entry.mjs,部署后启动它,它会在内部端口(默认 4321)上运行,同样只接受 Nginx 转发的请求。
Nginx 配置到底怎么写
很多初学者看见 Nginx 配置就头大。其实核心就一个 server 块加几个 location。白物集的正式配置:
server {
listen 80;
server_name ai.golfr20.cn;
client_max_body_size 10m;
# API 请求转发到 Express
location /api/ {
proxy_pass http://127.0.0.1:3001;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
# 静态资源:Astro 构建产物,直接文件系统返回
location /_astro/ {
alias /var/www/baiwuji/website/dist/client/_astro/;
expires 1y;
add_header Cache-Control "public, immutable";
access_log off;
}
# 所有其他请求走 Astro SSR 渲染
location / {
proxy_pass http://127.0.0.1:4321;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
}
拆开看每个 location 做了什么:
location /api/ — 任何以 /api/ 开头的请求,比如 https://ai.golfr20.cn/api/v1/articles,Nginx 把整个请求转发给 http://127.0.0.1:3001。proxy_set_header 把原始请求的 Host 和客户端 IP 传过去,这样 Express 里的 req.ip 拿到的就是真实用户 IP,不是 Nginx 本机 IP。
location /_astro/ — Astro 构建的静态文件(CSS、JS、图片)都在 dist/client/_astro/ 目录下,文件名带 hash 指纹。Nginx 直接从磁盘读取返回,expires 1y 告诉浏览器缓存一年,access_log off 省掉这些静态请求的日志噪音。按白物集的经验,首页的 _astro/ 请求占了总请求数的 60% 以上,直接配 Nginx 读磁盘比走 Node 省了大量 CPU。
location / — 兜底。任何不匹配上面两条的请求(页面路由、RSS、Sitemap 等)都转发给 Astro SSR 的 :4321。Astro 在服务端执行组件渲染、调用 API 拿数据、拼 HTML,然后返回给 Nginx,再返回给用户浏览器。
从本地推到线上的完整流程
白物集的部署流水线走 git hook。服务器上配一个 bare repo,每次 git push 触发:
# 服务器 bare repo 的 post-receive hook
#!/bin/bash
TARGET=/var/www/baiwuji
GIT_DIR=/var/repos/baiwuji.git
while read oldrev newrev refname; do
branch=$(git rev-parse --symbolic --abbrev-ref $refname)
if [ "$branch" = "main" ]; then
echo " → Deploying main branch..."
git --work-tree=$TARGET --git-dir=$GIT_DIR checkout -f main
cd $TARGET/api && npm ci --production
systemctl restart baiwuji-api
cd $TARGET/website && npm ci && npm run build
systemctl restart baiwuji-ssr
# 验证
curl -s -o /dev/null -w "%{http_code}" http://127.0.0.1:3001/api/v1/health
curl -s -o /dev/null -w "%{http_code}" http://127.0.0.1:4321/
echo " → Deploy complete"
fi
done
每次部署完了,要验证两个服务都活着。健康检查路由长这样:
router.get('/', (req, res) => {
const data = {
status: 'ok',
service: 'baiwuji-api',
version: '2.0.0',
time: new Date().toISOString(),
};
res.json(data);
});
curl http://127.0.0.1:3001/api/v1/health 返回 {"status":"ok"},说明 API 活着。再 curl http://127.0.0.1:4321/ 看 SSR 能否返回 200。两个都绿,部署算成功。
白物集有一个自动化脚本每两小时跑一轮检测,curl 本机的 health 和首页,挂了自动 systemctl restart。生产环境不能裸奔,自动化兜底是必须的。
部署新手最容易遇到的三个坑
端口被占。 第一次部署 npm start,报 EADDRINUSE——上一个进程没有正常退出。lsof -i :3001 看谁占着端口,kill -9 PID 清掉。更好的做法是用 systemd 管理进程,systemctl restart baiwuji-api 自动处理旧进程。
Nginx 改了配置没生效。 nano 完配置文件不要直接关掉就以为好了。跑 nginx -t 测试语法,通过了再 systemctl reload nginx。reload 和 restart 的区别:reload 不会中断正在处理的请求,生产环境用 reload。
防火墙没开。 云服务商的安全组规则默认只放行 22(SSH)端口。你得手动加一条入站规则,放行 80(HTTP)和 443(HTTPS)。忘记这一步,Nginx 配置得再好,外部也打不开——这个问题在白物集首次部署时耽误了十几分钟才排查出来。
动静分离才是 Nginx 的核心价值
很多人以为 Nginx 只是个跳板,proxy_pass 完事。这么说吧:白物集所有页面渲染走 Astro SSR 的 Node 进程,但 SSR 返回的 HTML 里引用的 CSS/JS 文件,全部由 Nginx 直读磁盘返回。这意味着 Node 进程不必为每个静态文件请求浪费时间——它只需要专心渲染页面 HTML。
Astro 构建时会把 CSS 打包成 hash 命名的文件放在 _astro/ 下:
dist/client/_astro/
├── index.abc123.css
├── article.DEF456.js
└── logo.789xyz.svg
文件名 hash 由文件内容计算,内容不变 hash 不变。Nginx 配置的 expires 1y 是安全的——hash 变了浏览器自然会请求新文件。
下一篇预告
部署完了,域名怎么解析?HTTPS 怎么配?DNS 是怎么一步步把你的域名指向服务器 IP 的?下一篇《域名、DNS、HTTPS——你的网站怎么被找到》拆给你看。