用 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

架构概览

graph TB Browser["浏览器"] Caddy["Caddy 反向代理 + 自动 HTTPS"] Astro["Astro SSR / Node 22\n博客 :4321"] FS[("Markdown 文件")] Mid["JWT 认证中间件"] Editor["CodeMirror 6 编辑器"] NewAPI["new-api :3000\nAI 服务"] ClipProxy["cliproxyapi :8317"] Browser -->|HTTPS| Caddy Caddy -->|"k330.com"| Astro Caddy -->|"airouter.k330.com"| NewAPI Caddy -->|"cliproxy.k330.com"| ClipProxy Astro --> FS Astro -->|/admin| Mid Mid --> Editor

Caddy 做反代和 HTTPS,Astro 以 SSR 模式运行,文章直接读文件系统,没有数据库。

内容思维导图

用 Markmap 把整篇文章的知识点结构化(可点击展开/折叠):

# K330 博客 ## 技术选型 - Astro 6 SSR - Islands 架构 - SSR 实时渲染 - Bun + Node 22 - 包管理 - 生产运行 - Tailwind v4 - CSS 变量主题 ## 渲染管道 - unified 框架 - remark-gfm - Shiki 代码高亮 - rehype-slug 目录 - Mermaid / Markmap 客户端 ## 功能特性 - 多主题切换 - Bear 默认 - GitHub / Notion - Terminal 暗色 - 在线编辑器 - CodeMirror 6 - split 预览 - 阅读辅助 - 目录侧边栏 - 浮动导航 - 荧光笔高亮 ## 部署运维 - Caddy 反代 - systemd 服务 - 自动 HTTPS

实现细节

Markdown 渲染管道

最初的计划是用 rehype-mermaid 做构建时 Mermaid 渲染(把 Mermaid 代码块在服务端转为 SVG),结果安装时就挂了:

gyp ERR! not ok
error: install script from "oniguruma" exited with 1

rehype-mermaid 依赖 playwrightpuppeteer 来无头渲染,在这台没有 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 刷新即生效,不需要重新构建。


部署

项目目录结构:

blog/
├── content/
│ ├── blog/  ← 公开博文(.md)
│ └── articles/  ← 技术文章(默认私密)
├── src/
│ ├── layouts/  ← Base.astro · Post.astro
│ ├── pages/  ← 路由文件
│ │ ├── index.astro
│ │ ├── blog/[slug].astro
│ │ └── api/admin/  ← save · preview · toggle-public
│ └── lib/
│ ├── auth.ts  ← bcrypt + JWT
│ └── content.ts  ← 渲染管道
└── dist/  ← bun run build 输出

① 构建

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 生成,目前觉得不急

文章发布流程

从写作到上线,四个步骤,无需重新构建:

写 Markdown AI / 编辑器辅助 CodeMirror 6 保存草稿 public: false /content/blog/ 改为公开 public: true 无需重新构建 刷新上线 SSR 实时读取 浏览器展示

流程线用 SVG stroke-dasharray + CSS animation 实现流动效果,颜色跟随当前主题的 --link 变量,切换主题时颜色同步变化。


总结

在内存约束下,选型优先级应该是:运行时内存占用 > 功能丰富度 > 开发体验。Astro 的 Islands 架构、Shiki 的内置高亮、CSS 变量主题切换、客户端 Mermaid——每一个选择都在把计算压力从服务器推向浏览器。

这个博客本身就是在验证这套思路能不能在约束条件下跑起来。答案是可以。