部署一个自托管的MEMOS笔记系统

Deploy a self-hosted MEMOS note system

对于个人知识管理,我非常热衷于搞一套自己的local-first和开源的self-host的服务,这样就能不依赖任何平台、数据完全自主可控、服务可随时迁移。不管是Obsidian、Hexo博客、以及本篇文章介绍的memos,都是这个选择标准。

本文的主角,memos是一个开源的轻量级笔记的服务,可以像发微博的方式记笔记,支持TAG标记和引用。具有账号系统和权限管理,网页访问随时登录,笔记可以公开或私有,非常灵活。我选择它是因为可以跟Obsidian互为补充,Obsidian还是太重了,在PC上好用但移动端体验不佳。所以我需要一个轻量级、随时可用的一种笔记服务。

本篇文章会介绍,如何在VPS上用Docker部署一个Memos服务,并结合Nginx绑定域名、Certbot签发以及自动更新SSL证书和定时备份memos数据库,还有我为该服务做的一些配置优化。

前置条件

实操本文前需要你具备以下条件:

  1. 具有一台VPS(最好是境外的,不用备案域名)
  2. 域名(文中以memos.example.com为例)
  3. 熟悉Linux系统和常用命令

部署

Docker

首先需要在VPS上需要安装docker:

1
2
apt-get install docker
apt-get install docker-compose

常用的docker命令:

1
2
3
4
5
6
7
8
9
10
11
12
13
# 列出所有镜像
docker images -a
# 删除镜像
docker rmi IMAGE_ID

# 列出所有容器
docker ps -a
# 停止容器
docker stop CONTAINER_ID
# 启动容器
docker start CONTAINER_ID
# 删除容器
docker rm CONTAINER_ID

Memos

然后用docker安装并运行memos的镜像,目前最新的版本是0.21.0

1
docker run -d --name memos -p 5230:5230 -v ~/.memos/:/var/opt/memos neosmemo/memos:stable

-p是指定端口,~/.memos/:/var/opt/memos是指定memos的数据存储路径。默认的就是放在~/.memos/目录下,可以根据需要更换目录。

执行后会产生下面几个文件:

其中的memo_prod.db就是memos的Sqlite数据库,可以通过DB Browser for Sqlite等工具打开,访问数据。

当执行完上面的docker命令之后,memos服务就已经启动了,可以使用localhost:5230来访问了。

容器开机自启动

先查看docker id:

1
2
3
root@vultr:~# docker ps -a
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
3f25b36e833e neosmemo/memos:stable "./memos" 4 hours ago Up 4 hours 0.0.0.0:5230->5230/tcp, :::5230->5230/tcp memos

然后通过update设置自启动:

1
docker update --restart=always 3f25b36e833e

关闭自启动:

1
docker update --restart=no

自定义域名

前面的docker命令执行完之后,就在本地启动了一个web服务。但只能以IP的形式访问,我们可以使用nginx来映射到域名。

首先安装nginx:

1
apt-get install nginx

查看nginx的默认配置文件:

1
2
3
nginx -t
nginx: the configuration file /etc/nginx/nginx.conf syntax is ok
nginx: configuration file /etc/nginx/nginx.conf test is successful

修改/etc/nginx/nginx.conf

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
user www-data;
worker_processes auto;
pid /run/nginx.pid;
include /etc/nginx/modules-enabled/*.conf;

events {
worker_connections 768;
# multi_accept on;
}

http {
##
# Basic Settings
##
sendfile on;
tcp_nopush on;
tcp_nodelay on;
keepalive_timeout 65;
types_hash_max_size 2048;
# server_tokens off;

# server_names_hash_bucket_size 64;
# server_name_in_redirect off;

include /etc/nginx/mime.types;
default_type application/octet-stream;

##
# SSL Settings
##

ssl_protocols TLSv1 TLSv1.1 TLSv1.2 TLSv1.3; # Dropping SSLv3, ref: POODLE
ssl_prefer_server_ciphers on;

##
# Logging Settings
##
access_log /var/log/nginx/access.log;
error_log /var/log/nginx/error.log;

##
# Gzip Settings
##
gzip on;
# gzip_vary on;
# gzip_proxied any;
# gzip_comp_level 6;
# gzip_buffers 16 8k;
# gzip_http_version 1.1;
# gzip_types text/plain text/css application/json application/javascript text/xml application/xml application/xml+rss text/javascript;

##
# Virtual Host Configs
##
include /etc/nginx/conf.d/*.conf;
include /etc/nginx/sites-enabled/*;
}

然后就可以在/etc/nginx/conf.d/目录下创建memo的nginx配置:

1
2
3
4
5
6
7
8
9
10
11
12
# memos.example.com.conf
server {
server_name memos.example.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;
}
}

这里只是添加了http的映射,不能开启https,还需要申请SSL证书后,再配置一个443的nginx配置,下文介绍。

SSL证书

Certbot

申请证书之前,请确保DNS已经被解析到机器的IP了。

安装certbot

1
apt-get install certbot

然后签发证书:

1
certbot certonly --standalone -d example.com

注意:执行命令时不能启动nginx和memos的服务,确保他们为关闭状态。
可以通过service nginx stop来关闭nginx服务,使用service nginx start

证书签发完毕之后会存放在/etc/letsencrypt/live/目录:

此时证书就申请好了。

然后需要编辑前面创建的nginx配置文件(/etc/nginx/conf.d/目录下),创建一个443 ssl的配置,走HTTPS:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# memos.example.com.conf
server {
listen 443 ssl;
server_name memos.example.com;

client_max_body_size 1024m;

ssl_certificate /etc/letsencrypt/live/memos.example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/memos.example.com/privkey.pem;

ssl_session_timeout 5m;

ssl_ciphers HIGH:!aNULL:!MD5;
ssl_prefer_server_ciphers on;

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;
}
}

这样我们的域名就支持HTTPS访问了。

定时更新证书

certbot签发的证书只有三个月,需要重新申请证书,为了自动化,可以设定crontab定时任务执行:

1
crontab -e

把下面的指令贴到后面即可:

1
0 3 1 * * certbot renew --quiet --pre-hook "service nginx stop" --post-hook "service nginx start"

意思是每个月1号的凌晨三点执行签发任务,并且执行前关闭nginx服务,执行完毕后再开启nginx。

问题处理

如果cerbot证书申请失败,记得检查本地端口占用情况,确保nginx服务已经退出,且不要有其他的程序占用80443端口。
如果摸不准端口是否被占用,可以使用lsof检查端口:

1
2
3
4
5
6
root@vultr:~# lsof -i :80
COMMAND PID USER FD TYPE DEVICE SIZE/OFF NODE NAME
nginx 1178482 root 6u IPv4 121041008 0t0 TCP *:http (LISTEN)
nginx 1178482 root 8u IPv6 121041010 0t0 TCP *:http (LISTEN)
nginx 1178483 www-data 6u IPv4 121041008 0t0 TCP *:http (LISTEN)
nginx 1178483 www-data 8u IPv6 121041010 0t0 TCP *:http (LISTEN)

如果使用certbot renew时出现下列错误:

可以尝试下列操作:

1
2
3
sudo apt-get update
sudo apt-get install --only-upgrade certbot
pip install --upgrade certbot urllib3 requests

数据库

定时备份

同样地,我们也可以定时备份我们的MEMOS数据库,数据安全永远是第一位的。

依然可以使用crontab来进行自动化备份,用GIT仓库的形式提交备份。

  1. 首先在Github上创建一个私有仓库
  2. 在VPS上,定位到memos的数据目录
  3. 创建一个git仓库,并做一次初始提交
  4. 创建crontab定时任务

关于备份的频率,我目前设置的是5分钟执行一次,可以根据需要控制频率。

而且,需要注意的是,不要直接使用git管理正在运行中的数据库,可能会造成数据库损坏。需要将其备份出来,用git管理这个备份。

我设置的.gitignore如下,作用是忽略./memos下所有的文件,只追踪backup*开头的文件:

1
2
.memos/*
!.memos/backup*

备份数据库的命令为:

1
sqlite3 memos_prod.db ".backup backup_memos_prod.db"

完整的crontab命令如下:

1
*/5 * * * * cd /root/.memos/ && /usr/bin/sqlite3 memos_prod.db ".backup backup_memos_prod.db" && /usr/bin/git add . && /usr/bin/git commit -m 'update' && /usr/bin/git push origin master

