搭建个人 Memos 服务
按语
要非常感谢 memos 项目这是我第一个 sponsor 的 GitHub 项目,没有它我也就无从谈自己的 Memos 服务这件事。在动手搭建之前我打算先思考一个问题:为什么我这么看重为自己搭建 Memos Service?
成长提升
帮助自己了解人肉上线一个生产可用的服务需要些什么技术?如果我自己运营一家一人公司的话哪些技术活是我必须掌握的?
- 从0到1的搭建可用于生产的线上服务,涉及到 CNAME(A)、DNS、Nginx、Cloudflare R2、HTTP、HTTPS Certificate、ECS 管理、Docker、DevOps 等;
- React 技术;
- SSR 技术;
- Vite 前后端绑定;
- Go 后端技术;
- gRPC/RESTful API 技术;
- iframe 网页嵌入;
- 静态网页生成、部署;
- 前后端调试;
- Telegram bot 开发、调试;
思考记录
现在有很多非常成熟的服务可以使用,为什么还需要一个完全由自己掌控的 Memos Service?
- 问:Obsidian 不够吗?
- 答:Obsidian 中的 memos 缺少了对外展示的渠道,Obsidian Publish 跟 Newsletter 重合且价格很高;
- 问:Flomo 不够吗?
- 答:没有展示的渠道,功能上跟 Obsidian 本质是一样的,只是它多了展示、统计、提醒、回顾等功能,由于使用 Obsidian 这些我并不需要。另外一个,数据托管在他们的服务器上有风险;
- 问:Newsletter 不够吗?
- 答:对于 fleeting ideads 记录(移动端)太重了不适合 Memos 形式的内容,之前考虑 micoblog 最终还是放弃了,因为它本质上还是一个博客平台还是太重了;
- 问:Twitter 不够吗?
- 答:账号风险、受众在国外,简中圈子氛围也没有特别融洽,自己也不喜欢被人品头论足,账号数据说没就没了;
- 问:Telegram 不够吗?
- 答:也是一样存在受众,可用性,数据安全等问题,我自己对电报的定位还是一个信息收集器,对我自己而言它不是一个好的记录的地方;
内省
搭建自己的 Memos Service 这个事情本质上还是跟“我想要什么?”这个命题相关,现阶段我自己的一些思考和想法是这样的:
- 关于记录,我希望是自由的、思考的、美好的、自主的;
- 关于技术,我希望是全面的、利他的、高效的;
- 关于自己,我希望是真诚的、正直的、善良的、周全的、至情至性的;
- 关于数据,我希望是安全的、可控的;
- 关于人生,我希望是按照星星而不是按照过往船只的灯光设定航向(By 产品沉思录);
下面开始正文部分,会详细介绍我是如何部署 Memos Service 的。
Memos 服务部署
云实例
部署之前需要购买一台服务器,出于性价比考虑,个人使用的情况,我只尝试过 AWS 的实例 ec2.t3.micro
和阿里云的实例 ecs.t5-lc1m1.small
(为了避免推销电话的骚扰阿里云我用的是 alibabacloud.com),为什么这么选型,以下是一个简单的评估方法:
云服务器的带宽,指的是出网带宽,用户发起请求,服务器发送数据给终端时,会占用这一部分的带宽。假如云服务器的带宽是 1M,最大的传输速度是 128kb/s,当用户浏览网站的时候,云服务器向用户发送数据,传输速度就是 128kb/s,1M=1024/8=128kb/s
。这个传输速度,看起来很慢,但实际上很多时候是够用的。我们浏览的网页,大多由文字和图片组成,一个汉字才 2 个字节,图片经过压缩,通常也在几十 kb 左右。只要页面内容不是特别多的话,1M 带宽的速度,跟 5M 带宽的打开速度没有什么差别。当然,这只是算同一时间,只有一个用户在访问网站的情况。如果网站同时有两个用户在浏览内容,理论上每个用户只能分到 60 多 kb,如果 10 个用户同时浏览,每个人只有 12.8kb/s 的速度,网站打开速度就会非常慢。那么,1M 的带宽到底能承受多少人在线呢?根据用户每秒请求数据量的大小估算,接口类的用户,每秒请求 10 次,每次数据量是 50 个汉字( 100 字节),1M 的带宽可以承载的用户数量为 128*1000/10/100=128 个用户
。用户需接收图片,假设每秒下载一次图片大小为 10kb,那么可以同时承载 12.8 人。如果是个人博客网站,一篇文章 1000 字,还会配 2 张图,那么这一篇文章大小在 100kb 左右,相当于每秒可以接收一个用户的访问。1 秒可以接收一个用户,相当于每小时能接收 3600 个用户,一天就是 86400 名。当然,这个只是理论数据,用户的访问不可能那么均匀,也不可能每秒请求一次。
另外还有一个非常经验型的判断方法,一般来说日均两三千 IP 以下的网站,1M 的带宽就够用了。另一种是观测云服务提供商的监控后台,出网带宽经常处于 128kb/s 峰值时,说明需要升级带宽了。
根据上述的判断方法 ECS 实例选型 1C1G 1M 完全够用了即 ec2.t3.micro
和 ecs.t5-lc1m1.small
完全满足要求。
Memos 容器部署
在 Linux 上部署 Memos Service 容器服务本身没有什么特殊的,具备基本的 Linux 的知识就可以了,这里主要是参考了 memos 提供的指导文档:Self-Hosting Memos,简言之就是通过 docker 启动 Memos 容器:
# choose one of them to startup Memos container.
# 1. startup with docker-run cmdline
docker run -d \
--init \
--name memos \
--publish 5230:5230 \
--volume ~/.memos/:/var/opt/memos \
ghcr.io/usememos/memos:latest
# 2. startup with docker-compose cmdline
# docker-compose.yml
## version: "3.0"
## services:
## memos:
## image: ghcr.io/usememos/memos:latest
## container_name: memos
## volumes:
## - ~/.memos/:/var/opt/memos
## ports:
## - 5230:5230
docker-compose up -d
Memos 域名配置
为了让 Memos 服务能够公网访问需要进行域名配置,这涉及到多个组件的配置包括:Nginx、HTTPS、CNAME。
Subdomain
完成云实例的购买之后可以在域名管理中添加对应的域名配置,我为 Memos 服务配置了一个子域名,如果你的 aws 提供了 public DNS 则可以配置一个 CNAME 指向该 public DNS,如果你的 ecs 提供了 public IP 则可以配置一个 A 指向该 public IP。
Nginx
Memos 容器服务启动之后需要一个反向代理才能与域名连接,所以配置 Nginx 充当反向代理:
- 为 Memos Service 配置 Nginx 反向代理
server {
server_name your-domain-name.com;
location / {
proxy_pass http://localhost:5230;
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;
}
}
- 使能反向代理,通过软件链接使能域名反向代理然后重新启动 Nginx systemd service 即可;
HTTPS
配置 HTTPS 是通过 Let's Encrypt 申请 SSL证书来完成的:
- 安装 certbot;
- 利用 certbot 申请域名对应的 SSL 证书;
注意在申请之前需要确保域名 CNAME 配置已经指向你的服务器。
Memos 存储配置
Memos Services 存储可以根据存储数据类型的不同大致分为两类:
- 文本数据、管理元数据;
- 非文本数据(image,video 等);
其中,文本数据以及管理相关的元数据 Memos 默认使用 SQLite 存储,如果有可用的 MySQL 服务也可以用 MySQL 替代 SQLite(出于对数据库服务的信任,如果使用了 MySQL 下文提及的数据备份可以不用配置)。非文本数据的主要考虑使用 OSS 对象存储用来加速访问,例如 Cloudflare R2,AWS S3,Aliyun OSS 等。
MySQL 配置
启动 memos 容器实例需要增加 MySQL 的配置参数:
--driver mysql
--dsn dbuser:dbpass@tcp(dbhost)/dbname
这样容器部署启动命令如下:
docker run -d \
--name memos \
-p 5230:5230 \
-v ~/.memos/:/var/opt/memos \
ghcr.io/usememos/memos:latest \
--driver mysql \
--dsn 'root:password@tcp(localhost)/memos_prod'
同时还可以考虑通过环境变量管理 MySQL 相关参数:
export MEMOS_DRIVER=mysql
export MEMOS_DSN=root:password@tcp(localhost)/memos_prod
docker run -d \
--name memos \
-p 5230:5230 \
-v ~/.memos/:/var/opt/memos \
ghcr.io/usememos/memos:latest \
--driver ${MEMOS_DRIVER} \
--dsn ${MEMOS_DSN}
对于存量的 SQLite 数据可以利用 Memos 自带的迁移命令将数据迁移到 MySQL:
/usr/local/bin/memos \
--driver mysql \
--dsn 'dbuser:dbpass@tcp(dbhost)/dbname' \
copydb --from sqlite://path_to_your_memos_prod.db
Cloudflare R2 配置
我采用的是 Cloudflare R2 作为非文本数据的存储,主要是因为 R2 每个月都有免费额度,目前对我个人使用来说是够用了。当然在进行 Cloudflare R2 的配置之前需要注册一个 Cloudflare 账号。
- 创建 R2 bucket,如下图所示在 R2 Section 中点击 Create bucket 按钮创建 R2 bucket;
- 创建 R2 的 Access Key,如下图所示在 Manage R2 API Tokens 中点击 Create API Token 创建 Access Key 注意配置好读写权限;
- Memos R2 配置,如下图所示在 Memos 设置中找到存储创建 R2 配置并选用;
数据备份
Memos 最重要其实是记录的数据,所以需要定期备份数据,无论是用 SQLite 还是用 MySQL 都应该养成定期备份的服务。我使用 Github Repo 进行自动备份,这种备份方法有一个问题就是受到 git LFS 的限制即最多 2GB 大小的备份文件,具体参考 About Git Large File Storage 说明。由于 Memos 大多是文本数据,所以 2GB 大致可以保存 10 亿汉字,目前来看足够用了。
GitHub repo auto-commit
为了能够自动备份到 GitHub 上,我自己写了一个脚本工具做自动化 commit 提交:
# memos data directory is ~/.memos/
tar -czf memos-archive.tar.gz ~/.memos/memos_prod.db
message="backup in `date '+%Y%m%d%H%M'`"
git add -A .
git commit -s -m "${message}"
git push
关于 GitHub Repo 配置的事情可以参考文档 Connecting to GitHub with SSH 。
cron task
为 Memos 备份配置定时任务,这样系统就自动执行备份程序:
- 创建备份脚本即上面提到的 auto-commit 脚本;
- Linux 系统配置 cron job,在
/etc/crontab
中增加自动备份定时任务,例如每天凌晨3点进行备份;
# /etc/crontab: system-wide crontab
# Unlike any other crontab you don't have to run the `crontab'
# command to install the new version when you edit this file
# and files in /etc/cron.d. These files also have username fields,
# that none of the other crontabs do.
SHELL=/bin/sh
# You can also override PATH, but by default, newer versions inherit it from the environment
#PATH=/usr/local/sbin:/usr/local/bin:/sbin:/bin:/usr/sbin:/usr/bin
# Example of job definition:
# .---------------- minute (0 - 59)
# | .------------- hour (0 - 23)
# | | .---------- day of month (1 - 31)
# | | | .------- month (1 - 12) OR jan,feb,mar,apr ...
# | | | | .---- day of week (0 - 6) (Sunday=0 or 7) OR sun,mon,tue,wed,thu,fri,sat
# | | | | |
# * * * * * user-name command to be executed
# memos backup
0 03 * * * root sh /root/memos-backup/backup.sh
服务集成
除了直接访问 Memos 的方式之外,我还在子域名中定制了一个只读的 Memos 用于进行公共展示。
Ghost embed
- 利用 memos.top 项目定制个人的 GitHub Pages,可以参考我的项目 edonyzpc/memos.edony.ink 主要是定制化了一个人相关的配置,还有 comment 相关的配置。GitHub Pages 配置可以参考文档:
- 在 Ghost Post 中通过 iframe 嵌入上面的 GitHub Pages,具体做法就是在 Post 中插入 HTML,HTML 中的代码如下:
<iframe
id="tweets"
title="talky talky"
src="https://memos.edony.ink/"
style="height: 100%; width: 100%;"
onload="resizeIframe(this)"
>
</iframe>
在 code injection 中插入 resizeIframe 的实现代码用于自动修改 iframe 的大小:
function resizeIframe(obj) {
obj.style.height = this.document.documentElement.scrollHeight + 'px';
}
支持 Memo 嵌入 Ghost 就完成了,具体效果可以参考:
- https://memos.edony.ink/ (GitHub Pages)
- https://edony.ink/memos/ (Ghost Embedded Post)
移动端
Memos 推荐的 iOS 客户端 Moe Memos 解决移动端使用的问题,我单独为 iOS 客户端生成了一个专用的 Access Token 出于安全考虑定期轮转,具体如下所示:
iOS 效果如下:
自定义配置
Style && Script 自定义
Memos 还支持 Style 和 Script 自定义配置,CSS Style 我还没有做过定制,我之前在配置 R2 的时候碰到 Cross-Origin Resource Sharing (CORS) 的问题(其实是配置错误,正确配置是没有问题的),我自己通过 Script 自动替换 Memos 中的 R2 object access URL,这个我在 Memos 的 issue 中有分享。主要就是设置的 Additional script 中添加如下代码,代码比较丑了解一下意思就行🤪:
function refresher() {
setTimeout(function () {
for (item of document.getElementsByClassName('mt-2')) {
for (img of item.getElementsByTagName('img')) {
if (img.src.startsWith('https://${cloudflare R2 bucket URL}')) continue;
img.setAttribute('src', 'https://${cloudflare R2 bucket URL}'' + img.src.split('/')[img.src.split('/').length - 1]);
}
}
}, 1500)
}
setInterval(function () {
refresher();
}, 30000)
Telegram bot 自定义
根据 Memos 的 Telegram bot 的指导文档配置了机器人自动转发 memo 消息,由于我自己还在为博客维护一个私有的 Channel,希望能够将 memos 自动转发到 Channel 里面,于是我在 Memos 代码中做了如下定制,具体可以参考我的代码https://github.com/edonyzpc/memos/blob/main/api/v1/memo.go#L424(由于是个人定制所以 Channel ID 直接写死,代码 Review 的时候轻喷🤣)。效果如下: