搭建个人 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.microecs.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 充当反向代理:

  1. 为 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;
    }
}
  1. 使能反向代理,通过软件链接使能域名反向代理然后重新启动 Nginx systemd service 即可;

HTTPS

配置 HTTPS 是通过 Let's Encrypt 申请 SSL证书来完成的:

  1. 安装 certbot;
  2. 利用 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 账号。

  1. 创建 R2 bucket,如下图所示在 R2 Section 中点击 Create bucket 按钮创建 R2 bucket;
  1. 创建 R2 的 Access Key,如下图所示在 Manage R2 API Tokens 中点击 Create API Token 创建 Access Key 注意配置好读写权限;
  1. 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 备份配置定时任务,这样系统就自动执行备份程序:

  1. 创建备份脚本即上面提到的 auto-commit 脚本;
  2. 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

  1. 利用 memos.top 项目定制个人的 GitHub Pages,可以参考我的项目 edonyzpc/memos.edony.ink 主要是定制化了一个人相关的配置,还有 comment 相关的配置。GitHub Pages 配置可以参考文档:
    1. Configuring a publishing source for your GitHub Pages site - GitHub Docs
    2. About custom domains and GitHub Pages - GitHub Docs
  2. 在 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 就完成了,具体效果可以参考:

移动端

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 的时候轻喷🤣)。效果如下:

References

  1. What is Memos - Memos
  2. Telegram Bot API
  3. usememos/memos: A privacy-first, lightweight note-taking service. Easily capture and share your great thoughts.