注意:crontab的命令,要以绝对路径,~等相对路径,不一定能正常工作。

数据库修复

如果访问memos时,数据库提示下面的错误:

1
database disk image is malformed

表示数据库损坏了,我们需要将里面的数据迁移出来,并且创建出一个新的数据库。

1
2
3
4
5
6
7
# install sqlite3
apt-get install sqlite3

# 将数据库备份到sql
sqlite3 memos_prod.db ".dump" > memos_prod.sql
# 通过sql创建新的数据库
sqlite new_memos_prod.db < memos_prod.sql

然后将旧的db移走或删除,把新的该名为memos_prod.db,再启动docker即可。

数据迁移

我之前的一些随笔,都是跟博客一起的,存储在一个单独的md文件中,诸如下面的形式:

1
2
3
4
5
6
## 2024-04-08 17:18
AAA,BBBBBBBBB,CCCCC

## 2024-04-08 17:19
DDDDDD,EEEEEEEEEEE,FFFFFFFF
![](img.png)

写了多年,已经有几百条内容了,如果需要手动搬运到Memos里,是一个比较繁琐的事情,我才不这么干。

结合GPT4+手动优化代码,生成了一份从md到memos数据库的Python代码,可以直接把上面格式的md文件,自动生成出符合Memos数据库表规范的csv文件,这样只需要手动把csv导入一次,就能够实现数据迁移了。

脚本:md2memos.py

执行命令会在md目录下生成两个csv文件,用于导入db数据库:

1
python md2memos.py --md MARKDOWN_FILE.md

注意:在生成csv导入memos的数据库时,需要把csv分别导入memoresouece表。然后对resource表做下面的操作,确保这两项位空。

1
2
UPDATE resource SET blob = NULL;
UPDATE resource SET internal_path = '';

配置优化

界面美化

  1. 字体修改为霞鹜文楷
  2. 接入bing的每日壁纸,这样每天打开背景都不同
  3. 半透效果
  4. 一些无用选项的隐藏

css代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
/* 修改字体 */
body{font-family: "LXGW WenKai Screen", sans-serif !important;}
/* 修改Memo字号 */
.memo-wrapper .text-base { font-size: 0.95rem}
/* 修改代码块字号 */
.text-sm { font-size: 0.85rem; }
/* 隐藏 通知 选项卡 */
#header-inbox { display: none;}
/* 隐藏 个人资料 选项卡 */
#header-profile { display: none; }
/* 隐藏 探索 选项卡 */
/* #header-explore { display: none;} */
/* 隐藏 about 选项卡 */
#header-about { display: none; }
/* 修改编辑器字体为等宽 */
textarea { font-family: 'Courier New', Courier, monospace;}
/* 隐藏via memos */
body .flex.flex-row.justify-between.items-center > .text-gray-500.dark:text-gray-400 { display: none;}
/* share memos width */
.share-memo-dialog>.dialog-container { width: auto; }

/* sidebar */
.w-56 { width: 12rem;}
/* comment */
.pt-16 { padding-top: 2rem; }

blockquote{
border: 1px solid #246ad1 !important;
border-left: 4px solid #246ad1 !important;
position:relative;
}
.blockquote-center{ background: none; }

