搭建个人 Memos 服务
13 min read

搭建个人 Memos 服务

介绍一下自己是如何通过 telegram bot,GitHub Pages,cron task,Cloudflare R2,docker 等工具和代码自定义部署个人 memos 服务,也总结一下我自己对 memos 的简单思考
搭建个人 Memos 服务
Photo by Lala Azizli on Unsplash

按语

要非常感谢 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.

Public discussion

足迹