离大谱: NextCloud 同步之 Obsidian 笔记消失术

 · 10 分钟阅读
 · 教授
文章目录

事件起因


熟悉的朋友可能知道,笔者一直以来都是使用 Oracle 的 OCI 实例在自托管一些日常自用的互联网服务。Oracle 提供的 ARM 的 4 核 24G 的实例性能确实非常不错,结合 Docker 、 Traefik 及 Portainer 能够非常便捷地管理各项服务。

当然,也正是因为过于便捷,往往实现服务的启用都非常简单直接,不太会考虑全局的一些依赖关系。后来随着 Docker 中挂起的 Container 越来越多,逐渐发现一些现有服务的不确定性。

譬如,在 docker-compose.yml 文件中,之前是没有指定每个容器的内部 IP 地址的,这就导致每当重启 docker daemon 或者 reboot 的时候,都会根据容器随机启动的顺序自动获取内部 IP 。其实通常情况下也都还好,不会有太大问题。之所以会成为问题,是因为实例上部署的 WordPress 需要额外设定 extra_hosts 为 Traefik 的内部 IP 地址才能避免出现“您的站点不能完成环回请求”的错误(这又是另一个故事了,有机会再详细聊一下)。由于 extra_hosts 的内容不太适合每次都去修改,能够想到的便是在自定义的 Network 下固定 Traefik 的 IP 。

这就是最初最单纯的想法。

其实这个想法一点问题都没有。

只是,谁让自己没事儿总爱手贱呢,这谁顶得住啊!

下面就来好好盘一盘,笔者是怎么一步步把自己带到坑里去的,希望以后能以此为戒,慎独!慎独!慎独!

故事还原


自从上次部署 NextCloud 已经过去几个月了,一直在多个终端设备间进行同步备份,自我感觉相当良好,甚至直接就把所有笔记以及一些非常重要的文件都放了进去。

坦白讲, NextCloud 的产品功能和性能都还是蛮好的,多端同步相当丝滑。只是在经历这次麻烦之前,自己并没有意识到 NextCloud 其实只是一款同步软件,并不能作为备份系统来使用。也正是没有树立正确的认知,才草率地以为只要本地设备的文件还在,就一定会自动将本地文件同步至服务器端。

所以,这其实终究还是认知导致的问题。

那么,我都做了什么才差点导致了悲剧的发生呢?步骤如下,但凡中间哪一步没做,都不太可能上演 Obsidian 笔记消失术:

  • 重写 NextCloud 的 docker-compose.yml 文件,在 Network 中指定网络的内部 IP;
  • 突发奇想,反正都要再拉起一遍容器了,干嘛不干脆顺便升个级?那就 sudo docker compose down 了,顺便把镜像也嘎掉好了,反正能拉最新的版本,怕啥;
  • 于是 sudo docker image prune -f -a ,嘎掉嘎掉,统统嘎掉;
  • 镜像都嘎掉了,是不是该把本地持久化的容器数据也一块儿给嘎了啊?反正回头再一同步都会把本地文件同步上去,省的还把一堆的带版本号的文件留在服务器上了,光占空间了,那可不行。于是把心一横, rm 大法一出手,便知有没有啊,嚯嘿;
  • 好了,干完这一切,那接下来就是见证奇迹的时刻了,赶紧 sudo docker compose up 先看看容器是不是正常挂起,嘿,你猜怎么着,成了!我太娘的还真是个天才!
  • 那就赶紧访问域名把管理员账号都配置好呗,嘿嘿,机智如我,必须把用户名和密码都配置成原先的,这样后面岂不是就直接同步起来了。天才,天才的想法!机智如我,哦耶!赶紧网页端登录一下,不错,一切正常,就是管理设置里面的外部存储还是提示“smbclient” 未安装,无法挂载 "SMB/CIFS ", "使用 OC 登录的 SMB/CIFS",请联系管理员安装。小问题,解决它(详细方法见后文反思部分);
  • 成功挂载外部存储以后,看看日志,一切顺利,挺好,那就让容器常驻后台吧, sudo docker compose down && sudo docker compose up -d 。基本都齐活了,那开始配置客户端吧;
  • 精彩开始。 MacBook 上客户端原先是登录状态,现在提示连接错误,点开账号一看,头像都灰了。于是点开账号旁边的三个点,心想,看来还是得再登录一遍啊。所以,并没有选择 Remove account ,而是点了 Log out 。重新输入账号信息之后(甚至还专门重新生成了应用密码),哇塞,直接就成功登录了呃。心中暗喜,果然还是靠谱的!然后点开 Settings 一看,之前设置里的同步文件夹都还在,而且状态栏里面的软件图标也开始显示在同步了;
  • 不错不错,看来过不了多久就可以全部同步完成了,还得是我啊,这效率,杠杠滴!哈哈哈;
  • 没几秒,同步完成!这时候内心隐隐感觉,事情似乎有点不对劲啊,本地笔记好歹也有一百多 M 呢,怎么这么快???