/* #root>div:nth-child(1) */
body
{
background-image: url('https://bing.immmmm.com/img/bing?region=zh-CN&type=image');
background-position: buttom;
backdrop-filter: blur(10px);
background-size: contain;
}


#root main,#root header,#root aside {
background-color: rgba(244 244 245 / 60%) !important;
background: content-box !important;
border-radius: 5px !important;
}

#root main,#root header,#root aside>div:nth-child(2),#root aside>div:nth-child(3)
{
background-color: white;
border-radius: 5px;
}

.px-2{
background: content-box !important;
}
.border-r {
border-right-width: 0px !important;
}

/* 移动端顶栏 */
.sm\:pt-2 {
background: unset !important;
--tw-backdrop-blur: auto !important
}
/* 顶栏文字 */
/* .text-gray-700{
color: snow !important;
}*/

/* 设置滚动条的样式 */
::-webkit-scrollbar {
width: 5px !important;
height: 5px !important;
}
/* 滚动槽 */
::-webkit-scrollbar-track {
background: #eee !important;
}
/* 滚动条滑块 */
::-webkit-scrollbar-thumb {
border-radius: 5px !important;
background-color: #ccc !important;
}
::-webkit-scrollbar-thumb:hover {
background-color: rgb(247, 149, 51) !important;
}

js代码:

1
2
3
4
5
6
7
8
function changeFont() {
const link = document.createElement("link");
link.rel = "stylesheet";
link.type = "text/css";
link.href = "https://cdn.staticfile.org/lxgw-wenkai-screen-webfont/1.7.0/lxgwwenkaiscreen.css";
document.head.append(link);
};
changeFont()

S3服务

Memos的配置和发表的文本都是存储在sqlite的db中的,但是sqlite对二进制文件不友好。而且这些二进制文件放到db里也不好管理。所以可以配置一个S3的服务(如腾讯云COS),用于存储上传的文件和图片等。

配置
名称 TencentCOS
端点 https://cos.ap-guangzhou.myqcloud.com
地区 ap-guangzhou
访问密钥 AKIXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXf7g
SecretKey gZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZr
存储桶 memos-1888888893
存储路径 memos/{year}/{month}/{day}/{filename}

这样就能把Memos文本与文件分离了。

Webhook

虽然我部署了Memos,但是我依然希望,我之前的md数据能够同步Memos的修改。

而Memos也提供了调用webhook的配置,我利用flask也写了一个简易的webhook server,当Memos里发布或者修改了内容,都能触发,这样就可以操作数据,追加到md里了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
from flask import Flask, request, jsonify
import logging
logging.basicConfig(level=logging.DEBUG)

app = Flask(__name__)

@app.route('/webhook', methods=['POST'])
def webhook():
data = request.json
print("收到的数据:", data)

# 这里可以添加处理Webhook数据的代码

return jsonify({'status': 'success'}), 200

if __name__ == '__main__':
app.run(host='0.0.0.0', debug=True, port=5000)

结语

基于web的服务还是很爽的,无需客户端,浏览器打开即可访问,随时随地能把想法记录下来。而且在iOS上safari可以直接把页面发送到桌面,就像普通的app一样打开访问,体验很好。

另外,memos还有很有RESTful API,可以将它扩展到其他的服务里,如可以嵌入到IOS的快捷指令(实现日程、记账、随笔等)、或者嵌入到博客里,就更为方便了。

全文完,若有不足之处请评论指正。

微信扫描二维码,关注我的公众号。

本文标题:部署一个自托管的MEMOS笔记系统
文章作者:查利鹏
发布时间:2024/04/13 10:34
本文字数:3k 字
原始链接:https://imzlp.com/posts/30014/
许可协议: CC BY-NC-SA 4.0
文章禁止全文转载,摘要转发请保留原文链接及作者信息,谢谢!
您的捐赠将鼓励我继续创作!