[{"content":"Prometheus + Grafana 跑了快一年，机器层面的问题基本都能及时发现。但上个月出了件事：AdGuard Home 的进程不知为何退出了，那个 LXC 本身好好的，CPU、内存、磁盘一切正常，Grafana 上一片祥和——可全家都上不了网。我是被家里人问“网怎么又断了”才知道的。\n这件事让我意识到，“机器活着”和“服务能用”是两回事。前者归指标监控管，后者需要有个东西站在用户的角度，定期去访问一下。Uptime Kuma 就是干这个的：一个带网页界面的可用性监控工具，支持 HTTP、TCP 端口、DNS、Ping、Docker 容器等多种检查方式，配置全在网页上点，还能做状态页。\n部署 监控程序不能和被监控的东西放在一起，否则 Docker 虚拟机一挂，监控也跟着挂，谁也不报警。所以我把它放在了跑 Jellyfin 的那台 N100 小主机上（192.168.10.30），它和笔记本上的 Proxmox 是两台独立的物理机。\n# /srv/compose/uptime-kuma/compose.yaml services: uptime-kuma: image: louislam/uptime-kuma:2 # 固定大版本，小版本随 pull 更新 restart: unless-stopped volumes: - ./data:/app/data # 想监控本机容器的运行状态才需要这一行，见下文 - /var/run/docker.sock:/var/run/docker.sock:ro ports: - \u0026#34;3001:3001\u0026#34; environment: TZ: Asia/Shanghai 镜像只固定到大版本 2，平时 docker compose pull 就能拿到 2.x 里的最新小版本，跨大版本升级时再手动改。docker compose up -d 之后打开 http://192.168.10.30:3001，第一次进去会先让你选数据库类型、再创建管理员账号，家用选内置的 SQLite 就够了。\n我加的监控项 类型 目标 说明 DNS 用 192.168.10.53 解析 nas.home.example.com AdGuard Home 能不能正常解析 HTTP(s) http://192.168.10.21:2283 Immich 网页 HTTP(s) - 关键字 http://192.168.10.30:8096/health Jellyfin，响应里要有 Healthy HTTP(s) http://192.168.10.21:3000 Grafana TCP 端口 192.168.10.20:3493 NAS 上的 NUT 服务 Ping 192.168.10.5 Proxmox 宿主机 Push 备份脚本主动上报 下文细说 DNS 监控是这次的重点：解析服务器填 192.168.10.53，记录类型选 A，查的是在 AdGuard Home 里配过 DNS 重写的内网域名，这样既不依赖外网，结果也是确定的。Home Assistant（部署过程见这篇）也加了一个 HTTP 检查，看 8123 端口的网页能不能打开。\n每个监控项的检查间隔设为 60 秒，重试次数 3 次。重试很重要，不然容器升级重启的那几十秒也会触发一次告警，狼来了几次之后，就没人再看告警了。\n通知方式选 ntfy，填上家里 ntfy 服务的地址和主题，勾上“默认开启”和“应用到所有现有监控项”，以后新加的监控也会自动带上。\n用 Push 监控定时任务 最有用的其实是 Push 类型。前面几种都是 Uptime Kuma 主动去检查别人，Push 反过来：Uptime Kuma 给你一个专属地址，被监控的一方定期来访问它，超过设定时间没来，就判定为故障。\n这正好解决了 restic 备份那篇结尾留下的问题：备份失败了没人知道。这个备份脚本后来按 systemd timer 那篇改成了由 timer 定时触发，我在脚本末尾加了一行：\n# /usr/local/bin/restic-backup.sh 的最后 # 脚本开头有 set -euo pipefail，前面任何一步失败都走不到这里 curl -fsS -m 10 --retry 3 \\ \u0026#34;http://192.168.10.30:3001/api/push/替换成你的Token?status=up\u0026amp;msg=OK\u0026amp;ping=\u0026#34; \\ \u0026gt; /dev/null Push 监控的心跳间隔设成 25 小时：备份每天跑一次，多留一个小时的余量，超过 25 小时没收到上报就告警。不管是脚本报错、timer 没触发，还是整台机器没开机，都能发现。\n踩过的坑 1. 谁来监控监控者？ Uptime Kuma 自己挂了怎么办？它自带一个 Prometheus 格式的 /metrics 接口，在设置里生成 API Key 之后，让 Prometheus 带上认证去抓取，再在 Grafana 里配一条抓取失败的告警。两套系统互相盯着，同时挂掉的概率就很低了。\n2. 数据目录别放在 NFS 上。 我一开始图省事，把 data 放在了 NAS 挂过来的目录里，用了几天就开始报数据库被锁。官方文档写得很清楚，不支持把数据放在 NFS 这类网络文件系统上，挪回本地磁盘就好了。\n3. docker.sock 的风险。 要监控本机容器的运行状态，得把 Docker 的 socket 挂进去。注意后面那个 :ro 只是让挂载点只读，并不能限制通过 socket 调用 API，拿到 socket 基本就等于拿到了宿主机的 root。我权衡之后只在这台机器上这么做了，其他机器上的服务都用 HTTP 或端口检查代替。\n4. 维护时的告警轰炸。 有一次给 Proxmox 升级重启，一口气收到十几条告警。后来学会了用“维护”功能：提前建一个维护窗口，期间相关的监控项暂停告警，状态页上也会显示正在维护。\n5. DNS 检查别太频繁。 我一度把 DNS 检查调到了每 10 秒一次，AdGuard Home 的查询日志里全是 Uptime Kuma 的请求，翻起来很不方便。改回 60 秒，再在 AdGuard Home 的查询日志设置里把这个检查用的域名加进忽略列表，清净多了。\n小结 Uptime Kuma 和 Prometheus 并不冲突：一个回答“服务能不能用”，一个回答“机器状态怎么样”。加上 Push 监控之后，家里的定时任务也有了兜底。我还给家里人做了一个简单的状态页，下次再“断网”，他们可以先看一眼是哪个服务出了问题——当然，最好是我先收到通知。\n","permalink":"https://www.homeexit.net/posts/uptime-kuma/","summary":"Prometheus 管指标，Uptime Kuma 管服务通不通：给网页、DNS、端口和备份都加上检查。","title":"用 Uptime Kuma 监控家里服务的可用性"},{"content":"Jellyfin 之前一直跑在笔记本的 Docker 虚拟机里，全靠 CPU 软解软编。平时电视直接播放（Direct Play）没问题，可一旦遇到电视不支持的格式，比如 HEVC 10bit 的片源，或者要把 ASS 特效字幕烧进画面，就得实时转码，两个虚拟 CPU 瞬间满载，画面一卡一卡的。\n最直接的办法是让核显来干活。但在笔记本上给 Proxmox 虚拟机直通核显很折腾：之前那篇说过，整块设备直通只能给虚拟机，而笔记本的核显直通坑又特别多。权衡之后，我买了一台 N100 的小主机（192.168.10.30），装 Debian 13，直接在宿主机上跑 Docker，媒体相关的服务都搬了过去。它的核显支持 HEVC 10bit 和 AV1 的硬件解码，功耗又低，正适合干这个。\n宿主机准备 Jellyfin 官方的 Docker 镜像里已经自带了打过补丁的 ffmpeg（jellyfin-ffmpeg）和核显的用户态媒体驱动（iHD），宿主机只要内核驱动能正常识别核显就行。Debian 13 的内核足够新，装完直接就能用，再确认一下核显需要的固件包装上了（Debian 上在 non-free-firmware 组件里）。\nls -l /dev/dri # crw-rw---- 1 root video 226, 1 ... card1 # crw-rw---- 1 root render 226, 128 ... renderD128 getent group render # 看 render 组的 GID，每台机器可能不一样 # render:x:992: dmesg | grep -iE \u0026#39;guc|huc\u0026#39; # 看 GuC/HuC 固件有没有加载成功 apt install intel-gpu-tools # 带 intel_gpu_top，后面验证用 renderD128 就是转码要用的设备节点，属于 render 组。记下 render 组的 GID，下面要用。\ncompose 文件 还是按老规矩放在 /srv/compose/jellyfin：\nservices: jellyfin: image: jellyfin/jellyfin:10.11.0 container_name: jellyfin restart: unless-stopped user: \u0026#34;1000:1000\u0026#34; group_add: - \u0026#34;992\u0026#34; # 宿主机 render 组的 GID，要写数字 devices: - /dev/dri:/dev/dri volumes: - ./config:/config - ./cache:/cache - /mnt/media:/media:ro ports: - \u0026#34;8096:8096\u0026#34; environment: TZ: Asia/Shanghai 关键是两处：devices 把 /dev/dri 映射进容器；group_add 让容器里的进程加入宿主机 render 组的 GID，这样才有权限打开 renderD128。媒体文件在 NAS 上，通过 NFS 只读挂到 /mnt/media。\n启动后，先在容器里验证驱动：\ndocker exec -it jellyfin /usr/lib/jellyfin-ffmpeg/vainfo --display drm --device /dev/dri/renderD128 # 正常的话能看到 Intel iHD driver，以及一长串 VAProfile 和 VAEntrypoint Jellyfin 里的设置 在“控制台 → 播放 → 转码”里：\n硬件加速选 Intel QuickSync (QSV)，设备保持默认的 /dev/dri/renderD128； 硬件解码把 H264、HEVC、HEVC 10bit、VP9、AV1 等都勾上，这一代核显都支持； 勾上启用硬件编码； 片源有 HDR 的话，打开色调映射，否则 HDR 片子转码出来颜色发灰； 底下的 Low-Power 编码选项要看核显代际，有些核显需要额外加载 HuC 固件才能用。我这台 dmesg 里能看到 HuC 加载成功，就勾上了。 至于 QSV 和 VAAPI 怎么选：在 Linux 上，QSV 本身就构建在 VA-API 之上，两者都需要 /dev/dri。Jellyfin 文档的建议是较新的核显用 QSV，比较老的才用 VAAPI。我两个都试了，都能正常转码，最后按文档选了 QSV。\n设置好后，找一部 4K HEVC 10bit 的片子，在播放器里手动把码率调低来强制转码，同时在宿主机上开着 intel_gpu_top，能看到 Video 那一栏的占用跳起来，就说明硬件转码生效了。播放信息面板里也能看到转码的原因和用的是哪种编解码器。\n踩过的坑 1. group_add 写了组名。 我一开始写的是组名 render，结果一播放就报错，日志里是打不开 renderD128 的权限错误。原因是组名是在容器里解析的，容器里的 render 组 GID 和宿主机不一定一样。改成宿主机上查到的数字 GID 就好了。\n2. 媒体目录只读导致刮削报错。 媒体目录是只读挂进去的，结果 Jellyfin 刮削时一直报写入失败。在媒体库设置里把“将媒体图像保存到媒体文件夹”关掉，元数据保存器里的 NFO 也不勾，元数据和图片就都存到 /config 里了。\n3. 电视找不到服务器。 电视在 IoT 网段里，而 Jellyfin 客户端的自动发现靠的是局域网广播，跨不了网段；手动输入地址也连不上，因为防火墙不让 IoT 访问内网。最后在路由器的 forward 链里、那条“丢弃私有地址”的规则前面加了一条，只放行电视到这台机器的 8096 端口：\niifname $IOT ip saddr 192.168.20.50 ip daddr 192.168.10.30 tcp dport 8096 accept 电视的 IP 在 dnsmasq 里按 MAC 地址做了固定分配，保证不会变。\n4. 字幕也会触发转码。 即使视频格式电视能直接播放，只要选了需要烧录的图形字幕或特效字幕，也会整段转码。这部分虽然也能用上硬件，但能用外挂文本字幕的就尽量用文本字幕。\n小结 换成核显转码之后，同一部 4K HDR 片子转成 1080p，CPU 占用从满载降到了二三十，整机功耗也就十几瓦，风扇基本听不到。N100 这类小主机拿来跑家庭影音确实合适，唯一的门槛就是 /dev/dri 的映射和 render 组的权限，弄明白之后其实就几行配置。\n","permalink":"https://www.homeexit.net/posts/jellyfin-intel-qsv/","summary":"在 N100 小主机上用 Docker 跑 Jellyfin，开启核显硬件转码，4K 转码时 CPU 不再满载。","title":"Jellyfin 用 Intel 核显（QSV/VAAPI）硬件转码"},{"content":"家里的智能设备越来越多，各家 App 互不相通，最后还是把它们都接进了 Home Assistant。手头有一台常年开机的小主机，就直接用 Docker 跑了容器版。\ndocker run -d \\ --name homeassistant \\ --restart unless-stopped \\ --privileged \\ -e TZ=Asia/Shanghai \\ -v /opt/homeassistant:/config \\ -v /run/dbus:/run/dbus:ro \\ --network host \\ ghcr.io/home-assistant/home-assistant:stable 为什么要用 host 网络 很多设备靠 mDNS、SSDP 这类局域网广播被发现，桥接网络下这些广播进不了容器，集成会一直搜不到设备。用 --network host 最省事，Web 界面直接在宿主机的 8123 端口。\n容器版没有加载项 容器版不带 Supervisor，也就没有\u0026quot;加载项商店\u0026quot;。Mosquitto、Zigbee2MQTT 这类服务需要自己另起容器，好处是每个服务都能单独升级、单独备份。\n备份 所有配置都在挂载进去的 /opt/homeassistant 目录里，我把它放进了每天的增量备份（见这篇）。升级镜像前再手动备份一次，出问题直接换回旧版本镜像。\n","permalink":"https://www.homeexit.net/posts/home-assistant-docker/","summary":"容器版 Home Assistant 的网络模式、加载项和备份。","title":"用 Docker 跑 Home Assistant 的几个注意点"},{"content":"装了 AdGuard Home 之后，查询日志让我第一次看清了家里那些“智能”设备有多能折腾：一个摄像头一天发几千个 DNS 请求，智能插座的固件好几年没更新过，扫地机器人和电视也在不停地往外连。这些设备和我的 NAS、电脑在同一个网段里，彼此之间畅通无阻，想想还是有点不放心。\n于是这个月终于下决心划 VLAN，把它们隔离出去。\n规划 VLAN 网段 用途 10 192.168.10.0/24 主网：电脑、手机、服务器 20 192.168.20.0/24 IoT：插座、摄像头、扫地机、电视 访问规则：\nIoT 可以访问公网，可以访问 AdGuard Home 的 53 端口，除此之外不能访问任何内网地址； 主网可以主动访问 IoT（比如在电脑上打开摄像头的管理页面），IoT 只能回应，不能主动发起连接。 需要的硬件有三样：能做 VLAN 的路由器、支持 802.1Q 的网管交换机、能把不同 SSID 绑定到不同 VLAN 的无线 AP。我家的路由器是一台装了 Debian 的双网口小主机，enp2s0 接光猫，enp1s0 接交换机，本来就用 nftables 做 NAT、用 dnsmasq 发 DHCP，改起来比较自由。\n为了少动已有的配置，主网在交换机上虽然是 VLAN 10，但在接路由器、AP 和 Proxmox 的这几个口上，它都以不带标签（untagged）的形式出现，VLAN 20 则带着标签（tagged）走同一根线。接有线 IoT 设备（电视）的那个口，设成 VLAN 20 不带标签。\n路由器：新增 VLAN 接口和 DHCP # /etc/network/interfaces 里新增 auto enp1s0.20 iface enp1s0.20 inet static address 192.168.20.1/24 # /etc/dnsmasq.d/iot.conf interface=enp1s0.20 dhcp-range=set:iot,192.168.20.100,192.168.20.250,255.255.255.0,12h dhcp-option=tag:iot,option:router,192.168.20.1 dhcp-option=tag:iot,option:dns-server,192.168.10.53 Debian 的 ifupdown 会把 enp1s0.20 这种带点的接口名自动识别成 VLAN 子接口。IoT 设备的 DNS 直接指向 AdGuard Home，这样在查询日志里还能按设备区分。\n路由器：防火墙规则 核心就是 forward 链里的几条规则。下面是和 IoT 相关的部分，NAT 和其他规则没变，省略：\ndefine LAN = \u0026#34;enp1s0\u0026#34; define IOT = \u0026#34;enp1s0.20\u0026#34; define WAN = \u0026#34;enp2s0\u0026#34; table inet filter { chain forward { type filter hook forward priority filter; policy drop; ct state established,related accept ct state invalid drop # 主网出去随意，也可以主动访问 IoT iifname $LAN accept # IoT 只放行到 AdGuard Home 的 DNS iifname $IOT ip daddr 192.168.10.53 meta l4proto { tcp, udp } th dport 53 accept # IoT 访问任何私有地址一律丢弃 iifname $IOT ip daddr { 10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16 } drop # 剩下的只能出公网 iifname $IOT oifname $WAN accept } } 规则从上往下匹配，established,related 那一条保证了主网发起的连接，IoT 设备能正常回包。input 链里也要限制 IoT 只能访问路由器的 DHCP（67/udp）和下面要用到的 mDNS（5353/udp），别让它们连得上路由器的 SSH。\nProxmox：网桥开启 VLAN 感知 Proxmox 那一侧，把 vmbr0 改成 VLAN 感知的网桥。以后哪台虚拟机或容器需要待在 IoT 网段，给网卡加个 tag 就行，不用再多插一根网线：\nauto vmbr0 iface vmbr0 inet static address 192.168.10.5/24 gateway 192.168.10.1 bridge-ports enp2s0 bridge-stp off bridge-fd 0 bridge-vlan-aware yes bridge-vids 2-4094 改完执行 ifreload -a。宿主机自己的地址还在不带标签的主网里，现有的虚拟机不受影响。需要放进 IoT 网段的，用 qm set \u0026lt;vmid\u0026gt; --net0 virtio,bridge=vmbr0,tag=20 就行；注意这样会重新生成 MAC 地址，想保留的话把原来的 MAC 写进参数里（virtio=原MAC,bridge=vmbr0,tag=20）。我打算以后把跑智能家居中控的那台也放进来。\n踩过的坑 1. 把自己锁在了外面。 我先在交换机上改端口，一不小心把自己电脑那个口的 PVID 改成了 20，瞬间连不上交换机的管理页面，最后只能按复位键重来。教训：先配路由器，再配交换机，而且永远最后才动自己正在用的那个口。\n2. 新 SSID 的兼容性。 给 IoT 新建的无线网络只开 2.4GHz，加密用 WPA2，名字用纯英文。不少 IoT 设备不支持 5GHz 和 WPA3，开着混合模式时有的设备连都连不上。\n3. 所有设备都要重新配网。 智能插座、扫地机得在各自的 App 里重新配一遍网络，花了我一整个周末。\n4. 投屏和设备发现失效了。 手机在主网，电视在 IoT 网段，投屏时找不到电视。原因是这类发现机制依赖 mDNS 组播，而组播不能跨网段。在路由器上开启 Avahi 的反射功能可以解决：\n# /etc/avahi/avahi-daemon.conf [server] allow-interfaces=enp1s0,enp1s0.20 [reflector] enable-reflector=yes 注意这只能解决 mDNS，依赖 SSDP 的 DLNA 投屏它管不了。我家主要用的那种投屏刚好走 mDNS，算是运气好。\n5. 写死了 DNS 的设备。 有个设备不用 DHCP 下发的 DNS，而是写死了公共 DNS。在路由器上加一条 DNAT，把 IoT 网段发往任何地址 53 端口的请求都转到 AdGuard Home，就能管住它。\n小结 划完 VLAN 之后，AdGuard Home 日志里的 IoT 设备都变成了 192.168.20.x，一眼就能和自己的设备区分开，还可以给整个网段单独设一套更严格的过滤规则。家里的网络第一次有了“内外之分”，花一个周末，值。\n","permalink":"https://www.homeexit.net/posts/iot-vlan/","summary":"把智能插座、摄像头、电视等 IoT 设备划进单独的 VLAN，只许它们访问 DNS 和公网。","title":"给家里网络划 VLAN，把 IoT 设备隔离出去"},{"content":"手机存储又满了。家里人的照片散落在几部手机、旧硬盘和网盘里，网盘会员每年续费也心疼。Immich 我观望了很久，一直顾虑它更新太快、时不时有破坏性变更。去年秋天它发布了 2.0 正式版，宣布进入稳定阶段，我觉得是时候上车了。\n它最吸引我的地方：手机 App 能自动备份，时间线浏览很流畅，有人脸识别和按内容搜索（比如搜“猫”“海边”），用起来很接近商业相册。\n存储规划 部署前先想清楚数据放哪，这比装软件本身更重要：\n程序跑在 Docker 虚拟机（192.168.10.21）上，目录按compose 目录约定放在 /srv/compose/immich。 照片原图（.env 里的 UPLOAD_LOCATION）放在 NAS 的 ZFS 数据集 tank/photos 上，通过 NFS 挂进虚拟机。ZFS 快照按之前定的快照策略自动打，误删了也能找回来。 数据库（DB_DATA_LOCATION）放在虚拟机的本地磁盘上。官方文档明确说数据库目录不要放在网络共享上，否则容易出各种诡异的问题。 部署 Immich 的 compose 文件变化比较频繁，不建议自己手写或者照抄别人的。按官方文档，从发布页下载当前版本的 docker-compose.yml 和 example.env 两个文件（文档里给了现成的下载命令），把 example.env 重命名为 .env，再改这几项：\n# /srv/compose/immich/.env 里我改动的部分 UPLOAD_LOCATION=/mnt/photos/immich DB_DATA_LOCATION=/srv/compose/immich/postgres # 默认是 release（总是最新版），我改成了当时的具体版本号，升级前先看更新说明 IMMICH_VERSION=v2.x.y # 只用字母和数字，不要带特殊字符 DB_PASSWORD=换成随机生成的字母数字串 TZ=Asia/Shanghai NFS 的部分，NAS 那边把 /tank/photos 导出给 Docker 虚拟机，虚拟机这边写进 fstab：\n# NAS 上的 /etc/exports /tank/photos 192.168.10.21(rw,sync,no_subtree_check,no_root_squash) # Docker 虚拟机上的 /etc/fstab 192.168.10.20:/tank/photos /mnt/photos nfs defaults,_netdev 0 0 Immich 的容器默认以 root 运行，如果 NFS 开着默认的 root_squash，root 写进去的文件会被映射成 nobody，经常出现写不进去的情况，所以我只对这一个 IP 开了 no_root_squash。\n然后 docker compose up -d，等几个容器都起来后打开 http://192.168.10.21:2283，第一个注册的账号就是管理员。\n从手机迁移 迁移分两部分。\n手机上现有的照片，直接用 Immich 的手机 App：服务器地址填 http://192.168.10.21:2283，登录后在备份设置里选要备份的相册。我只选了相机胶卷和几个重要的相册，聊天软件自动保存的图片就算了。几万张照片第一次上传要很久，我是睡前插上充电器、连着家里的 Wi-Fi、让 App 停在前台跑，两个晚上传完。之后打开后台备份，新拍的照片就会自动传上去。\n以前导出到电脑和旧硬盘上的照片，用官方的命令行工具批量上传更快：\n# 需要较新版本的 Node.js npm i -g @immich/cli # API Key 在网页的“账户设置 → API 密钥”里生成 immich login http://192.168.10.21:2283/api 你的API密钥 # 先用 --dry-run 看看会传哪些文件；--album 会按文件夹名自动建相册 immich upload --recursive --album --dry-run /data/old-photos immich upload --recursive --album /data/old-photos CLI 会先计算文件的校验和，服务器上已经有的会跳过，所以中断了重跑也没关系。\n踩过的坑 1. NFS 没挂上，Docker 就先启动了。 有一次虚拟机重启时 NAS 还没开机，NFS 没挂上，Immich 照样启动，往本地那个空的 /mnt/photos 目录里写了一堆东西；等 NFS 挂上之后，这些文件就被“盖住”了。解决办法是让 Docker 依赖这个挂载点，用 systemctl edit docker.service 加一段覆盖配置：\n[Unit] RequiresMountsFor=/mnt/photos 代价是 NFS 挂不上时所有容器都不会启动。不过对我来说，这总比数据写错地方要好。\n2. 没有拍摄时间的图片跑到了“今天”。 截图、从聊天软件里保存的图片很多没有 EXIF 拍摄时间，Immich 只能退而使用文件的修改时间，而我在拷贝过程中把修改时间弄丢了，结果几千张图都堆在了导入那天。补救办法是导入前用 exiftool 从文件名里提取时间写回 EXIF；已经导入的，也可以在网页上批量修改日期。\n3. 刚导入时机器忙不过来。 导入后 Immich 要给每张照片生成缩略图、做人脸检测和内容识别。Docker 虚拟机只有两个核，CPU 满载了两天，笔记本风扇一直在叫。可以在管理后台的任务页面暂停某些任务或者调低并发，等夜里再跑。\n4. 重复照片。 手机 App 备份和命令行导入难免有重叠。完全相同的文件 Immich 会自动跳过，但经过压缩的副本就认不出来了。新版本里有一个查找重复照片的工具，会把相似的照片分组列出来，人工逐组确认删除即可。\n备份 照片是家里最不能丢的数据。Immich 自己会定时把数据库导出到上传目录下的 backups 子目录里，所以重点备份好 UPLOAD_LOCATION 就行。上传目录里的缩略图（thumbs）和转码视频（encoded-video）都可以重新生成，备份时我把它们排除掉，原图再用 restic 推一份到对象存储。\n小结 用了一个月，家里人已经习惯在 Immich 里翻照片了，按人物和地点浏览的功能尤其受欢迎。目前只在家里局域网用，手机在外面拍的照片，回家连上 Wi-Fi 后自动同步，对我来说正合适。\n","permalink":"https://www.homeexit.net/posts/immich-setup/","summary":"在 Docker 虚拟机上部署 Immich，照片原图放到 NAS 上，再把几万张手机照片迁移过去。","title":"自建相册 Immich 的部署与从手机相册迁移"},{"content":"以前定时任务都丢在 crontab 里，出了问题只能去翻邮件或者自己写日志。后来逐步换成了 systemd timer，记录一下写法。\n先写一个 service，描述要做的事：\n# /etc/systemd/system/backup.service [Unit] Description=Nightly backup [Service] Type=oneshot ExecStart=/usr/local/bin/backup.sh 再写一个同名的 timer，描述什么时候做：\n# /etc/systemd/system/backup.timer [Unit] Description=Run backup nightly [Timer] OnCalendar=*-*-* 03:00:00 RandomizedDelaySec=15min Persistent=true [Install] WantedBy=timers.target systemctl daemon-reload systemctl enable --now backup.timer systemctl list-timers backup.timer 比 cron 好在哪 输出自动进 journal，journalctl -u backup.service 就能看到每次运行的日志和退出码。 Persistent=true：计划时间机器正好关着，开机后会补跑一次。 RandomizedDelaySec 可以把多台机器的任务错开，避免同时把 NAS 打满。 可以用 After=、Requires= 声明依赖，比如等网络存储挂载好了再跑。 想手动触发一次，直接 systemctl start backup.service。\n","permalink":"https://www.homeexit.net/posts/systemd-timer/","summary":"定时任务从 crontab 迁移到 systemd timer 的写法和好处。","title":"用 systemd timer 替代 cron"},{"content":"家里现在有四台“机器”：笔记本上的 Proxmox 宿主机、NAS、Docker 虚拟机，还有跑 AdGuard Home 的小容器。平时都是出了问题才登上去看，比如上个月 Docker 虚拟机的磁盘悄悄涨到了 95%，还是某个服务报错我才发现。smartd 和 NUT 只在出问题时通知，我还想看到趋势：磁盘每个月涨多少、夏天 CPU 温度有多高、停电时电池掉得多快。\n于是上了最经典的组合：每台机器跑 node_exporter 暴露指标，Prometheus 定时去抓取并存起来，Grafana 负责画图和告警。\n在每台机器上装 node_exporter Debian 仓库里就有，宿主机、NAS、Docker 虚拟机都直接 apt install prometheus-node-exporter，装完它会监听 9100 端口，curl -s http://localhost:9100/metrics 能看到一大堆指标就对了。AdGuard Home 那个 LXC 我没装，容器里看到的很多数据和宿主机是一回事，意义不大。\n用 Compose 跑 Prometheus 和 Grafana 服务端放在 Docker 虚拟机上，目录照旧按compose 目录约定来，放在 /srv/compose/monitoring：\nservices: prometheus: image: prom/prometheus:v3.5.0 restart: unless-stopped command: - --config.file=/etc/prometheus/prometheus.yml - --storage.tsdb.path=/prometheus - --storage.tsdb.retention.time=90d volumes: - ./prometheus.yml:/etc/prometheus/prometheus.yml:ro - ./data/prometheus:/prometheus ports: - \u0026#34;192.168.10.21:9090:9090\u0026#34; grafana: image: grafana/grafana:12.1.0 restart: unless-stopped environment: GF_SECURITY_ADMIN_PASSWORD: ${GRAFANA_ADMIN_PASSWORD} volumes: - ./data/grafana:/var/lib/grafana ports: - \u0026#34;192.168.10.21:3000:3000\u0026#34; prometheus.yml 里列出要抓取的目标：\nglobal: scrape_interval: 30s scrape_configs: - job_name: node static_configs: - targets: - 192.168.10.5:9100 # Proxmox 宿主机 - 192.168.10.20:9100 # NAS - 192.168.10.21:9100 # Docker 虚拟机自己 docker compose up -d 之后，打开 http://192.168.10.21:9090，在 Status 菜单里查看各个抓取目标的状态，三个都是 UP 就成了。\nGrafana 里添加 Prometheus 数据源，地址填 http://prometheus:9090（同一个 compose 项目里的容器可以用服务名互相访问），然后导入社区里很有名的 Node Exporter Full 仪表盘（ID 1860），CPU、内存、磁盘、网络、温度一下子全有了。\n把 UPS 数据接进来 node_exporter 有个 textfile 收集器，会读取指定目录里的 .prom 文件，把里面的内容当成指标一起暴露出来。Debian 打包的版本默认读的是 /var/lib/prometheus/node-exporter 目录。所以只要写个小脚本，把 upsc 的输出转换一下就行。在连着 UPS 的 NAS 上：\n#!/bin/sh # /usr/local/bin/nut-textfile.sh，用 cron 每分钟跑一次 OUT=/var/lib/prometheus/node-exporter/nut.prom upsc homeups@localhost 2\u0026gt;/dev/null | awk -F\u0026#39;: \u0026#39; \u0026#39; $1 == \u0026#34;battery.charge\u0026#34; { print \u0026#34;nut_battery_charge_percent \u0026#34; $2 } $1 == \u0026#34;battery.runtime\u0026#34; { print \u0026#34;nut_battery_runtime_seconds \u0026#34; $2 } $1 == \u0026#34;ups.load\u0026#34; { print \u0026#34;nut_ups_load_percent \u0026#34; $2 } $1 == \u0026#34;ups.status\u0026#34; { print \u0026#34;nut_on_battery \u0026#34; ($2 ~ /OB/ ? 1 : 0) } \u0026#39; \u0026gt; \u0026#34;$OUT.tmp\u0026#34; \u0026amp;\u0026amp; mv \u0026#34;$OUT.tmp\u0026#34; \u0026#34;$OUT\u0026#34; 先写临时文件再 mv，是为了避免 node_exporter 读到写了一半的文件。硬盘的 SMART 数据也可以用同样的思路采集，Debian 还有个 prometheus-node-exporter-collectors 包，里面自带了 smartmon 等几个现成的采集脚本。\n常用的查询和告警 自己画面板时用得最多的几个查询：\n# 各挂载点的使用率（排除 tmpfs 之类的虚拟文件系统） 100 * (1 - node_filesystem_avail_bytes{fstype!~\u0026#34;tmpfs|overlay|squashfs\u0026#34;} / node_filesystem_size_bytes{fstype!~\u0026#34;tmpfs|overlay|squashfs\u0026#34;}) # 哪台机器抓取失败了 up == 0 # 现在是不是在用电池 nut_on_battery == 1 告警我只配了三条：磁盘使用率超过 85%、有目标连续 5 分钟抓不到、UPS 切到电池供电。用的是 Grafana 自带的告警功能，联系方式选 Webhook，推到家里的 ntfy 上，消息格式粗糙了一点，但能用。告警规则贵精不贵多，一开始我加了一堆 CPU、内存的告警，半夜被吵醒几次之后全删了。\n踩过的坑 数据目录权限。 第一次启动时两个容器都起不来，日志里是 permission denied。prom/prometheus 镜像里的进程以 nobody（UID 65534）运行，Grafana 是 UID 472，而 bind mount 的目录是 root 创建的。分别执行 chown -R 65534:65534 data/prometheus 和 chown -R 472:472 data/grafana 就好了。 容器里的 localhost 不是宿主机。 我最初把 Docker 虚拟机自己写成了 localhost:9100，结果一直是 DOWN：在 Prometheus 容器里，localhost 指的是容器自己。改成虚拟机的局域网 IP 就好了；也可以在 compose 里加上 extra_hosts: [\u0026quot;host.docker.internal:host-gateway\u0026quot;]，然后用 host.docker.internal 访问宿主机。 抓取间隔别太短。 一开始我设的是 5 秒，数据量大了好几倍，家用根本用不着这么细。现在三台机器、30 秒一次、保留 90 天，Prometheus 的数据目录也就一两 G。 node_exporter 别暴露到不该去的地方。 它默认监听所有网卡，在家里倒无所谓，但如果机器还有别的网络接口，最好在 /etc/default/prometheus-node-exporter 里用 --web.listen-address 限制监听地址。 小结 这套东西搭起来不过一个晚上，回报却很直接：现在打开 Grafana 就能看到每台机器的状态。还顺手发现了两件事：笔记本在午后室温高的时候 CPU 温度会到七十多度；NAS 的数据盘使用率每个月稳定涨 3%，可以提前规划扩容了。\n","permalink":"https://www.homeexit.net/posts/prometheus-grafana-homelab/","summary":"用 Prometheus 采集、Grafana 画图，给家里几台机器做指标监控，顺带接入 UPS 电量数据。","title":"Prometheus + Grafana 监控家里的机器"},{"content":"起因是家里的电视：开机广告、首页推荐里夹带的广告，还有各种 App 时不时往外报数据。电视上没法装插件，手机 App 里的广告也拦不住，能统一管住所有设备的，只有 DNS 这一层。另一个需求是内网服务越来越多，每次都要输 192.168.10.21:8080 这种地址，想给它们起个好记的名字。\nAdGuard Home 正好能同时满足这两件事：它是一个带网页管理界面的 DNS 服务器，支持过滤规则、查询日志、按客户端设置规则，也能做本地域名解析。\n放在哪里 DNS 是基础设施，它挂了全家都上不了网，所以我没把它放进 Docker 虚拟机（那台机器时不时要重启升级），而是按LXC 和虚拟机那篇的思路，单独开了一个非特权 LXC：Debian 12 模板，1 核、512M 内存，固定 IP 192.168.10.53，好记。\nAdGuard Home 是单个可执行文件，官方提供了打包好的压缩包，装起来很简单：\n# 在 LXC 里，先从官方发布页下载 AdGuardHome_linux_amd64.tar.gz tar -xzf AdGuardHome_linux_amd64.tar.gz -C /opt cd /opt/AdGuardHome ./AdGuardHome -s install # 注册成系统服务并启动 ./AdGuardHome -s status # 回到 PVE 宿主机：让这个容器开机自启，并且排在第一个启动 pct set 105 --onboot 1 --startup order=1 装好后浏览器打开 http://192.168.10.53:3000 走初始化向导：管理界面端口我改成了 80，DNS 监听所有网卡的 53 端口，再设置管理员账号和密码。\n基本配置 向导完成后，大部分设置都能在网页上改，我主要动了这几处：\n上游 DNS：填了路由器（它再转发给运营商的 DNS）和一个 DNS-over-HTTPS 地址；Bootstrap DNS 填路由器，用来解析 DoH 服务器本身的域名。 过滤列表：保留默认的 AdGuard DNS filter，又加了一两个常见的中文规则列表。列表不是越多越好，下文会说。 DNS 重写：在“过滤器 → DNS 重写”里加上内网域名，比如 nas.home.example.com 指向 192.168.10.20，也支持 *.home.example.com 这样的通配符。 这些设置最终都写在 /opt/AdGuardHome/AdGuardHome.yaml 里。有时候直接改文件更快，但一定要先停掉服务，否则运行中的程序会用内存里的配置把你的修改覆盖掉。和 DNS 相关的部分大致长这样：\ndns: bind_hosts: - 0.0.0.0 port: 53 upstream_dns: - 192.168.10.1 - https://dns.example.com/dns-query bootstrap_dns: - 192.168.10.1 ratelimit: 20 # 每个客户端每秒最多 20 个请求，这是默认值 cache_size: 4194304 # 缓存大小，单位字节 自定义规则用的是类似广告过滤插件的语法：|| 开头表示匹配域名及其所有子域，@@ 开头表示放行，! 开头是注释。\n! 额外拦截 ||ads.example.com^ ||telemetry.example.com^ ! 被误杀的，放行 @@||cdn.example.com^ 让全家用上 最后一步是在路由器的 DHCP 设置里，把下发的 DNS 服务器改成 192.168.10.53。设备重新连网或者续租后就会生效，在 AdGuard Home 的查询日志里能看到每台设备的请求。\n踩过的坑 一开始偷懒只改了路由器的上游。 最初我没动 DHCP，只是把路由器自己用的 DNS 改成了 AdGuard Home。这样所有请求都由路由器转发过来，日志里只有一个客户端，按设备设置规则也就无从谈起。更糟的是触发了速率限制：全家的请求都算在路由器头上，高峰时一秒钟超过 20 个，部分请求直接被丢掉，表现就是网页时不时打不开。改成 DHCP 直接下发之后，问题就消失了。 要不要填第二个 DNS。 我一开始在 DHCP 里把路由器填成了备用 DNS，结果发现不少设备并不是“主的不通才用备用”，而是两个轮着用甚至同时问，广告时有时无。最后只留了一个 DNS，靠 LXC 开机自启和它本身的稳定性兜底，同时记好了万一它挂了怎么把 DHCP 改回去。 规则列表加太多会误杀。 有段时间我加了五六个列表，结果购物 App 的商品链接打不开，某个视频网站的播放器也加载不出来。排查方法是在查询日志里按客户端过滤，找被拦截的请求，再加白名单。现在只保留两三个维护活跃的列表。 有些设备会绕过它。 手机系统的“私人 DNS”、浏览器里的“安全 DNS”都会直接走加密 DNS，不经过 AdGuard Home。自己的设备关掉就行，其他的就随它了。 53 端口被占用。 我的 Debian 容器里没有这个问题，但如果装在 Ubuntu 上，systemd-resolved 自带的 DNS 存根会占着 53 端口，需要在 resolved 的配置里设置 DNSStubListener=no 把它让出来，官方文档里有详细步骤。 小结 AdGuard Home 跑了一个多月，那个 LXC 的内存占用一直在一百多兆。查询日志里被拦截的请求占了一成多，大部分来自电视和几个智能家居设备。这也让我开始琢磨：是不是该把这些设备单独隔离到一个网段里去了。\n","permalink":"https://www.homeexit.net/posts/adguard-home/","summary":"在小容器里装 AdGuard Home 接管全家的 DNS，顺带拦广告，再给内网服务起好记的域名。","title":"AdGuard Home 做家庭 DNS 过滤"},{"content":"NAS 上的数据放在 ZFS 上。快照创建是瞬间完成的，刚创建时几乎不占额外空间，是防误删最划算的手段。\nzfs snapshot tank/data@manual-2025-08-27 zfs list -t snapshot -o name,used,creation tank/data zfs rollback tank/data@manual-2025-08-27 手动打快照容易忘，现在交给 sanoid 自动管理，配置在 /etc/sanoid/sanoid.conf：\n[tank/data] use_template = production [template_production] hourly = 24 daily = 30 monthly = 6 autosnap = yes autoprune = yes 这样会保留最近 24 小时每小时一个、最近 30 天每天一个、最近 6 个月每月一个快照。只想找回单个文件时不用回滚整个数据集，到 /tank/data/.zfs/snapshot/ 下对应的快照目录里把文件复制出来就行。\n要注意的是，快照和原数据在同一个存储池里，池坏了快照也一起没了。所以快照只防误操作，不能代替异地备份。\n","permalink":"https://www.homeexit.net/posts/zfs-snapshot-policy/","summary":"手动快照、sanoid 自动快照，以及快照不能代替备份。","title":"ZFS 快照：我的保留策略"},{"content":"家里的备份之前一直靠 rsync 增量备份，每天把重要目录同步到 NAS 和一块 USB 硬盘上。这解决了“误删”和“单块盘坏掉”的问题，但所有副本都在同一个屋檐下，万一遇上火灾、进水或者失窃，就全军覆没了。按照 3-2-1 原则（三份副本、两种介质、一份异地），还缺异地的那一份。\n上个月整理完 compose 目录之后，要备份的东西也清楚了：Docker 虚拟机的 /srv/compose，配置和数据都在里面。NAS 上的文档目录也用同样的方法另跑一份，下面以 Docker 虚拟机为例。总量不到 100G，放在对象存储上一个月也就几块钱。\n为什么选 restic 客户端加密：数据在本地加密后才上传，对象存储那边只能看到一堆密文块。 去重和压缩：每次备份都是一个完整的快照，但只上传变化的部分。 原生支持 S3 协议：大多数对象存储都兼容 S3 接口，换服务商只需要改仓库地址。 单个可执行文件，没有依赖。恢复时在任何机器上下载一个文件就能用。 Debian 12 仓库里的 restic 版本比较老，我直接用了官方发布的二进制（写这篇时是 0.18），下载 restic_0.18.0_linux_amd64.bz2，解压后放到 /usr/local/bin/restic，以后用 restic self-update 升级。\n初始化仓库 先在对象存储的控制台里建一个桶（bucket），再建一对只对这个桶有读写权限的访问密钥。然后把连接信息写进一个只有 root 能读的文件：\n# /root/.config/restic/env （chmod 600） export RESTIC_REPOSITORY=\u0026#34;s3:https://s3.example.com/homelab-backup\u0026#34; export RESTIC_PASSWORD_FILE=\u0026#34;/root/.config/restic/password\u0026#34; export AWS_ACCESS_KEY_ID=\u0026#34;替换成你的 Access Key\u0026#34; export AWS_SECRET_ACCESS_KEY=\u0026#34;替换成你的 Secret Key\u0026#34; # 部分服务商要求指定区域，按它的文档填 # export AWS_DEFAULT_REGION=\u0026#34;...\u0026#34; 环境变量名虽然带着 AWS，但对所有 S3 兼容的服务都是这么用的。password 文件里放仓库的加密密码，用 openssl rand -base64 32 生成一个就行。\nsource /root/.config/restic/env restic init # 初始化仓库，只需执行一次 restic backup /srv/compose --exclude-file=/root/.config/restic/excludes restic snapshots # 查看已有的快照 excludes 文件里每行一个排除规则。我排除了各种缓存目录，以及数据库的数据目录——数据库单独处理，见下文。\n备份脚本 直接备份正在运行的数据库目录，恢复出来很可能是不一致的。我的做法是先用 pg_dump 导出成 SQL 文件，再连同其他文件一起备份：\n#!/usr/bin/env bash # /usr/local/bin/restic-backup.sh set -euo pipefail source /root/.config/restic/env # 1. 导出数据库 mkdir -p /srv/dumps cd /srv/compose/miniflux docker compose exec -T db pg_dump -U miniflux miniflux \u0026gt; /srv/dumps/miniflux.sql # 2. 备份 restic backup /srv/compose /srv/dumps \\ --exclude-file=/root/.config/restic/excludes \\ --tag daily # 3. 按策略删除旧快照：保留 7 天、4 周、6 个月 restic forget --tag daily --keep-daily 7 --keep-weekly 4 --keep-monthly 6 # 4. 每周日真正清理数据，并抽查 5% 的数据能否正常读出 if [ \u0026#34;$(date +%u)\u0026#34; = \u0026#34;7\u0026#34; ]; then restic prune restic check --read-data-subset=5% fi forget 只是删掉快照记录，很快；prune 才会真正清理不再被引用的数据块，要下载和重写一部分数据，比较慢，所以我放到每周做一次。check --read-data-subset 会实际下载一部分数据来校验，比只检查仓库结构更让人放心。\n定时执行用的是 cron，在 /etc/cron.d/restic 里加一行，每天凌晨 3 点半跑，输出追加到一个日志文件里。\n一定要做恢复演练 没恢复过的备份不算备份。配好之后我挑了个周末，在一台临时虚拟机上只装了 restic，试着把 Miniflux 整个恢复出来：\nrestic snapshots --tag daily restic ls latest /srv/compose/miniflux # 先看看快照里有什么 restic restore latest --target /tmp/restore-test --include /srv/compose/miniflux restic restore latest --target /tmp/restore-test --include /srv/dumps 然后用恢复出来的 compose.yaml 起一个空数据库，把 SQL 导进去，打开网页能看到原来的订阅，才算真正放心。\n踩过的坑 仓库密码丢了就全完了。 restic 的加密是真加密，忘了密码谁也救不回来。我把密码存进了密码管理器，又打印了一份放在抽屉里。注意密码管理器自己的数据也在备份里，别形成“要恢复密码库得先有密码”的死循环。 第一次上传占满了上行带宽。 家里上行只有几十兆，第一次全量备份跑了好几个小时，期间视频通话一卡一卡的。加上 --limit-upload 2048（单位是 KiB/s）限速之后就好了。 残留的锁。 有一次备份到一半虚拟机重启了，之后的备份都报仓库被锁。确认没有其他 restic 进程在跑之后，执行 restic unlock 清掉过期的锁即可。 改了备份路径，旧快照不会被清理。 forget 默认按“主机名 + 备份路径”分组应用保留策略。我中途给 backup 命令加了 /srv/dumps，新旧快照就分成了两组，旧的那组永远保留着最后几个快照。可以用 --group-by host,tags 改变分组方式，或者确认无误后手动 forget 掉旧组。 留意服务商的计费规则。 有些对象存储对删除的数据有最短存储期限，或者按请求次数收费，频繁 prune 可能反而多花钱。这也是我把 prune 放到每周一次的原因之一。 小结 restic 用起来很省心：配置就是几个环境变量，备份、清理、校验、恢复都是一条命令。现在每天凌晨自动跑，异地的那一份总算补上了。还差的一块是失败告警：目前只能靠自己偶尔去翻日志，这点还不太放心，以后再想办法。\n","permalink":"https://www.homeexit.net/posts/restic-s3-backup/","summary":"在本地 rsync 备份之外，用 restic 把数据加密去重后推到对象存储，补上异地的那一份。","title":"用 restic 把备份推到对象存储"},{"content":"自从按LXC 和虚拟机那篇的思路把 Docker 都挪进一台专门的虚拟机（192.168.10.21）以后，上面的服务越加越多，到现在已经有十来个了。早期有的是一条长长的 docker run 命令，有的写在 compose 文件里；有的数据放在命名卷里，有的挂在家目录下……上个月想把其中一个服务迁走，翻了半天才找齐它的数据。趁着五一假期，把整套东西按统一的规矩重新整理了一遍。\n目录约定 所有服务都放在 /srv/compose 下面，一个服务一个目录：\n/srv/compose/ ├── .gitignore ├── update-all.sh ├── miniflux/ │ ├── compose.yaml │ ├── .env # 密码等敏感信息，不进 git │ └── data/ # 所有持久化数据，不进 git ├── vaultwarden/ │ ├── compose.yaml │ ├── .env │ └── data/ └── homepage/ ├── compose.yaml └── data/ 我给自己定的几条规矩：\n一个服务一个目录，它依赖的数据库、缓存也放在同一个 compose 文件里。Compose 默认用目录名做项目名，所以目录名也决定了容器名的前缀。 文件统一叫 compose.yaml，这是现在 Compose 首选的文件名。 数据一律 bind mount 到 ./data/ 下，不用命名卷。备份和迁移时整个目录打包带走就行，docker compose down -v 手滑也不会把数据删掉。 密码等敏感信息放 .env，compose.yaml 里用 ${变量名} 引用。 镜像固定版本号，不用 latest。 整个 /srv/compose 用 git 管理，.gitignore 里排除 */.env 和 */data/。每次改了什么、什么时候升的级，都有据可查。 一个完整的例子 以 RSS 阅读器 Miniflux 为例，它需要一个 PostgreSQL：\nservices: miniflux: image: miniflux/miniflux:2.2.6 restart: unless-stopped depends_on: db: condition: service_healthy ports: - \u0026#34;192.168.10.21:8080:8080\u0026#34; environment: DATABASE_URL: postgres://miniflux:${DB_PASSWORD}@db/miniflux?sslmode=disable RUN_MIGRATIONS: 1 CREATE_ADMIN: 1 ADMIN_USERNAME: admin ADMIN_PASSWORD: ${ADMIN_PASSWORD} db: image: postgres:16 restart: unless-stopped environment: POSTGRES_USER: miniflux POSTGRES_PASSWORD: ${DB_PASSWORD} POSTGRES_DB: miniflux volumes: - ./data/postgres:/var/lib/postgresql/data healthcheck: test: [\u0026#34;CMD\u0026#34;, \u0026#34;pg_isready\u0026#34;, \u0026#34;-U\u0026#34;, \u0026#34;miniflux\u0026#34;] interval: 10s start_period: 30s 几个细节：文件开头不再写 version:，新版 Compose 会提示这个字段已经过时；depends_on 配合 healthcheck，保证数据库真正可用后应用才启动；端口绑定在虚拟机自己的局域网 IP 上，而不是默认的所有地址。postgres:16 我只固定了大版本，16.x 的小版本更新可以放心跟。\n更新流程 我不用自动更新工具，而是每周末手动过一遍：\n平时用 Miniflux 订阅各个项目发布页的 Atom 源，有新版本时能看到更新说明，重点找 breaking changes。 升级前先给虚拟机打个快照：在 PVE 宿主机上执行 qm snapshot 200 pre-update，出了问题可以整机回滚。 改 compose.yaml 里的版本号，git commit 记一笔。 进对应目录执行 docker compose pull \u0026amp;\u0026amp; docker compose up -d，然后 docker compose logs -f --tail=100 盯一会儿日志。 跑一两天没问题，把快照删掉。 对于只固定了大版本的镜像，我写了个小脚本批量拉一遍：\n#!/usr/bin/env bash # /srv/compose/update-all.sh：逐个目录拉取镜像，并重建有变化的容器 set -euo pipefail for dir in /srv/compose/*/; do [ -f \u0026#34;${dir}compose.yaml\u0026#34; ] || continue echo \u0026#34;==\u0026gt; ${dir}\u0026#34; (cd \u0026#34;$dir\u0026#34; \u0026amp;\u0026amp; docker compose pull --quiet \u0026amp;\u0026amp; docker compose up -d --remove-orphans) done # 清理不再被使用的旧镜像 docker image prune -f up -d 只会重建镜像或配置发生了变化的容器，其他的不受影响，所以全部跑一遍也没关系。\n踩过的坑 1. 日志把磁盘写满。 Docker 默认的 json-file 日志不做轮转，有个服务一直在刷错误日志，半年攒了好几个 G。在 /etc/docker/daemon.json 里给日志设上限：\n{ \u0026#34;log-driver\u0026#34;: \u0026#34;json-file\u0026#34;, \u0026#34;log-opts\u0026#34;: { \u0026#34;max-size\u0026#34;: \u0026#34;10m\u0026#34;, \u0026#34;max-file\u0026#34;: \u0026#34;3\u0026#34; } } 重启 Docker 之后，这个设置只对新创建的容器生效，已有的容器要 docker compose up -d --force-recreate 一遍。\n2. 端口映射绕过了防火墙。 我在虚拟机上开了 ufw，以为没放行的端口外面就访问不到，结果发现 Docker 发布的端口照样能连。原因是 Docker 直接往 iptables 里插自己的规则，流量根本不经过 ufw 管的那几条链。所以才有了上面“端口绑定到具体 IP”那条规矩，只给本机用的服务干脆绑到 127.0.0.1。\n3. 数据库大版本不能直接改 tag。 有一次手痒把 postgres:15 改成了 16，结果数据库起不来，日志说数据目录是旧版本初始化的，不兼容。PostgreSQL 跨大版本要先 pg_dump 导出、换新版本后再导入。好在有快照，回滚了事。现在数据库的大版本，我只在专门腾出时间时才动。\n4. 目录权限。 bind mount 的目录如果事先不存在，Docker 会以 root 身份创建它，而有些镜像里的程序是以普通用户运行的，启动后写不进去。遇到这种情况，先看镜像文档里程序用的 UID，再把 data 下对应的目录 chown 过去。\n小结 这套约定说不上多高明，核心就是“一个目录装下一个服务的全部”，再加上 git 记录变更。整理完之后最直接的感受是心里有底了：哪个服务在哪、数据在哪、上次什么时候升的级，一眼就能看到。下一步要解决的，是这些 data 目录的异地备份。\n","permalink":"https://www.homeexit.net/posts/docker-compose-layout/","summary":"整理 Docker 虚拟机里的 compose 目录约定和更新流程，让十来个服务不再乱成一锅粥。","title":"用 Docker Compose 管理家里的服务：目录结构与更新流程"},{"content":"想要每天一份完整的备份目录，又不想每天都占用一份完整的空间。rsync 的 --link-dest 正好能做到：没变化的文件直接硬链接到上一次的备份，只有新增和修改过的文件才会真正复制。\n#!/bin/sh set -e SRC=/data/ DEST=/mnt/backup TODAY=$(date +%F) rsync -aH --link-dest=\u0026#34;$DEST/latest\u0026#34; \u0026#34;$SRC\u0026#34; \u0026#34;$DEST/$TODAY\u0026#34; ln -sfn \u0026#34;$DEST/$TODAY\u0026#34; \u0026#34;$DEST/latest\u0026#34; 每个日期目录看起来都是完整的一份，可以直接浏览、直接拷回去，但实际占用的空间只有每天变化的那部分。删掉某一天的目录也不影响其他日期，因为硬链接要等所有引用都删掉，数据才会真正释放。\n清理旧备份 find /mnt/backup -maxdepth 1 -type d -name \u0026#39;20*\u0026#39; -mtime +30 -exec rm -rf {} + 第一次运行时 latest 还不存在，rsync 会提示一句然后做一次全量复制，属于正常现象。目标盘需要是支持硬链接的文件系统，ext4、XFS、ZFS 都可以，exFAT 不行。\n","permalink":"https://www.homeexit.net/posts/rsync-incremental-backup/","summary":"每天一份完整的备份目录，实际只占用变化部分的空间。","title":"用 rsync --link-dest 做每日增量备份"},{"content":"上个月小区检修，一周里停了两次电。笔记本服务器有电池，没什么感觉；NAS 那台旧台式机可就惨了，两块机械盘直接断电。事后用上次配好的 smartctl 看了一眼，Power-Off_Retract_Count（非正常断电时磁头紧急归位的次数）又涨了。盘倒是没坏，但心里发毛，于是下单了一台 UPS。\n硬件和拓扑 买的是一台 650VA 的在线互动式 UPS，带 USB 通信口，接上电脑会被识别成一个 HID 设备，用 NUT（Network UPS Tools）的 usbhid-ups 驱动就能读。挂在它上面的有 NAS、笔记本、路由器和交换机，总负载六十瓦左右，标称能撑十几分钟。\nNUT 的结构是“一台机器连着 UPS，其他机器通过网络听它的”：\nNAS（192.168.10.20）用 USB 线连着 UPS，运行驱动和 upsd 服务，自己是 primary； 笔记本上的 Proxmox（192.168.10.5）作为 secondary，通过网络监听 UPS 状态。 为什么让 NAS 当主节点？因为它最怕断电，而且主节点会等其他机器都开始关机后才最后一个关，并在关机的最后通知 UPS 切断输出。\n主节点配置（NAS） apt install nut lsusb # 先确认能看到 UPS nut-scanner -U # 自动探测 USB 上的 UPS，输出可以参考着写 ups.conf Debian 12 里的 NUT 是 2.8 版本，配置都在 /etc/nut/ 下面，一共要改五个文件。先是驱动和用户：\n# /etc/nut/ups.conf [homeups] driver = usbhid-ups port = auto desc = \u0026#34;UPS next to the NAS\u0026#34; # 不信 UPS 自己报的低电量，改由下面两个阈值来判断 ignorelb override.battery.charge.low = 40 override.battery.runtime.low = 300 # /etc/nut/upsd.users [upsmon_local] password = CHANGE_ME_LOCAL upsmon primary [upsmon_remote] password = CHANGE_ME_REMOTE upsmon secondary 然后是运行模式、监听地址和 upsmon：\n# /etc/nut/nut.conf MODE=netserver # /etc/nut/upsd.conf LISTEN 127.0.0.1 3493 LISTEN 192.168.10.20 3493 # /etc/nut/upsmon.conf（只列出改动的几行） MONITOR homeups@localhost 1 upsmon_local CHANGE_ME_LOCAL primary SHUTDOWNCMD \u0026#34;/sbin/shutdown -h +0\u0026#34; NUT 2.8 开始用 primary/secondary 取代了以前的 master/slave，旧写法暂时还认，但网上老教程里的配置最好顺手改掉。\n改完之后我是直接重启了一次 NAS，比挨个重启驱动和服务省心。起来以后执行 upsc homeups@localhost，能看到 battery.charge、battery.runtime、ups.status 等一长串数据，就说明驱动正常了。ups.status 里 OL 表示市电供电，OB 表示在用电池，LB 表示低电量。\n从节点配置（Proxmox） PVE 宿主机上只需要客户端：\napt install nut-client sed -i \u0026#39;s/^MODE=.*/MODE=netclient/\u0026#39; /etc/nut/nut.conf echo \u0026#39;MONITOR homeups@192.168.10.20 1 upsmon_remote CHANGE_ME_REMOTE secondary\u0026#39; \u0026gt;\u0026gt; /etc/nut/upsmon.conf systemctl restart nut-monitor upsc homeups@192.168.10.20 ups.status # 能读到 OL 就说明连上了 PVE 关机时会先挨个关掉虚拟机和容器，所以不需要在每台虚拟机里都装 NUT。\n关机的逻辑 整个流程是这样的：停电后 UPS 状态变成 OB，upsmon 只记录、发通知，并不会马上关机；等电量低于 40% 或者预计剩余时间少于 300 秒，驱动把状态标成 LB，主节点随即发出强制关机（FSD）；secondary 收到后开始关机，主节点等它们断开连接（默认最多等 15 秒，由 HOSTSYNC 控制）后自己关机，最后让 UPS 延迟一小段时间切断输出。\n如果想要“停电满 5 分钟就关”这种按时间的策略，可以用 NUT 自带的 upssched 做定时器。我觉得按电量判断更直接，就没上。\n踩过的坑 UPS 自带的低电量阈值太低。 我这台默认要到电量很低、只剩两三分钟时才报 LB，而 PVE 关掉几台虚拟机就要一两分钟，旧电池的剩余时间估算又不准。所以我用了 ignorelb 加 override 的写法，宁可早点关。 驱动认不到设备。 装完包第一次启动时报权限错误，原因是 NUT 的 udev 规则在 UPS 插上时还没装好。把 USB 线拔了重插一下就好了。 测试别乱用 fsd。 upsmon -c fsd 会真的触发整套关机流程，所有机器都会关掉。我的测试方法是挑晚上没人用的时候，直接拔掉 UPS 的插头，盯着 journalctl -u nut-monitor -f 看，先能看到“on battery”的日志，等电量掉到阈值，再看各台机器是不是按顺序关机。 主节点关得太快？ 笔记本关机时要等虚拟机挨个关完，有时候 UPS 切断输出时它还没关完。好在它有自己的电池，断不断电都无所谓，这也算是笔记本当服务器的一个意外好处。 来电后自动开机。 NAS 是台式机，BIOS 里把“断电恢复后的行为”设成开机就行，UPS 切断输出再恢复供电时它会自己起来。笔记本就麻烦了：它有电池，从头到尾没真正断过电，关机之后不会自己开。好在它的有线网卡支持 Wake-on-LAN，我在 NAS 上加了一个开机后执行的小任务，等几分钟后发一个唤醒包给笔记本。 小结 NUT 的配置文件多，第一次看有点绕，但理清楚“谁连着 UPS、谁听谁的”之后其实很直观。配好之后又赶上了一次停电，NAS 和笔记本都按顺序关了机，来电后十分钟内全部恢复，算是没白折腾。\n","permalink":"https://www.homeexit.net/posts/nut-ups-shutdown/","summary":"给家里的 NAS 和 Proxmox 主机配好 NUT，停电时按剩余电量有序关机，来电后再自动开机。","title":"UPS 接入 NUT，停电后自动关机"},{"content":"上个月家里又多了一台机器：一台淘汰下来的旧台式机，装了 Debian 12，插了两块二手的 4T 机械盘，专门当存储用（下文就叫它 NAS）。二手盘便宜是便宜，但谁也不知道前任主人是怎么用的，心里总有点不踏实。再加上笔记本服务器里那块固态也跑了好几年，我决定把硬盘健康监控认真搞一下。\n目标很简单：硬盘出现变坏的苗头时，手机上能收到通知，而不是等到数据读不出来才发现。\nsmartctl：先看看盘的状况 SMART 是硬盘自己记录的健康数据，Linux 下用 smartmontools 里的 smartctl 来读：\napt install smartmontools smartctl --scan # 列出能识别的盘 smartctl -i /dev/sda # 型号、序列号、是否支持 SMART smartctl -H /dev/sda # 总体健康结论，PASSED 不代表万事大吉 smartctl -A /dev/sda # 属性表，重点看这个 smartctl -t short /dev/sda # 发起短自检，一般两分钟左右 smartctl -l selftest /dev/sda # 查看自检记录 smartctl -a /dev/nvme0 # NVMe 盘的输出格式不一样，用 -a 看全部 -H 的结论只有在属性跌破厂商设定的阈值时才会变成 FAILED，往往为时已晚。真正有参考价值的是属性表里的原始值（RAW_VALUE），机械盘我主要看这几项：\n5 Reallocated_Sector_Ct：已经被重映射的坏扇区数。不为零就要留意，持续增长就该准备换盘了。 197 Current_Pending_Sector：等待重映射的可疑扇区。 198 Offline_Uncorrectable：离线扫描时发现的无法纠正的扇区。 199 UDMA_CRC_Error_Count：传输校验错误，通常是线材或接口的问题，跟盘本身关系不大。 9 Power_On_Hours：通电时间，二手盘看这个就知道用了多久。 NVMe 固态是另一套格式，重点看 Percentage Used（寿命消耗百分比）、Available Spare（剩余备用块）和 Media and Data Integrity Errors。\n我那两块二手盘，一块通电三万多小时、各项干净；另一块的 199 有几百的计数，换了根 SATA 线之后就不再涨了，算是虚惊一场。\nsmartd：让它自己定期检查 手动看一次不难，难的是长期坚持。smartd 是 smartmontools 自带的守护进程，可以定时检查属性、安排自检、发现问题时调用脚本通知。配置文件是 /etc/smartd.conf：\n# /etc/smartd.conf # 默认的 DEVICESCAN 一行要注释掉，否则它后面的配置会全部被忽略 #DEVICESCAN -d removable -n standby -m root -M exec /usr/share/smartmontools/smartd-runner /dev/disk/by-id/ata-DISK_MODEL_SERIAL_A -a -o on -S on -n standby,q -s (S/../.././02|L/../../6/03) -W 4,45,55 -m \u0026lt;nomailer\u0026gt; -M exec /usr/local/bin/smartd-notify.sh /dev/disk/by-id/ata-DISK_MODEL_SERIAL_B -a -o on -S on -n standby,q -s (S/../.././02|L/../../7/03) -W 4,45,55 -m \u0026lt;nomailer\u0026gt; -M exec /usr/local/bin/smartd-notify.sh 逐个解释一下：\n-a：开启一组常用检查，包括健康状态、属性变化、错误日志和自检日志。 -o on -S on：打开盘自带的离线数据收集和属性自动保存。 -n standby,q：盘处于待机状态时跳过本次检查，不去把它唤醒，q 表示跳过时不写日志。 -s (S/../.././02|L/../../6/03)：每天凌晨 2 点做短自检，周六凌晨 3 点做长自检。格式是“类型/月/日/星期/小时”的正则，星期 1 是周一、7 是周日。两块盘的长自检我错开到了周六和周日，避免同时跑。 -W 4,45,55：温度变化超过 4 度记录一次，达到 45 度记一条信息，达到 55 度告警。 -m \u0026lt;nomailer\u0026gt; -M exec ...：不发邮件，改为执行指定的脚本。 设备路径用 /dev/disk/by-id/ 下面的名字而不是 /dev/sda，因为 sdX 的顺序每次开机都可能变，by-id 里带着型号和序列号，是固定的。\n通知脚本很短。我家里自建了一个 ntfy，直接用 curl 推过去：\n#!/bin/sh # /usr/local/bin/smartd-notify.sh # smartd 会通过环境变量把出问题的设备和原因传进来 curl -fsS \\ -H \u0026#34;Title: SMART alert on $(hostname)\u0026#34; \\ -H \u0026#34;Priority: high\u0026#34; \\ -d \u0026#34;${SMARTD_DEVICE}: ${SMARTD_FAILTYPE} - ${SMARTD_MESSAGE}\u0026#34; \\ https://ntfy.example.com/homelab-disk 记得 chmod +x。改完配置后验证一下：\n# 临时在某一行末尾加上 -M test，重启后会立刻触发一次通知，确认收到后再删掉 systemctl restart smartmontools journalctl -u smartmontools -n 50 # 看 smartd 有没有报配置错误 smartd -q showtests # 打印接下来的自检计划，检查正则写对没有 踩过的坑 DEVICESCAN 吃掉了后面的配置。 我第一次只是在文件末尾追加了自己的几行，结果怎么都不生效，翻了 man 才知道 DEVICESCAN 之后的行全被忽略了。 USB 硬盘盒读不到 SMART。 我还有一块放在 USB 硬盘盒里的冷备份盘，直接 smartctl -a 报错，要加 -d sat 告诉它走 SATA 转译。有些廉价硬盘盒的桥接芯片干脆不支持透传，那就没办法了。 Proxmox 上已经装好了。 PVE 默认就带了 smartmontools，smartd 也在跑，网页的“磁盘”页面能直接看到 SMART 状态。笔记本上我只改了配置文件加上通知，不需要另外安装。 服务名。 Debian 12 上的服务单元叫 smartmontools，smartd 只是它的别名，查日志时用 smartmontools 更稳妥。 小结 SMART 不能预测所有故障，很多盘是毫无征兆直接挂掉的，所以它替代不了备份，只是让“慢慢变坏”的那一类问题能被提前发现。配好之后基本不用管，手机上一直收不到消息，就是最好的消息。\n","permalink":"https://www.homeexit.net/posts/smartd-disk-health/","summary":"给家里的硬盘配上 smartd 定时自检和手机告警，顺便记下几个真正值得关注的 SMART 指标。","title":"用 smartctl 和 smartd 监控硬盘健康"},{"content":"上一篇把旧笔记本装成了 Proxmox VE，之后一个多月陆陆续续建了不少东西，删了又建，建了又删。刚开始我的原则很简单：能用 LXC 就用 LXC，毕竟省资源。后来踩了几次坑，才慢慢形成自己的判断标准，记录一下。\n本质区别 LXC 容器和宿主机共用同一个内核，本质上是一组被隔离起来的进程。启动只要一两秒，内存按实际用量算，一个跑 Debian 的空容器只占几十 MB。 虚拟机（KVM）有自己完整的内核和虚拟硬件，隔离更彻底，可以装任何系统，代价是要预留内存，启动也慢一些。 由此带来的直接后果是：容器里不能加载内核模块，很多内核参数也改不了，宿主机升级了内核，容器里的“内核”也跟着变；而虚拟机里怎么折腾都影响不到宿主机。\n用命令行创建 网页上点几下就能建，不过我更喜欢用命令行，方便记录，也方便以后重复。先下载容器模板，再创建一个非特权容器：\npveam update pveam available --section system | grep debian-12 # 模板的版本号以上一条命令的输出为准 pveam download local debian-12-standard_12.7-1_amd64.tar.zst pct create 101 local:vztmpl/debian-12-standard_12.7-1_amd64.tar.zst \\ --hostname test-ct \\ --cores 1 --memory 512 --swap 512 \\ --rootfs local-lvm:8 \\ --net0 name=eth0,bridge=vmbr0,ip=192.168.10.101/24,gw=192.168.10.1 \\ --unprivileged 1 \\ --features nesting=1 \\ --timezone host \\ --onboot 1 pct start 101 pct enter 101 # 直接进入容器的 shell，先用 passwd 设个密码 nesting=1 建议打开，新版本的 systemd 在容器里需要它，网页上建非特权容器时这一项默认就是勾上的。\n虚拟机也一样，下面是我给 Docker 准备的那台：\nqm create 200 --name docker \\ --ostype l26 --machine q35 --cpu host \\ --cores 2 --memory 6144 --balloon 2048 \\ --scsihw virtio-scsi-single \\ --scsi0 local-lvm:64,iothread=1,discard=on \\ --net0 virtio,bridge=vmbr0 \\ --ide2 local:iso/debian-12.7.0-amd64-netinst.iso,media=cdrom \\ --boot \u0026#39;order=scsi0;ide2\u0026#39; \\ --agent enabled=1 qm start 200 装好系统后记得在虚拟机里 apt install qemu-guest-agent 并启动它，这样 PVE 才能拿到虚拟机的 IP，关机时也能让虚拟机正常关机。--cpu host 把宿主机的 CPU 特性直接暴露给虚拟机，单机用没问题，以后如果要在不同型号的机器之间迁移再另说。\n我现在的选择标准 单一用途、轻量的服务，比如 DNS、一个小网页、一个同步服务：用非特权 LXC。资源占用小，备份恢复都快。 Docker：放虚拟机。Proxmox 的官方文档也建议把 Docker 这类应用容器跑在虚拟机里。在 LXC 里跑 Docker 不是不行，但要开 nesting，存储驱动、AppArmor 时不时出问题，宿主机一升级说不定又坏了。我一开始在容器里跑过一阵，最后还是挪进了虚拟机。 需要特定内核、内核模块，或者干脆不是 Linux：只能虚拟机。 需要整块设备直通，比如一张 PCIe 网卡或者一整个硬盘控制器：虚拟机。 拿不准的时候，我会问自己一个问题：这个服务挂了或者被攻破，会不会波及宿主机？如果担心，就放虚拟机。\n踩过的坑 1. 非特权容器的挂载权限。 非特权容器里的 UID 是映射过的：容器里的 root（UID 0）在宿主机上其实是 100000，容器里的 1000 对应宿主机的 101000。把宿主机目录 bind mount 进去之后，容器里看到的文件全属于 nobody，写也写不进去。最简单的办法是在宿主机上把目录的属主改成映射后的 UID：\n# 在宿主机上执行 mkdir -p /srv/share chown -R 101000:101000 /srv/share # 对应容器里 UID 1000 的用户 pct set 101 -mp0 /srv/share,mp=/mnt/share pct reboot 101 更精细的做法是改 /etc/subuid、/etc/subgid 和容器配置里的 lxc.idmap，把某个 UID 原样映射进去，不过对家用来说上面的办法就够了。\n2. 非特权容器里挂不了 NFS/SMB。 想在容器里直接 mount 网络共享是不行的，只能先在宿主机上挂好，再像上面那样 bind mount 进容器。\n3. bind mount 不会被备份。 PVE 自带的 vzdump 对容器和虚拟机都能备份，但容器的 bind mount 挂载点是不参与备份的，只有建在 PVE 存储上的卷才会。我一开始以为备份了整个容器就万事大吉，后来看备份文件的大小才发现不对。\n4. 容器里的时区。 模板默认是 UTC，日志里的时间总是差八个小时。创建时加上 --timezone host 就会跟宿主机保持一致，已经建好的容器可以用 pct set 101 --timezone host 补上。\n小结 LXC 和虚拟机不是二选一的关系，混着用才是常态。我现在的布局是：一台虚拟机专门跑 Docker，承载大部分服务；几个轻量的、跟基础设施有关的东西各自放一个小 LXC。资源最紧张的永远是内存，16G 分下来其实很快就见底了，下一步可能要考虑加条内存，或者干脆再添一台机器。\n","permalink":"https://www.homeexit.net/posts/proxmox-lxc-vs-vm/","summary":"在 Proxmox 上折腾了一个多月，总结 LXC 容器和虚拟机各自适合的场景，以及几个容易踩的坑。","title":"Proxmox 里 LXC 容器和虚拟机怎么选"},{"content":"抽屉里有台用了五六年的旧笔记本，i5-8250U、16G 内存，自从换了新电脑就一直在吃灰。我早就想在家里放一台常开的小服务器，跑点自己用的服务，但买 NAS 或者小主机都得花钱，也不确定自己能折腾多久，干脆先拿它练手。\n笔记本当服务器其实有几个天然的好处：\n功耗低，我这台空载实测十瓦出头，一个月的电费可以忽略； 自带电池，相当于一个迷你 UPS，短时间停电不会直接断电； 自带屏幕和键盘，网络配置搞砸了，打开盖子就能在本地救回来。 系统选了 Proxmox VE（下面简称 PVE），目前最新的是 8.2，底层是 Debian 12。直接装个 Debian 当然也能用，但我还不确定以后要跑哪些东西。有了 PVE，想试新东西就开个虚拟机或者容器，玩坏了删掉重来，宿主机本身始终保持干净。\n安装前的准备 先进 BIOS 做几件事：打开 CPU 虚拟化（VT-x），顺手把 VT-d 也打开，以后想做设备直通会用到。Secure Boot 我关掉了，PVE 从 8.1 开始其实支持 Secure Boot，但我不想在这上面多花时间。\n然后是网络。PVE 的虚拟机和容器都是通过网桥接到物理网卡上的，无线网卡基本没法拿来做网桥，所以一定要有有线网口。我这台正好带一个 RJ45 口，没有的话就得配个 USB 网卡。\n安装本身没什么好说的：下载官方 ISO，在另一台 Linux 上用 dd 写进 U 盘，引导起来一路下一步。磁盘用默认的 ext4（会顺带建好 LVM-thin 存储），IP 设成固定的 192.168.10.5，网关 192.168.10.1。装完在别的电脑浏览器里打开 https://192.168.10.5:8006，用 root 登录就能看到管理界面。\n装完先改软件源 没有订阅的话，默认启用的 enterprise 源在 apt update 时会报 401。把它注释掉，换成免费的 no-subscription 源：\n# 注释掉企业源（ceph.list 里如果也是 enterprise 源，一样处理） sed -i \u0026#39;s/^deb/# deb/\u0026#39; /etc/apt/sources.list.d/pve-enterprise.list sed -i \u0026#39;s/^deb/# deb/\u0026#39; /etc/apt/sources.list.d/ceph.list # 加上 no-subscription 源 echo \u0026#34;deb http://download.proxmox.com/debian/pve bookworm pve-no-subscription\u0026#34; \\ \u0026gt; /etc/apt/sources.list.d/pve-no-subscription.list # PVE 升级要用 full-upgrade，不要只用 apt upgrade apt update \u0026amp;\u0026amp; apt full-upgrade 登录时那个“没有有效订阅”的弹窗，网上有改前端 JS 去掉它的办法，我没弄：每次升级相关的包都会被覆盖回来，点一下确定也不费事。\n笔记本特有的几个设置 合盖不休眠。 服务器当然要合上盖子塞进柜子里，但默认合盖会触发挂起。改 logind 的配置：\n# /etc/systemd/logind.conf，[Login] 段下面这几行去掉注释并改成 ignore [Login] HandleLidSwitch=ignore HandleLidSwitchExternalPower=ignore HandleLidSwitchDocked=ignore 改完执行 systemctl restart systemd-logind 生效，合上盖子再从别的机器 ping 一下，确认没掉线。\n让屏幕自己熄灭。 PVE 装完是纯文本控制台，屏幕一直亮着，费电也伤屏。在内核参数里加上 consoleblank=60，一分钟没操作就熄屏：编辑 /etc/default/grub，把 GRUB_CMDLINE_LINUX_DEFAULT 改成 \u0026quot;quiet consoleblank=60\u0026quot;，然后执行 update-grub 并重启。如果安装时选了 ZFS 并且是 UEFI 启动，PVE 用的是 systemd-boot，要改的就是 /etc/kernel/cmdline，改完执行 proxmox-boot-tool refresh。\n电池。 锂电池长期满电对寿命不好。我这台的 BIOS 里有充电阈值的选项，设成了充到 80% 就停。没有这个选项的机器，至少隔段时间看看电池有没有鼓包。\n网络配置 PVE 的网络配置在 /etc/network/interfaces，安装程序会自动建一个叫 vmbr0 的网桥，把物理网卡挂上去，宿主机的 IP 也配在网桥上：\nauto lo iface lo inet loopback iface enp2s0 inet manual auto vmbr0 iface vmbr0 inet static address 192.168.10.5/24 gateway 192.168.10.1 bridge-ports enp2s0 bridge-stp off bridge-fd 0 以后建的虚拟机和容器默认都挂在 vmbr0 上，跟家里其他设备在同一个网段。\n踩过的坑 装完网页打不开。 第一次装的时候我顺手插着一个 USB 网卡，安装程序把管理网络绑到了它上面，而网线插的是板载网口。解决办法是在本地控制台改 interfaces 里的 bridge-ports，然后执行 ifreload -a（PVE 8 用的是 ifupdown2，改完不用重启）。 网卡名可能会变。 网上有不少人遇到过升级内核或者加了 PCIe 设备之后网卡名变化，网桥找不到网卡，整台机器失联。我后来用 systemd 的 .link 文件按 MAC 地址把网卡名固定了下来。动网络配置之前，记得确认自己能在本地控制台登录，给自己留好后路。 散热。 合盖之后键盘面的进风变差，跑了几天发现合盖时 CPU 温度明显高一截。最后把它竖着放进了一个文件架里，盖子留一条缝，温度就下来了。 小结 跑了两周，目前开着两个测试用的容器，整机功耗在十瓦到十五瓦之间，风扇几乎不转，比我预想的安静。对于刚开始折腾家庭服务器来说，一台旧笔记本加 PVE 是成本很低的入门方式，不满意了随时可以换硬件，虚拟机和容器都能备份后迁走。\n接下来要考虑的是：服务到底放在 LXC 容器里还是虚拟机里。这个等用一段时间再单独写一篇。\n","permalink":"https://www.homeexit.net/posts/proxmox-old-laptop/","summary":"把吃灰的旧笔记本装上 Proxmox VE，处理好软件源、合盖和熄屏问题，当成一台低功耗家庭服务器。","title":"给旧笔记本装 Proxmox VE 当家庭服务器"},{"content":"这里记录我折腾家庭服务器、NAS 和家里网络时踩过的坑，以及最后采用的方案，主要写给以后的自己看。\n家里的设备 一台退役的旧笔记本，装了 Proxmox VE，跑几个 LXC 容器和虚拟机； 一台 NAS，数据放在 ZFS 上，负责存储和备份； 一台低功耗小主机，用 Docker 跑 Home Assistant、Jellyfin、相册这类常驻服务； 一台 UPS，停电时负责让上面这些机器体面地关机。 关于文章 文章里的命令都在自己的机器上跑过，但每个人的环境不一样，照着操作之前请先做好备份。如果发现哪里写错了，欢迎指正。\n本站由 Hugo 生成，可以通过 RSS 订阅更新。内容采用 CC BY-NC 4.0 许可，转载请注明出处。\n","permalink":"https://www.homeexit.net/about/","summary":"\u003cp\u003e这里记录我折腾家庭服务器、NAS 和家里网络时踩过的坑，以及最后采用的方案，主要写给以后的自己看。\u003c/p\u003e\n\u003ch2 id=\"家里的设备\"\u003e家里的设备\u003c/h2\u003e\n\u003cul\u003e\n\u003cli\u003e一台退役的旧笔记本，装了 Proxmox VE，跑几个 LXC 容器和虚拟机；\u003c/li\u003e\n\u003cli\u003e一台 NAS，数据放在 ZFS 上，负责存储和备份；\u003c/li\u003e\n\u003cli\u003e一台低功耗小主机，用 Docker 跑 Home Assistant、Jellyfin、相册这类常驻服务；\u003c/li\u003e\n\u003cli\u003e一台 UPS，停电时负责让上面这些机器体面地关机。\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch2 id=\"关于文章\"\u003e关于文章\u003c/h2\u003e\n\u003cp\u003e文章里的命令都在自己的机器上跑过，但每个人的环境不一样，照着操作之前请先做好备份。如果发现哪里写错了，欢迎指正。\u003c/p\u003e","title":"关于"}]