然后点开同步记录,果然是悲剧了呃,全都是删除文件的信息......

于是去 Finder 里查看,什么情况???除了正在 Obsidian 里编辑的 3 篇笔记之外,其他所有同步文件夹内的资料全没了!然后赶紧打开 Trash 查看,居然也没有!

慌了,真的慌了,人都呆了。

立马搜索一下,在官方论坛也遇到有人发生过类似的情况。还专门有人回复大家说, NextCloud 只是同步工具,并不是备份工具,新挂起的容器在服务器端没有文件,因此它认为其他设备端也应该与服务器端同步。于是一顿嘎嘎乱杀,然后电脑上的文件直接被嘎掉了,甚至都不会问你也不会进垃圾桶, Obsidian 笔记大消失术就这么神奇地上演了。

心如死灰的感觉。

条条大路通罗马,你偏偏挑了一条,呃,水路?

然鹅,坚毅的天才是不可能被打败的。谁说水路就到不了罗马?

如今还是又好好地用上了啊,虚惊一场,虚惊一场 Screenshot 2023-09-14 at 11.10.34 PM.png

备份、备份、还得是备份!


凡事不要慌,先给自己一个大逼兜。

找回笔记的过程还是相对简单的。真正困难的是,其实并不能清楚地记得还有哪些重要文件丢失了,真的就这样没了。之所以能找回笔记,也仅仅因为平时使用最多,多端同步最及时,也会有定期的备份习惯,比如,定期 Merge 进 iCloud 里面。毕竟 Obsidian 在 iPad 端最方便的同步方式还是使用 iCloud ,所以有养成这样的定期备份习惯。只是可惜,依然还有一些最近的笔记丢失了,不过世间的事原本就很难完美,平常心,平常心。

在同步之外,备份是必不可少的一道步骤。

原本想在这篇文章里给出可行的备份方案分享给大家的,不过本文的重点在于强调混淆同步与备份的概念将带来怎样的灾难性后果,因此暂不做展开,后续有机会再单独写一篇文章进行分享。

经验教训


1. 关于 Docker 容器的网络

Docker 的网络配置其实具有丰富的功能,简单来讲网络是允许服务相互通信的 layer 。

通过 networks 元素,可以配置可在多个服务中重复使用的命名网络。要在多个服务中使用一个网络,必须使用 networks 属性明确授予每个服务访问权限。 networks 元素还有其他语法,可以提供更精细的控制。

我们首先来看一个简单的示例:

services:
  proxy:
    build: ./proxy
    networks:
      - frontend
  app:
    build: ./app
    networks:
      - frontend
      - backend
  db:
    image: postgres
    networks:
      - backend

networks:
  frontend:
    # Use a custom driver
    driver: custom-driver-1
  backend:
    # Use a custom driver which takes special options
    driver: custom-driver-2
    driver_opts:
      foo: "1"
      bar: "2"

这个Docker Compose 配置文件描述了一个多容器应用程序,其中包括一个代理服务 proxy 、一个应用程序服务 app 和一个数据库服务 db 。这些服务使用不同的网络来隔离它们的通信。在 services 部分, proxydb 服务是隔离的,因为它们不共用一个网络。只有 app 可以与两者对话。其中, frontend 网络用于连接代理和应用程序,而 backend 网络用于连接应用程序和数据库。此外,下方 networks: 部分还使用了自定义的网络驱动程序和选项来配置网络的行为。

