用 Astro 6 + Bun 在 1GB 服务器上搭建这个博客
这篇文章记录这个博客本身的诞生过程。代码当场写就,架构实时决策,包括若干次推倒重来。
约束条件
服务器规格如下:
| 项目 | 规格 |
|---|---|
| CPU | 2 核 |
| 内存 | 1 GB |
| 磁盘 | 19 GB |
| 已运行服务 | new-api(~50MB)、cliproxyapi(~14MB)、Caddy |
| 可用内存 | ~350 MB |
| 运行时 | Node 20、Bun 1.3、Go 1.25 |
结论: 不能跑 Next.js(~150MB),不能跑数据库服务,不能用任何需要 Chrome/Puppeteer 的工具。
技术选型
为什么是 Astro
| 框架 | 内存占用 | Markdown | 备注 |
|---|---|---|---|
| Astro SSR | ~60MB | 原生 | Islands 架构,按需激活 JS |
| Next.js 15 | ~150MB | 需配置 | 功能全但太重 |
| Nuxt 3 | ~120MB | Content 模块 | 中规中矩 |
| 纯 Go | ~20MB | 需自建 | 生态贫乏 |
Astro 的 Islands 架构是关键:页面默认输出纯 HTML,只在需要交互的地方(比如编辑器、主题切换器)激活 JavaScript。对于博客这种内容主导的场景,这意味着绝大多数页面零 JS 运行时开销。
运行时:Bun + Node 22
Bun 负责包管理和开发时构建,Node 22 负责生产环境运行。中间有个坑:
# Astro 6 要求 Node >= 22.12.0
# 服务器原来只有 Node 20
$ bun run build
Node.js v20.20.2 is not supported by Astro!
Please upgrade Node.js to a supported version: ">=22.12.0"
Bun 有自己的 JS 引擎,不吃 nvm。解法是用 nvm 装 Node 22 并设为默认,Bun 构建时在 shebang 里调用系统 node:
source ~/.nvm/nvm.sh
nvm install 22
nvm alias default 22
架构概览
Caddy 做反代和 HTTPS,Astro 以 SSR 模式运行,文章直接读文件系统,没有数据库。
内容思维导图
用 Markmap 把整篇文章的知识点结构化(可点击展开/折叠):
实现细节
Markdown 渲染管道
最初的计划是用 rehype-mermaid 做构建时 Mermaid 渲染(把 Mermaid 代码块在服务端转为 SVG),结果安装时就挂了:
gyp ERR! not ok
error: install script from "oniguruma" exited with 1
rehype-mermaid 依赖 playwright 或 puppeteer 来无头渲染,在这台没有 Chrome 的小服务器上根本跑不起来。
解法:客户端渲染。 把 Mermaid 代码块在浏览器里渲染,对服务器零压力:
// 页面底部注入,加载 Mermaid CDN 后替换 code 块
import mermaid from 'https://cdn.jsdelivr.net/npm/mermaid@11/dist/mermaid.esm.min.mjs';
mermaid.initialize({ startOnLoad: false, theme: 'neutral' });
document.querySelectorAll('code.language-mermaid').forEach(el => {
const div = document.createElement('div');
div.className = 'mermaid';
div.textContent = el.textContent;
el.parentElement.replaceWith(div);
});
await mermaid.run();
代码高亮用 Astro 内置的 Shiki,双主题配置(亮色 github-light / 暗色 github-dark),不需要额外依赖。
Bear 主题
这个博客默认用 Bear 的 Markdown 渲染风格。Bear 的核心美学是:系统字体、暖白底色、苹果蓝链接、克制的间距。
用 CSS 自定义属性实现,四套主题各自覆盖一组变量:
:root { /* Bear(默认) */
--bg: #FAFAF8;
--fg: #1c1c1e;
--link: #007AFF;
--code-bg: #F2F2F7;
--font-body: -apple-system, BlinkMacSystemFont, "SF Pro Text", Georgia, serif;
}
[data-theme="terminal"] {
--bg: #0d1117;
--fg: #c9d1d9;
--link: #58a6ff;
--font-body: "SF Mono", Menlo, Monaco, monospace;
}
主题切换存 localStorage,页面加载时立即读取,无闪烁:
<script>
const saved = localStorage.getItem('theme');
if (saved) document.documentElement.dataset.theme = saved;
</script>
把这个 <script> 放在 <body> 开头(非 defer),确保在页面渲染前就完成主题设置。
认证
管理后台用 bcrypt + JWT,没有数据库,密码哈希在启动时计算:
// src/lib/auth.ts
const PASSWORD_HASH = bcrypt.hashSync(
process.env.ADMIN_PASSWORD ?? 'changeme',
10
);
export function verifyPassword(input: string) {
return bcrypt.compareSync(input, PASSWORD_HASH);
}
Token 存 HTTP-only Cookie,有效期 30 天。Astro middleware 拦截所有 /admin/* 路由:
// src/middleware/index.ts
export const onRequest = defineMiddleware((ctx, next) => {
if (ctx.url.pathname.startsWith('/admin')) {
if (ctx.url.pathname === '/admin/login') return next();
const token = ctx.cookies.get(COOKIE_NAME)?.value;
if (!token || !verifyToken(token)) {
return ctx.redirect('/admin/login');
}
}
return next();
});
在线编辑器
用 CodeMirror 6,从 ESM CDN 加载,不打进 bundle:
import { EditorView, basicSetup } from 'https://esm.sh/codemirror@6';
import { markdown } from 'https://esm.sh/@codemirror/lang-markdown@6';
import { oneDark } from 'https://esm.sh/@codemirror/theme-one-dark@6';
Split view 预览通过调用后端 /api/admin/preview 端点实现,服务端跑 unified 管道返回 HTML,前端直接 innerHTML。
文章可见性
所有文章默认私密,frontmatter 里一行控制:
---
title: "内部技术文档"
public: false # 改成 true 即可公开,无需 build
showOnHome: false # 是否显示在首页精选
---
SSR 模式下,每次请求实时读文件,改了 frontmatter 刷新即生效,不需要重新构建。
部署
项目目录结构:
① 构建
cd /home/sdmike/blog
source ~/.nvm/nvm.sh && bun run build
② 注册系统服务
blog.service 用 bash 先加载 nvm 再启动 Node,确保拿到正确的 Node 22:
[Service]
ExecStart=/bin/bash -c 'source /home/sdmike/.nvm/nvm.sh && node ./dist/server/entry.mjs'
Environment=HOST=127.0.0.1
Environment=PORT=4321
Environment=NODE_ENV=production
Environment=ADMIN_PASSWORD=your-password
Environment=JWT_SECRET=your-random-secret
sudo cp blog.service /etc/systemd/system/blog.service
sudo systemctl daemon-reload
sudo systemctl enable --now blog
③ 配置 Caddy
Caddy 自动申请并续期 TLS 证书,配置极简:
k330.com {
reverse_proxy localhost:4321
}
sudo cp Caddyfile /etc/caddy/Caddyfile
sudo systemctl reload caddy
④ 验证
curl -sI https://k330.com | head -5 # 检查 HTTPS 和响应码
systemctl status blog # 检查服务状态
journalctl -u blog -f # 实时日志
最终资源占用
$ ps aux | grep -E "new-api|cliproxy|node|caddy" | awk '{print $4, $11}'
# new-api ~50MB
# cliproxyapi ~14MB
# blog (node) ~60MB
# caddy ~10MB
# 合计 ~134MB (1GB 服务器上绰绰有余)
没做的事
有些东西刻意留空:
- RSS:Astro 有官方插件,一个 endpoint 搞定,还没加
- 搜索:Pagefind 构建时生成静态索引,客户端搜索,适合后期加
- 评论:Giscus(GitHub Discussions),零服务端,适合技术读者
- OG 图片:Satori 生成,目前觉得不急
文章发布流程
从写作到上线,四个步骤,无需重新构建:
流程线用 SVG stroke-dasharray + CSS animation 实现流动效果,颜色跟随当前主题的 --link 变量,切换主题时颜色同步变化。
总结
在内存约束下,选型优先级应该是:运行时内存占用 > 功能丰富度 > 开发体验。Astro 的 Islands 架构、Shiki 的内置高亮、CSS 变量主题切换、客户端 Mermaid——每一个选择都在把计算压力从服务器推向浏览器。
这个博客本身就是在验证这套思路能不能在约束条件下跑起来。答案是可以。