对于个人知识管理,我非常热衷于搞一套自己的local-first和开源的self-host的服务,这样就能不依赖任何平台、数据完全自主可控、服务可随时迁移。不管是Obsidian、Hexo博客、以及本篇文章介绍的memos ,都是这个选择标准。
本文的主角,memos 是一个开源的轻量级笔记的服务,可以像发微博的方式记笔记,支持TAG标记和引用。具有账号系统和权限管理,网页访问随时登录,笔记可以公开或私有,非常灵活。我选择它是因为可以跟Obsidian互为补充,Obsidian还是太重了,在PC上好用但移动端体验不佳。所以我需要一个轻量级、随时可用的一种笔记服务。
本篇文章会介绍,如何在VPS上用Docker部署一个Memos服务,并结合Nginx绑定域名、Certbot签发以及自动更新SSL证书和定时备份memos数据库,还有我为该服务做的一些配置优化。
前置条件 实操本文前需要你具备以下条件:
具有一台VPS(最好是境外的,不用备案域名)
域名(文中以memos.example.com
为例)
熟悉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:~ 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:
查看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 ; } http { sendfile on ; tcp_nopush on ; tcp_nodelay on ; keepalive_timeout 65 ; types_hash_max_size 2048 ; include /etc/nginx/mime.types; default_type application/octet-stream; ssl_protocols TLSv1 TLSv1.1 TLSv1.2 TLSv1.3 ; ssl_prefer_server_ciphers on ; access_log /var/log/nginx/access.log; error_log /var/log/nginx/error .log; gzip on ; 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 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 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 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 0 3 1 * * certbot renew --quiet --pre-hook "service nginx stop" --post-hook "service nginx start"
意思是每个月1号的凌晨三点执行签发任务,并且执行前关闭nginx服务,执行完毕后再开启nginx。
问题处理 如果cerbot证书申请失败,记得检查本地端口占用情况,确保nginx服务已经退出,且不要有其他的程序占用80
和443
端口。 如果摸不准端口是否被占用,可以使用lsof
检查端口:
1 2 3 4 5 6 root@vultr:~ 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仓库的形式提交备份。
首先在Github上创建一个私有仓库
在VPS上,定位到memos的数据目录
创建一个git仓库,并做一次初始提交
创建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 apt-get install sqlite3 sqlite3 memos_prod.db ".dump" > memos_prod.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分别导入memo
和resouece
表。然后对resource表做下面的操作,确保这两项位空。
1 2 UPDATE resource SET blob = NULL; UPDATE resource SET internal_path = '';
配置优化 界面美化
字体修改为霞鹜文楷
接入bing的每日壁纸,这样每天打开背景都不同
半透效果
一些无用选项的隐藏
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-wrapper .text-base { font-size : 0.95rem }.text-sm { font-size : 0.85rem ; }#header-inbox { display : none;}#header-profile { display : none; }#header-about { display : none; }textarea { font-family : 'Courier New' , Courier, monospace;}body .flex .flex-row .justify-between .items-center > .text-gray-500 .dark :text-gray-400 { display : none;}.share-memo-dialog >.dialog-container { width : auto; }.w-56 { width : 12rem ;}.pt-16 { padding-top : 2rem ; }blockquote { border : 1px solid #246ad1 !important ; border-left : 4px solid #246ad1 !important ; position :relative; } .blockquote-center { background : none; } 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 } ::-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, jsonifyimport logginglogging.basicConfig(level=logging.DEBUG) app = Flask(__name__) @app.route('/webhook' , methods=['POST' ] ) def webhook (): data = request.json print ("收到的数据:" , data) 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的快捷指令(实现日程、记账、随笔等)、或者嵌入到博客里,就更为方便了。