相对而言, services 部分的内容非常容易理解。但是 networks: 部分则具有相当复杂的属性标签系统来帮助我们精细化地控制容器中的网络环境。

2. Docker 网络的属性标签

在谈论 Docker 网络的属性标签前,我们需要先理解清楚 Docker 默认的 3 种网络。

通过  docker network ls 可用查看服务器上的 Docker 网络信息,默认情况下应该类似这样:

$ docker network ls
NETWORK ID          NAME                DRIVER              SCOPE
1befe23acd58        bridge              bridge              local
726ead8f4e6b        host                host                local
ef4896538cc7        none                null                local

上面的输出显示,网桥网络与网桥驱动程序相关联。值得注意的是,网络和驱动程序是相连的,但它们并不相同。在本例中,网络和驱动程序的名称相同,但它们并不是一回事!上面的输出还显示,网桥网络的作用域是本地。这意味着该网络只存在于这台 Docker 主机上。所有使用网桥驱动程序的网络都是如此--网桥驱动程序提供了单主机联网功能。

这 3 种网络的区别如下:

  • bridge - bridge 网络是新容器的默认网络,除非指定不同的网络,否则所有新容器都将连接到该网络。
  • host - 使用主机的网络协议栈。
  • none - 关闭联网。

使用 hostnone 等内置网络的语法与自定义网络有所不同,因为这些网络隐含在 Compose 的范围之外。要使用它们,必须定义一个名称为 hostnone 的外部网络,以及一个 Compose 可以使用的别名(以下示例中为 hostnetnonet ),然后授予 services 使用其别名访问该网络的权限。示例:

services:
  web:
    networks:
      hostnet: {}

networks:
  hostnet:
    external: true
    name: host
services:
  web:
    ...
    networks:
      nonet: {}

networks:
  nonet:
    external: true
    name: none

了解到默认网络的这些特征后,我们再来看看 Docker 网络属性标签的含义:

  • driver: 指定该网络应使用的驱动程序。如果平台上没有可用的驱动程序, Compose 会返回错误信息。
  • driver_opts: 指定一系列要传递给驱动程序的键值对选项。这些选项取决于驱动程序。更多信息请查阅驱动程序文档。
  • attachable: 如果 attachable 设置为 true,那么除了服务外,独立容器也应能连接到该网络。如果独立容器附加到该网络,它就可以与服务和其他也附加到该网络的独立容器通信。
  • enable_ipv6: 启用 IPV6 网络。
  • external: 如果被设置为 true , external 指定在应用程序之外维护该网络的生命周期。Compose 不会尝试创建这些网络,如果网络不存在,则会返回错误信息。此外,除名称外,所有其他属性都无关紧要。如果 Compose 检测到任何其他属性,它就会以无效为由拒绝该 Compose 文件。
  • ipam: ipam 指定自定义 IPAM 配置。这是一个包含多个属性的对象,每个属性都是可选。详细可参考 ipam
  • internalinternal 设置为 true 时,可以创建一个外部隔离的网络。
  • labels: 使用标签为容器添加元数据,可以使用数组或字典。建议使用反向 DNS 符号,以防止标签与其他软件使用的标签冲突。示例可参考 labels
  • namename 设置网络的自定义名称,名称字段可用于引用包含特殊字符的网络。它还可以与 external 属性结合使用,以定义 Compose 要检索的平台网络,通常是设置成某个参数。

接下来我们看看实际应用的情况。

3. 配置容器使用固定 IP

回到故事的起点,为 Docker 容器配置固定 IP 。这里以 NextCloud 的配置为例。

本次故事中使用的 docker-compose.yml 文件如下:

version: '3'

services:
  nextcloud:
    image: nextcloud:apache
    container_name: nextcloud
    hostname: nextcloud
    restart: always
    ports:
      - 127.0.0.1:8888:80
      - 127.0.0.1:8443:443
    volumes:
      - ./nc_data:/var/www/html
      - /onedrive:/data/onedrive    # 数据持久化也映射 Rclone 网盘,方便挂载
    links:
      - db-nc
    depends_on:
      - redis-nc
      - db-nc
    networks:
      proxy:
        ipv4_address: 172.18.0.8
