这次想分享的项目是:一台N100小主机搞定全家数据:书库、相册、笔记、同步与备份的容器化部署实录。起因是上个月我盘点家里设备时发现,电子书散在电脑和平板里,手机照片囤了三个网盘,笔记软件换了又换,两台旧笔记本都存着不同版本的工作文档,真正要用的时候永远找不到最新那份。折腾了一周多,最后把方案收敛成一台N100迷你主机,Docker Compose统一编排,跑起了书库、相册、笔记、文件同步和自动备份五个服务,局域网内全设备访问,用了一两个月相当稳定。
这篇文章会把从选型、系统初始化、Compose编排、反代安全到排障维护的完整过程写一遍。适合家里有多台设备、数据比较散、又不想买成品NAS的玩家参考,也适合刚接触自建服务的同学拿来做一套能落地的“家庭私有化数据中心”范例。
1. 选型定案:N100小主机和旧笔记本之间,我为什么选了前者
1.1 需求清单先理清楚,选型才不会跑偏
动手买设备之前,我先把需求列成了一张表。不是所有数据都值得上服务器,只有那些“每天都要用、跨设备同步、有长期保存价值”的数据才值得放进这个项目。
我的清单如下:
- 电子书统一管理,要有网页书架,能在手机平板上打开阅读,最好支持格式在线转换
- 手机相册自动备份,人脸识别、按时间线浏览这类功能尽量齐全
- 笔记要求支持Markdown,桌面端移动端都能实时同步
- 两台电脑和一个移动硬盘需要做文件同步,不想再手动拷来拷去
- 所有数据要有自动备份,硬盘出问题能恢复
- 设备放客厅角落,功耗尽量低,噪音必须低
这条清单直接决定了后面的选型方向。其实很多人买设备之前没想清楚就跑去看硬件参数,结果要么性能过剩、要么硬盘位不够,最后发现核心需求没被满足。
1.2 几类方案的实际对比
我把市面上主流的自建方案都过了一遍,包括成品NAS、旧笔记本改服务器、树莓派、迷你主机,直接列个对比表。
| 方案 | 优点 | 缺点 | 适合人群 |
|---|---|---|---|
| 成品NAS | 系统成熟、APP完善、上手快 | 价格高、硬件性能保守、系统相对封闭 | 不想折腾、买来即用的用户 |
| 旧笔记本 | 零成本、自带电池和屏幕 | 功耗高、接口少、硬盘位不足、常年运行有散热隐患 | 手头有闲置电脑、想先验证需求的人 |
| 树莓派 | 体积小、功耗极低 | 性能弱、内存吃紧、跑不动带AI功能的相册服务 | 纯跑轻量服务的极客 |
| N100迷你主机 | 功耗低、性能足够、接口丰富、体积小 | 需要自己动手装系统维护 | 愿意折腾、想要灵活性的玩家 |
我最初也考虑过直接用一台旧笔记本,毕竟零成本。但仔细算了一笔账就放弃了:那台笔记本待机功耗在20W上下,满载能到45W,加上风扇噪音和电池鼓包风险,全年不断电运行并不划算。而且笔记本只有一个2.5寸硬盘位,相册原始文件加上备份根本塞不下。
树莓派4B我有,性能确实太勉强。Immuich要跑机器学习模型做人脸识别,树莓派虽然能装但缩略图生成和识别速度能慢到让人失去耐心。N100这颗四核小处理器虽然谈不上多强,但应付家庭级别服务绰绰有余,关键点在于它自带两个2.5G网口和M.2加SATA双盘位,后续扩展余量明显更大。
1.3 为什么强调“单台搞定全部”,而不是每个服务一台设备
最早我有个错误的倾向:一个服务装一台设备。书库放旧笔记本、相册放树莓派、同步再开一台小主机。后来发现这纯粹给自己找麻烦。服务越多,设备越多,意味着要管理的系统、要记得的密码、要处理的故障点成倍增加。某个设备一关机,全家服务瘫一半,排查问题要同时看三台设备的日志。
所以这个项目的核心原则就是:所有服务收敛到一台机器上,用Docker Compose管起来。单台设备的优势很直接:
- 硬件资源可以共享,N100空闲时功耗极低,忙时全力输出
- 备份只需要围绕一个宿主机做策略,不用考虑跨设备协调
- 系统维护只要在一台机器上执行,重启、升级、换盘都简单
- 故障面收敛,出现问题优先查Docker日志,不用满屋子找是哪台设备挂了
实测下来N100加上两根8G内存,同时跑Calibre-Web、Immich、Joplin Server、Syncthing、PostgreSQL、Redis、Nginx,内存占用大致在70%左右,CPU大部分时间在个位数徘徊,完全够用。选它的另外一个理由是,这类小主机大多支持DDR4笔记本内存和NVMe固态,后续扩容成本低,二手件也好找。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 系统初始化与存储布局:那些容易被忽略的“地基”问题
2.1 Debian 12安装时的分区和固件细节
设备到手第一步是装系统。我选了Debian 12,原因很简单:稳定、资源占用低、软件源里东西全,而且Docker的兼容性最好。自带桌面环境没必要,服务器版镜像就够。
安装时有一个经常被忽略的点:/boot分区不要用默认的几百MB。Debian 12的内核更新比较频繁,旧内核不会自动清理,过小的/boot分区很容易在几次升级后写满,导致apt更新失败。我直接给了2GB。另一个是swap分区,这台机器内存虽然有16G,但Immich缩略图生成时偶尔会冲高,所以我额外划了8G的swap文件而不是swap分区,方便以后调整。
固件方面,如果你的N100小主机用的时Intel i226-V网卡,Debian 12自带的内核通常能直接识别,不需要额外装驱动。装完系统后先用lspci确认网卡型号,再检查ip link里的网卡名称。我这里两块网卡都在,命名是enp1s0和enp2s0,第一块接路由器,第二块暂时空着,以后可以接另一个网段做隔离。
2.2 存储目录怎么规划,才不会被Docker卷绕晕
很多新手部署Docker服务很随意,每个服务自己创建volume,时间一长数据散落在/var/lib/docker/volumes里,备份的时候根本不知道哪些数据需要备份、哪些可以丢弃。我在这个项目的初期就把存储绑定挂载做了统一规划。
宿主机上建了这样的目录结构:
bash复制/srv/data/
├── books/ # Calibre书库原始文件
├── photos/ # Immich照片原始文件和上传目录
├── notes/ # Joplin笔记数据
├── sync/ # Syncthing同步目录
└── backup/ # 备份暂存区
所有容器的数据目录都显式挂载到/srv/data下的对应子目录,容器本身的镜像层和临时数据才放在Docker volume里。这样做的直接好处是:备份时只需要打包/srv/data这一个根路径,恢复时也只需要把这个目录恢复到新机器的同样位置,容器配置按原样启起来数据就回来了。
另一个细节是磁盘挂载。N100小主机通常有两个盘位,我做了分工:M.2 NVMe固态装系统和Docker镜像,SATA机械盘挂载到/srv/data专门存数据。系统盘和数据盘分开的最大价值在于,以后系统崩了重装不会碰数据盘,数据盘坏了也只需要替换备份,不用动系统。
2.3 基础系统的三个初始化动作
系统装好之后,我做了三件基础工作,顺序不能乱。
第一,SSH改用密钥登录。编辑/etc/ssh/sshd_config,把PasswordAuthentication设为no,然后重启sshd。这一步做完,靠扫端口暴力破解密码这条路基本就堵死了。别在刚装完系统的时候急着暴露任何端口,先把密钥配好再考虑服务。
第二,开启UFW防火墙。Debian默认没开防火墙,我执行了这样一组命令:
bash复制ufw default deny incoming
ufw default allow outgoing
ufw allow 22/tcp
ufw allow 80/tcp
ufw allow 443/tcp
ufw enable
刚开始只放行SSH、HTTP和HTTPS。其他服务端口一律不开,后文会说明为什么这样做反而更安全。
第三,确认时区和时间同步。家庭服务器常年在角落运行,时间错了会导致定时任务乱跑、日志时间错乱。我执行:
bash复制timedatectl set-timezone Asia/Shanghai
timedatectl set-ntp true
设置完用timedatectl确认NTP服务同步正常。这三步看着基础,但绝大多数自建服务出问题都离不开SSH暴露、端口乱开、时区不对这三件事。
2.4 Docker与Compose的安装和日志回收
Docker我用的官方源安装,装完后顺手安装docker-compose-plugin,Compose v2直接集成在Docker CLI里,不用单独维护Python版本的docker-compose。
Docker跑久之后最容易被忽视的是日志文件膨胀。某个容器日志写得多,几天就能吃掉几个G磁盘。我在/etc/docker/daemon.json里限制了单容器日志大小:
json复制{
"log-driver": "json-file",
"log-opts": {
"max-size": "10m",
"max-file": "3"
}
}
改完重启Docker生效。这是我在多个项目里验证过性价比最高的配置,一行设置能省掉后续无数手工清日志的活。
3. 四个核心服务的Compose编排与配置要点
3.1 书库:Calibre-Web + Calibre命令行,先把元数据搞干净
电子书管理我选了Calibre-Web,它本身是Calibre的Web前端,界面干净,支持在线阅读、OPDS订阅、格式转换。但它不替你做格式转换,真正干活需要容器里的Calibre程序,所以我用了linuxserver镜像并挂载了一个mod。
在Compose里书库服务长这样:
yaml复制 calibre-web:
image: lscr.io/linuxserver/calibre-web:latest
container_name: calibre-web
environment:
- PUID=1000
- PGID=1000
- TZ=Asia/Shanghai
- DOCKER_MODS=linuxserver/mods:universal-calibre
volumes:
- ./calibre-web/app:/app
- /srv/data/books:/books
depends_on:
- caddy
restart: unless-stopped
挂载mod后容器内才有ebook-convert命令,Calibre-Web做在线格式转换才能依赖它。但有一点先说清楚:别把Calibre-Web当导入工具用。它在页面上导入书籍只是把文件扔进书库,不会自动下载元数据、不会整理封面、不会生成metadata.db。真正的书库整理我是在电脑上用Calibre客户端把书全部洗一遍:补齐书名、作者、封面、检查格式,生成好metadata.db之后再让Calibre-Web指向这个书库目录。
如果你直接让Calibre-Web接管一堆散乱的电子书文件,书架会乱得没法看。我整理完一千多本书,Calibre客户端花了大概半小时,这个时间的投入非常值。
3.2 相册:Immich的参数调优与目录映射
相册服务选择上,Immich最近几年进步非常快,自动备份、时间线、人脸识别、关键词搜索都有,体验上已经很接近主流云相册。它后端依赖PostgreSQL和Redis,架构相对重,但既然是一台16G内存的机器,跑起来并不吃力。
关键不在服务本身,在几个容易被坑的点。第一个是时区。Immich的容器环境必须显式设置TZ,否则手机上传的原始照片会显示成UTC时间,所有时间线错八个小时。第二个是上传目录。这里我做了专门挂载:
yaml复制 immich-server:
image: ghcr.io/immich-app/immich-server:release
container_name: immich-server
environment:
- TZ=Asia/Shanghai
- DB_HOSTNAME=immich-db
- DB_USERNAME=immich
- DB_PASSWORD=${DB_PASSWORD}
- DB_DATABASE=immich
- REDIS_HOSTNAME=immich-redis
volumes:
- /srv/data/photos/upload:/usr/src/app/upload
restart: unless-stopped
第三个是机器学习容器的模型下载。Immich人脸识别依赖的模型文件在首次启动时会自动下载,这台小主机是正常的家庭宽带环境,下载过程比较慢,而且偶尔会卡住。解决办法是下载完成后把模型缓存目录也放到数据盘,避免容器重建后重新下载。模型缓存路径因版本而异,我是通过docker exec进容器查看实际缓存目录后,再用绑定挂载把它的宿主路径固定下来。
实际跑起来之后,一个四百多G照片量级的上传目录,N100生成缩略图速度还不错,人脸识别在夜间空闲时会批量跑完。手机上配合Immich App设置“仅在连接WiFi时自动备份”,基本是零干预状态。
3.3 笔记:Joplin Server的数据库和备份关系
笔记我用的是Joplin,配合Joplin Server做同步。它支持Markdown,客户端覆盖Windows、macOS、Linux、Android和iOS,数据完全自主掌控。
Joplin Server本身还是轻量的,但它依赖PostgreSQL。我的思路是让笔记和相册共用同一个PostgreSQL实例,数据库实例单独部署,两个服务通过不同的database隔离。这样既能减少容器数量,又能保证数据独立。
Joplin Server部署的一个关键参数是APP_BASE_URL。这个必须设置成客户端最终访问的地址,比如https://joplin.local.lan或者基于IP的本地域名,不能写http://localhost:22300,否则安卓App从局域网访问的时候,服务端生成的链接全是错的,客户端会报“无法同步”。
注意,Joplin Server本身的数据库里只存笔记内容和版本历史,附件文件默认存放在它的文件系统目录里。所以备份时要同时备份数据库和这个文件目录,只备份其中一个都会导致恢复不完整。
3.4 同步:Syncthing的端口和文件夹策略
文件同步我选了Syncthing,这个工具没有中心服务器,设备之间点对点直传,局域网速度可以跑满千兆甚至更高。核心组网思路是这样的:
- 手机照片交给Immich自动备份,不放在Syncthing里
- 两台电脑里的工作文档、常用软件配置,通过Syncthing双向同步
- 移动硬盘作为单向接收方,做第二份实时副本
Syncthing默认开三个端口:8384是Web管理界面,22000是设备间传输端口,21027是发现协议UDP端口。其中22000这一项要注意,如果你部署在只有一块网卡的家庭网关后面,可以直接映射到宿主机,但如果宿主机还跑了其他服务,建议在Syncthing的GUI里设置一个较难猜的API密钥,避免局域网里的其他设备直接改同步配置。
我用的是linuxserver的镜像,设置PUID=1000 PGID=1000,数据目录挂载到/srv/data/sync。文件夹设计上,我建议同步根目录只放一个共享文件夹,然后在里面按用途建子目录,比如work、life、software。不要一开始就建一堆同步文件夹,否则每台设备都要挨个接受共享,维护成本会迅速上升。
3.5 一条Compose还是多条Compose:我的取舍
我的做法是分成了三条Compose文件,而不是把所有服务塞进一个docker-compose.yml。这样区分的一条Compose管基础依赖,包括PostgreSQL、Redis、Caddy反代;第二条管核心应用,包括Calibre-Web、Immich、Joplin Server、Syncthing;第三条管备份工具。原因很简单:相册升级频繁,单独跑一条Compose升级时不影响书库和笔记;基础数据库很少升级,单独管理不容易误操作。
每条Compose文件放在独立目录,各自配有.env文件存敏感变量。容器网络方面,我创建了两个Docker网络,一个叫db-net承载数据库相关通信,一个叫app-net承载应用间的通信。实际上这两个网络分开的意义不大,真正重要的是别把数据库端口映射到宿主机,只在Docker网络内部访问。
4. 统一访问入口与安全加固:不把裸端口暴露在局域网里
4.1 为什么我不给每个容器单独做端口映射
很多人的习惯是装一个服务就把端口映射到宿主机,书库用8083、相册用2283、笔记用22300,最后维护一个长长的端口列表。我这次刻意改变做法:所有Web服务都不直接暴露宿主机端口,只有Caddy发布80和443端口。
这么做,第一是解决Docker和UFW防火墙的一个老问题。Docker的-p端口映射会改动iptables规则,效果上是绕过UFW直接开放端口的,即使UFW默认deny incoming,映射出来的端口在局域网里照样能访问。你在UFW里只放行了22、80、443,结果容器的8083端口依然裸露在局域网里,防火墙形同虚设。统一走Caddy反代,宿主机实际开放的端口只有80和443,UFW才真正具备约束力。
第二是记忆成本低。无论访问什么服务,地址都是https://服务名称.本地域名,打开浏览器书签就能用,不用记端口。
4.2 Caddy反代配置示例,自动本地HTTPS
Caddy最大的好处是自动获取和管理HTTPS证书,本地环境下我用自建CA的方式做HTTPS,浏览器信任一次就能长期生效。当然如果你要做外网访问,Caddy也能自动从Let‘s Encrypt申请证书,具体取决于你的解析方式。这里给一个本地反代的Caddyfile示例:
text复制# Caddyfile
{
local_certs
}
calibre.local.lan {
reverse_proxy calibre-web:8083
}
immich.local.lan {
reverse_proxy immich-server:2283
}
joplin.local.lan {
reverse_proxy joplin-server:22300
}
容器和服务名之间通过Docker网络解析,所以反向代理地址可以写成容器名,不需要写宿主机IP。calibre-web、immich-server、joplin-server这些名称要和Compose里的服务名一致,否则Caddy解析不到容器地址。我把这条Compose里的容器网络设置为同一个bridge网络,容器之间自然互通。
4.3 容器权限与目录权限的收口
安全加固不只是防火墙,容器的运行权限同样重要。我给每个服务配置里尽量加上read_only、cap_drop这样的参数,实际配置如下:
yaml复制 calibre-web:
security_opt:
- no-new-privileges:true
cap_drop:
- ALL
在linuxserver镜像上直接cap_drop: ALL通常没问题,因为它们启动进程不需要特殊权限。对Immich这类复杂服务,直接drop ALL可能会有副作用,所以我没有强行对所有容器套用,而是区分对待。原则就是:能不给的权限就不给,容器必须跑在非root用户下。
目录权限方面,我统一把/srv/data下的子目录属主设为1000:1000,和PUID/PGID保持一致。这样一个用户ID贯穿宿主机文件系统和容器内部,避免了典型的“宿主机看到文件归root,容器内进程无法写入”的权限错乱问题。
4.4 把备份拆成“本地备份+异机备份”两层
安全加固最后一步是备份。我用restic做备份,它的优势是增量备份、加密、去重,而且恢复工具链比较成熟。备份策略分了二层:
- 本地备份:每天凌晨3点把
/srv/data备份到同一台主机上挂载的移动硬盘。因为只是在同一个物理设备上复制,所以不指望它防火灾、防硬盘整机损坏,只防“误删、服务配置改坏、数据库损坏”这类软故障。 - 异机备份:每周一次,通过restic备份到一台旧笔记本共享的目录里。这样即使小主机整机挂掉,数据也还在另一台设备上。
实际上,这两层备份现在的效果我验证过:一次迁移测试中直接拔掉数据盘,找一台全新机器装上系统,把restic备份恢复到新盘,按原Compose重启服务,所有数据原样回来。关键在于,备份之后一定要演练恢复,否则到真出事那天你才发现备份从来就没成功过。
5. 上线头两周踩过的坑:时区、权限、日志与重启恢复
5.1 时区没设置,照片和笔记时间差了8小时
这个坑是我第一周就踩的。当时上传了一组下午拍摄的照片,结果Immich时间线里全挂在凌晨,一查发现原始照片的EXIF时间是本地时区,但Immich容器内部用的是UTC,处理时直接拿UTC当本地时间用了。
解决很简单,在所有应用容器上加环境变量:
yaml复制environment:
- TZ=Asia/Shanghai
但要注意,改完时区后已经入库的旧数据不会自动修正。Immich里部分老照片的时间必须在后台设置里重新触发一次元数据刷新,或者用它的“时间校正”功能手动调整。因此我的建议是:部署时第一时间就把TZ环境变量写好,不要等数据多了再补。
Joplin也踩了类似问题,同步到服务端的笔记创建时间一度差8小时,后来我在Joplin Server的配置里也加上了TZ环境变量,同时把数据库时区设为系统时区才彻底解决。
5.2 PostgreSQL目录权限冲突:容器起不来的经典原因
部署Immich的过程里,依赖的PostgreSQL容器一直起不来。docker logs看报错,看到的是chmod: changing permissions of '/var/lib/postgresql/data': Operation not permitted。查了一圈,原因是宿主机上挂载给PostgreSQL的目录属主是root,而容器内的PostgreSQL进程要求目录属主是uid 999或postgres用户。
处理方式很直接:
bash复制chown -R 999:999 /srv/data/postgresql
不同镜像的PostgreSQL UID可能不同,官方postgres镜像默认用的是uid 999,linuxserver的版本可能又是另一套。遇到这个报错别急着改权限为777,先确认镜像里的uid,再对应chown。不然权限设置得太宽,数据库文件会被宿主机上其他进程碰到,有安全风险。
这个坑的本质是容器数据卷归属问题。为了避免以后每个服务都来一次“chown摸底”,我建目录的时候就直接把所有数据目录设成1000:1000,再按需要单独调整个别目录。
5.3 端口冲突、日志膨胀、容器自启动
上线期间还遇到两个不大不小的问题。一个是Syncthing的8384管理端口和某个服务的管理端口冲突,实际影响不大,但排查时要花时间。二是有一次我手动重启宿主机之后,有一半容器没跟着起来。排查发现是因为这些Compose文件里的服务没有加restart: unless-stopped,或者依赖的服务没先启动。
统一加上这个重启策略后,宿主机重启恢复基本不需要人工干预。日志方面虽然已经在daemon.json里限制了单容器日志大小,但我仍然在宿主机上加了logrotate配置,专门处理系统日志和Docker容器日志的轮转,避免/var/log目录积压。
5.4 一次完整的恢复演练:restic restore到新机器
这套配置真正让我心里踏实的是一次恢复演练。我在小主机正常运行时,把/srv/data完整备份到移动硬盘,然后模拟“数据盘损坏”的场景。
- 在另一台测试机上装好Debian和Docker
- restic初始化仓库后执行
restic restore latest --target /srv/data - 把Compose文件从Git仓库拉下来,修改数据路径后启动
- 服务起来后检查Calibre-Web书架、Immich照片、Joplin笔记、Syncthing文件,全部一致
整个恢复过程大约40分钟。这次演练暴露了一个小问题:restic恢复的目录属主是执行恢复的用户,不是原来的1000,几秒钟就修好了。但这个坑如果在真实故障时遇到,可能会让整个恢复流程卡住,建议在恢复流程文档里单独写一条“恢复后执行chown -R 1000:1000”。
6. 功耗、长期维护与下一个阶段的计划
6.1 功耗实测:待机8W,一个月不到7度电
设备运行在客厅角落,我用一个功率计插座测过一周的功耗:待机状态大概7到9W,负载较高时冲到20W左右,平均下来约10W。一个月耗电量大概7度,按照居民电价折算不到5块钱。这比我那台旧笔记本动辄几十瓦的功耗理想太多,也正因为功耗低,我不用纠结“24小时开着是不是浪费”,直接让它常驻运行。
从长期角度看,一台低功耗小主机的成本核心其实是硬盘,N100本身非常省电,噪音基本可以忽略。这也是我推荐这类设备做家庭服务器的核心原因:性能刚刚好,功耗刚刚好,生命周期和升级空间都比较理想。
6.2 日常维护的三个动作:升级、体检、修剪
项目稳定运行后维护其实很轻。我维护的日常动作就是三个。
升级方面,我每两周执行一次docker compose pull加docker compose up -d,更新应用镜像。但数据库镜像不会轻易升级,尤其PostgreSQL大版本升级容易出兼容性问题,只做安全更新。
体检方面,用smartctl -H /dev/sda看一眼硬盘健康状态,用docker stats观察内存和CPU占用趋势,用df -h看数据盘剩余空间。这一套不到一分钟。
修剪方面,定期执行docker system prune -f清理悬空镜像和构建缓存,收回那些升级后残留的临时空间。
这三个动作我全部固化成一个小脚本,放在宿主机/usr/local/bin/maintenance.sh,需要的时候手动执行一次。这台机器的定位是家务活,我不想为它维护一套复杂的监控平台,越简单越好。
6.3 后续我还想做的几件事
设备跑稳定后我又规划了几件事,目前还没全部落地,可以作为这套方案的扩展方向。
第一,把Compose文件和Caddy配置纳入版本管理。我目前是把它们放在一个Git仓库里,但还没做到“一次命令全新部署”,下一步想整理成模板变量,让部署完全可复现。
第二,给相册服务加一个定期清理流程。Immich备份的照片里,截屏和重复照片占了不少空间,需要定期筛选整理,而不是一味扩容硬盘。
第三,远程访问方案我还在研究,思路倾向于用IPv6直连并在Caddy自动申请证书,尽量绕过中转服务器。这套方案不影响局域网体验,我打算确认稳定后再写一篇文章记录。
自建家庭服务器这件事,最怕的不是硬件不够强,而是一开始没想清楚需要什么、数据放哪里、坏了怎么恢复。这套N100加Docker Compose的方案,对我来说最大的价值不是“跑起来有多酷”,而是“所有数据在本地、所有备份可验证、所有配置可重现”,家里的数字资产终于不再是一堆散落的孤岛了。