#        ipv6_address: 2001:3984:3989::10
    environment:
      - REDIS_HOST=redis-nc
      - APACHE_PORT=11000
      - APACHE_IP_BINDING=0.0.0.0
      - MYSQL_HOST=db-nc
      - MYSQL_DATABASE=database    # 不建议明文,可以使用docker secrets配置
      - MYSQL_USER=user            # 不建议明文,可以使用docker secrets配置
      - MYSQL_PASSWORD=pass        # 不建议明文,可以使用docker secrets配置
    labels:
      - traefik.enable=true
      - traefik.docker.network=proxy
      - traefik.http.routers.nextcloud.entrypoints=websecure
      - traefik.http.routers.nextcloud.rule=Host(`nc.yourname.com`)
      - traefik.http.routers.nextcloud.middlewares=default@file
      - traefik.http.routers.nextcloud.middlewares=error-pages

  redis-nc:
    image: redis:alpine
    container_name: redis-nc
    hostname: redis-nc
    restart: always
    networks:
      proxy:
        ipv4_address: 172.18.0.99
#        ipv6_address: 2001:3984:3989::10
    expose:
      - 6378

  db-nc:
    image: mariadb:10.5
    container_name: db-nc
    hostname: db-nc
    command: --transaction-isolation=READ-COMMITTED --binlog-format=ROW
    restart: always
    volumes:
      - ./db_data:/var/lib/mysql
    networks:
      proxy:
        ipv4_address: 172.18.0.9
#        ipv6_address: 2001:3984:3989::10
    environment:
      - MYSQL_DATABASE=database   # 不建议明文,可以使用docker secrets配置
      - MYSQL_USER=user           # 不建议明文,可以使用docker secrets配置
      - MYSQL_ROOT_PASSWORD=pass  # 不建议明文,可以使用docker secrets配置
      - MYSQL_PASSWORD=pass       # 不建议明文,可以使用docker secrets配置
    expose:
      - 127.0.0.1:3306:3306

volumes:
  db_data:
  nc_data:

networks:
  proxy:
    external: true
    # 下面的字段仅为对 Docker 网络属性标签使用的示例,
    # 当 external 设置为 true 时, 
    # Compose 并不会实际执行下方的属性标签。
    ipam:
      driver: default     # 单机版默认为 bridge,Swarm 版默认为 overlay
      config:
        - subnet: "172.18.0.0/16"   # subnet 也可在创建网络时指定
#        - subnet: "2001:3984:3989::/64"

配置文件整体还是非常简单清晰的,你完全预想不到就这也会引发一场 Obsidian 笔记的大消失术。

这里在 services 下,指定服务的网络 IP 即可,下方的 networks 部分甚至都不需要改动(上面加了一些标签供大家更好地理解 Docker 网络的属性)。

当然,如果涉及到使用多个网络,在 services 下各个服务中的 networks 标签的写法也有需要注意的地方。在 docker-compose.yml 中, label 标签的参数写法有 2 种: ListingMapping

同一个 label 标签下,只可用使用同一种参数写法,混用时则会让 Compose 报错。简单来说就是:

# Listing正确写法示例:
    networks:
      - default
      - proxy

# Mapping正确写法示例(注意网络名是以冒号结尾):
    networks:
      default:
      proxy:
        ipv4_address: 172.18.0.99
        ipv6_address: 2001:3984:3989::10

# 错误示例:
    networks:
      - proxy:
        ipv4_address: 172.18.0.99
        ipv6_address: 2001:3984:3989::10

# 错误示例:
    networks:
      - default
      proxy:
        ipv4_address: 172.18.0.99
        ipv6_address: 2001:3984:3989::10

往往细节的地方格外需要注意。

4. NextCloud 容器可能遇到的一些问题

a. 使用客户端登录失败

使用客户端登录时,可能会遇到The polling URL does not start with HTTPS despite the login URL started with HTTPS. Login will not be possible because this might be a security issue. Please contact your administrator.的问题,解决方法如下:

# 找到config.php文件:
cd ~/nc_data/config

# 编辑config.php文件
sudo nano config.php

然后:

# 更新及添加这部分:
  'overwrite.cli.url' => 'https://nc.yourname.com',
  'overwritehost' => 'nc.yourname.com',
  'overwriteprotocol' => 'https',

b. 掛載外部存儲

在Docker中安裝smbclient(每次重启机器或容器服务之后都要来一遍,神烦,不过好在已经挂载上的网盘依然在):

# 查看nextcloud實例的名稱
sudo docker ps

# 進入nextcloud的docker容器,nextcloud为容器名
sudo docker exec -it nextcloud bash

# 切換為容器內的root用戶,更新apt-get倉庫
apt-get update

# 安裝smbclient
apt-get install smbclient libsmbclient-dev

# 安裝smbclient的php擴展
pecl install smbclient

# 啟用smbclient
docker-php-ext-enable smbclient

# 隨後退出容器,重啟docker和守护进程即可
sudo systemctl restart docker
sudo systemctl daemon-reload

即便安装smbclient之后,挂载已映射到本地的 Rclone 远端磁盘时,仍然无法成功在后台将其加入外部存储中,因为容器外的磁盘并未映射入容器中,需要额外进行一次挂载。

# 参考docker-compose.yml文件,已在volume挂载
    volumes:
      - ./nc_data:/var/www/html
      - /onedrive:/data/onedrive

c. Docker 重新拉取 NextCloud 最新版镜像后,安全与设置警告出现数据库丢失索引

错误提示:

数据库丢失了一些索引。由于给大的数据表添加索引会耗费一些时间,因此程序没有自动对其进行修复。您可以在 Nextcloud 运行时通过命令行手动执行“occ db:add-missing-indices”命令修复丢失的索引。索引修复后会大大提高相应表的查询速度。
- 在数据表“oc_systemtag_object_mapping”中无法找到索引“systag_by_tagid”。
- 在数据表“text_steps”中无法找到索引“textstep_session”。

处理步骤:

# 进入容器的bash命令行
sudo docker exec -it nextcloud bash
# 参考官方指南进行修复
su www-data -s /bin/bash -c  'php occ db:add-missing-indices'

运行结果如下:

root@xxxxxxxxx:/var/www/html# su www-data -s /bin/bash -c  'php occ db:add-missing-indices'
Check indices of the share table.
Check indices of the filecache table.
Check indices of the twofactor_providers table.
Check indices of the login_flow_v2 table.
Check indices of the whats_new table.
Check indices of the cards table.
Check indices of the cards_properties table.
Check indices of the calendarobjects_props table.
Check indices of the schedulingobjects table.
Check indices of the oc_properties table.
Check indices of the oc_jobs table.
Check indices of the oc_direct_edit table.
Check indices of the oc_preferences table.
Check indices of the oc_mounts table.
Check indices of the oc_systemtag_object_mapping table.
Adding systag_by_tagid index to the oc_systemtag_object_mapping table, this can take some time...
oc_systemtag_object_mapping table updated successfully.
Adding additional textstep_session index to the oc_text_steps table, this can take some time...
oc_text_steps table updated successfully.

5. NextCloud 是同步工具,不是备份工具!

只同步,不备份,简直就是在刀尖上跳舞,可刺激了!

实在肝不动备份工具的分享了,写到这儿都 1 万 2 千多字了。

罢了,罢了,保命要紧,溜了,溜了。

总结


讲真,这次的风波算是给自己认认真真上了一课。此前并未养成良好的数据备份习惯,也从来都不觉得自用服务需要多重视稳定性,以为用户只有自己一个人便可以为所欲为的心态应该停止了。其实,作为单用户而言,自身数据的安全性需求往往更为突出,一不小心就全军覆没了,简直哭都没地方哭。

望诸君引以为鉴。

最后,还是说点开心的事儿吧。苹果周二开完发布会到现在,终于可以开始预约 Apple Watch Ultra 2 了,今天早上在地铁上完成了预约,现在就等着下周五收货了,开心!

image.png

接下来就等着周五晚上 8 点预约 iPhone 啦~

参考资料