自建服务 .cntr Compose 清理复盘
Jun 8, 2026 - ⧖ 21 minTLDR
这轮清理的核心结论是:.cntr 不应该继续承担”自托管服务收藏夹”的角色,它应该只保存仍有运行价值、边界清楚、可以复现的 Compose service。
服务可以删,知识点要留下。所以这份文档仍然保留了大量原始 YAML、旧笔记和逐项判断;它不是一篇压缩后的选型文章,而是一份带复盘结论的清理记录。
本轮处理里,大部分 service 被删除;golang 没删,而是从历史全家桶改造成共享 Go 微服务依赖栈。这也是本次复盘里最重要的反例:旧东西不一定都要删,有些应该重新定义职责。
背景
.cntr 最初是方便本地或 VPS 快速启动服务的 Compose 集合。问题是,随着实验过的工具越来越多,它逐渐从”现役服务目录”变成了”自托管服务收藏夹”:监控、面板、穿透、通知、自动化、AI image、RSS、网盘、开发依赖都堆在一起。
这种目录最容易产生两类负担。第一类是运行负担:即使服务没有启动,配置里仍然可能保留端口、默认密码、token 占位、数据卷和反代假设。第二类是认知负担:每次看到一个 service,都需要重新判断它到底是正在使用、曾经使用、未来可能使用,还是只是一个 recipe。
所以这轮清理不是单纯删文件,而是把 .cntr 重新收敛成更接近基础设施事实的目录:保留运行入口,删除兴趣清单,捞回知识点。
判断标准
这轮判断主要按下面几条规则走:
- 是否仍在现役链路里。如果没有调用方、反代、备份、告警或明确入口,就不能因为”也许以后有用”继续保留。
- 是否只是 recipe。包含
<YOUR_TOKEN>、password、localhost、Add other necessary variables这类模板痕迹的 compose,更像收藏的部署样例,不是部署事实。 - 是否有不可替代的状态。订阅、账号、workflow、监控项、credential、UI DB 这类状态如果没有备份策略,就不适合假装已经被声明式管理。
- 容器是否是最佳形态。DNS、远程桌面 agent、主机监控、PXE、VPN/组网这类强绑定宿主机能力的工具,不一定适合放在
.cntr。 - 是否和已有核心服务重复。监控面板、LLM gateway、自动化平台、穿透工具尤其容易重复。
- 删除后知识是否已经留下。可以删除 service,但要保留当时的选型判断、坑点和可迁移知识。
本轮结果速览
| 类别 | 删除 | 保留/改造 |
|---|---|---|
| 监控面板 | beszel, netdata, nezha, uptime | — |
| 网络穿透 | cloudflared, frp | — |
| 自动化 | n8n, qinglong | — |
| AI Model Routing | litellm, metapi | — |
| 其他 | adguardhome, portainer, ntfy, hedgedoc, miniflux, peinture, iventoy, s-ui | — |
| 开发依赖 | — | golang(改造) |
| 现役服务 | — | axonhub, caddy, sub-store, actions-runner, rustdesk, openlist |
下面是逐项记录。YAML codeblock 保留原始片段;明显的密钥、默认弱密码或疑似真实 token 不继续落盘。
adguardhome
---
# https://mynixos.com/nixpkgs/package/adguardhome
# https://mynixos.com/nixpkgs/options/services.adguardhome
# https://github.com/einverne/dockerfile/blob/master/adguardhome/docker-compose.yml
version : '3.8'
services :
adguardhome :
image : adguard/adguardhome
container_name : adguardhome
restart : unless-stopped
ports :
- 53:53/tcp
- 53:53/udp
- 80:80/tcp # Web interface port (can be changed)
- 443:443/tcp # HTTPS for web interface (optional)
- 3000:3000/tcp # Initial setup port (can be changed)
volumes :
- ./adguardhome-data/work:/opt/adguardhome/work
- ./adguardhome-data/conf:/opt/adguardhome/conf
environment :
# Optional: Set PUID and PGID for file permissions
# - PUID=1000
# - PGID=1000
- TZ=America/New_York # Replace with your timezone
AdGuardHome 可以容器化,但 DNS 不是普通 Web app。它绑定 53/tcp 和 53/udp,还会和 systemd-resolved、路由器 DHCP、局域网网关、防火墙规则产生强耦合。
如果它只是临时试用,compose 很方便;如果它要成为真实上游 DNS 或家庭网络基础设施,更适合用 NixOS 的 services.adguardhome 管理。宿主机服务能把监听地址、端口、防火墙、状态目录、开机顺序、resolved 行为放在同一份声明式配置里,不需要再多一层 Docker 网络和端口映射。
这份 compose 还暴露 80/443/3000,并且时区是模板值,说明它更像 recipe 而不是正在使用的部署事实。
beszel
- date : 2025-12-05
des : 新购了VPS,所以调研了beszel和nezha,最终部署了beszel。只从使用体验来说,这两个都各有问题:1、二者都不支持自动发现,都是需要先部署server,然后再由server下发token,配置agent的compose之后,再连回来,整个过程就不够“声明式”。并且他这个机制需要保证agent的token跟server在DB里存的(fingerprint跟token)能够保持一致。否则匹配不上,就需要重新像新host一样重新添加进来(一个场景,已经用这个universal token注册了3台机器,将来有新agent接入beszel server时,肯定要生成新token,如果改成新token,之前的3台机器肯定就掉了,所以之后只要新增host,就要保存相应token)。2、不使用nezha的原因在于,现在不支持预配置admin账号,只有OAuth一种添加账号的方式(非常蠢)。|||最终决定,暂时仍然使用beszel,之后转到dokploy之后,直接使用其内置的monitor就够了。
# 关于“agent自动加入server”的问题
# 这种涉及到 server-agent,需要把 agent(自动)加入回 server 的场景(不限于 monitor)(注意不一定是自动加入),大概有几种解决思路?各自的代表性工具分别是啥?
# 这类问题可以抽象成:“agent 怎么被 server 知道 + 怎么被信任” 这两个问题。
# - 静态白名单(ServerStatus)
# - 组织级 token(Nezha、Datadog 等)
# - 短期 join token + 长期证书(K8s、Consul 等)
# - 服务发现(Prometheus+K8s)
# - 局域网广播 / mDNS(家用 / IoT)
---
name : beszel
services :
beszel :
image : henrygd/beszel:latest
container_name : beszel
restart : unless-stopped
ports :
- 8090:8090
volumes :
- ./beszel_data:/beszel_data
- ./beszel_socket:/beszel_socket
beszel-agent :
image : henrygd/beszel-agent:latest
container_name : beszel-agent
restart : unless-stopped
network_mode : host
volumes :
- ./beszel_agent_data:/var/lib/beszel-agent
- ./beszel_socket:/beszel_socket
- /var/run/docker.sock:/var/run/docker.sock:ro
environment :
LISTEN : /beszel_socket/beszel.sock
HUB_URL : http://localhost:8090
TOKEN : <token>
KEY : "<key>"
Beszel 的核心问题不是“监控不好用”,而是它的加入流程不是声明式的。server 侧 DB 里保存的 token/fingerprint 与 agent 配置必须匹配,新增机器时容易把已有 agent 的信任关系变成运行态手工操作。
这类 server-agent 系统可以按两个问题拆:server 怎么知道 agent,server 怎么信任 agent。Kubernetes/Consul 这类用短期 join token 换长期证书;Prometheus 在 Kubernetes 里更多依赖服务发现;Datadog/Nezha 这类偏组织级 token;ServerStatus 更接近静态白名单。Beszel 对少量机器可用,但和 dotfiles/Nix 的声明式习惯不够贴。
如果之后确实上 Dokploy,直接复用它的内置 monitor 更符合“部署平台统一管理”的方向。
cloudflared
- date : 2025-08-01
des : |
移除“【技术选型】穿透工具”【frp】、【ngrok】、【cft】、【easytier】。这几个工具之间其实没有替代关系,确实各自都有自己的使用场景。具体是用哪个,可以用一下“4连问”找到匹配工具。是否为web服务(还是需要暴露TCP/UDP)?是否需要长期暴露?是否在用cloudflare?是否有公网VPS?如果是web服务且需要长期暴露且在cf,那就用Cloudflare Tunnel。如果只是暂时暴露,那用ngrok最方便(免费版session限8h)。如果需要长期暴露且有公网VPS(又或者需要暴露非Web协议(如SSH、数据库端口))那就用frp或者EasyTier。
写到这里,我想起来我之前遇到的一个情况就很典型。我之前有次需要在第二天去外地演示一个项目,但是呢,需要RDB, CK乱七八糟服务加起来在test环境数据量差不多700GB,这个服务才能拉起来。那天已经11点半了才意识到这个问题。为了第二天演示正常,就只能赶紧把所有服务在我本地起了一套,把数据也migrate了一份到本地。就很累很疲惫。如果当时知道有cloudflare tunnel的话(之前只知道ngrok,但是ngrok有8h限制,也就没用),其实就不需要这么搞了是吗?另外,其实也可以直接用RD工具直接连接查看也可以,但是这个场景下肯定不如cft好用。
---
services :
cloudflared :
image : cloudflare/cloudflared:latest
container_name : cloudflared
restart : unless-stopped
command : tunnel run --token <YOUR_TUNNEL_TOKEN>
# Alternatively, if using a named tunnel with a config file:
# command: tunnel --config /etc/cloudflared/config.yaml run <YOUR_TUNNEL_NAME>
volumes :
- ./config:/etc/cloudflared # Mount a local directory for configuration if using a config file
networks :
- my_network
networks :
my_network :
external : true # Or define it as a bridge network if not already existing
# services.cloudflared = {
# enable = true;
# package = pkgs.cloudflared;
# tunnels.${tunnelId} = {
# inherit credentialsFile;
# originRequest = {
# connectTimeout = "15s";
# tlsTimeout = "10s";
# tcpKeepAlive = "30s";
# keepAliveConnections = 64;
# noHappyEyeballs = true;
# };
# ingress = {
# "alist.lucc.dev" = {
# service = "http://127.0.0.1:5244";
# };
# };
# default = "http_status:404";
# };
# };
Cloudflare Tunnel 的最佳位置是“长期公开 Web 服务,且域名/DNS 已经在 Cloudflare”。它省掉公网 IP、反代入口和证书暴露,但它不是通用内网组网工具。
自己设备之间,Tailscale 更优先:直连成功时是 WireGuard 点对点,路径短、暴露面小;如果打洞失败走 DERP,中继性能才会退化。非自己设备、需要公网访问时,再按场景选择:临时 demo 用 ngrok,长期 Web 用 Cloudflare Tunnel,长期 TCP/UDP 且有 VPS 用 frp/EasyTier。
这份 compose 还是 token 模板态。真要长期跑 cloudflared,更建议用 named tunnel、声明式 ingress 和宿主机/NixOS service,而不是把 <YOUR_TUNNEL_TOKEN> 放在 compose recipe 里。
frp
- date : 2025-08-01
des : |
移除“【技术选型】穿透工具”【frp】、【ngrok】、【cft】、【easytier】。这几个工具之间其实没有替代关系,确实各自都有自己的使用场景。具体是用哪个,可以用一下“4连问”找到匹配工具。是否为web服务(还是需要暴露TCP/UDP)?是否需要长期暴露?是否在用cloudflare?是否有公网VPS?如果是web服务且需要长期暴露且在cf,那就用Cloudflare Tunnel。如果只是暂时暴露,那用ngrok最方便(免费版session限8h)。如果需要长期暴露且有公网VPS(又或者需要暴露非Web协议(如SSH、数据库端口))那就用frp或者EasyTier。
---
# https://github.com/nykma/frp
# https://mynixos.com/nixpkgs/package/frp
# https://mynixos.com/nixpkgs/options/services.frp
name : frp
services :
frpc :
image : nykma/frp:0.58.0
restart : always
volumes :
- ./config:/frp/config
- ./log:/frp/log
extra_hosts :
# To visit host's port in a container,
# you should fill in the correct IP of your docker host.
- "dockerhost:192.168.0.64"
# ports:
# - "127.0.0.1:7400:7400" # admin_port
frps :
image : nykma/frp:0.58.0
restart : always
volumes :
- ./config:/frp/config
- ./log:/frp/log
ports :
- "7000:7000" # bind_port
- "7000:7000/udp" # kcp_bind_port
- "7500:7500" # dashboard_port
# - "80:80" # vhost_http_port
# - "443:443" # vhost_https_port
# # WARNING: container up/down will be VERY SLOW (even failed)
# # if too much ports opened here!
- "20000-20020:2000-2020"
entrypoint :
- '/frp/frps'
- '-c'
- '/frp/frps.toml'
frp 的价值在于“有公网 VPS 时,把内网 TCP/UDP 服务长期暴露出去”。它比 Cloudflare Tunnel 更适合非 Web 协议,也比 ngrok 更适合长期固定入口。
它的代价是运维面变大:frps/frpc 配置、鉴权、dashboard 暴露、端口范围、日志、版本升级都需要自己负责。当前已经有 Tailscale 的前提下,自己的设备互联不需要 frp;只有“第三方公网访问 + 非 Web/TCP/UDP + 有 VPS”时才重新考虑。
原 compose 里的 20000-20020 映射和注释提醒也说明:端口范围越大,Docker compose 的创建/销毁成本越明显,长期维护时应只开放实际需要的端口。
hedgedoc
---
# https://docs.hedgedoc.org/setup/docker/
# https://docs.hedgedoc.org/guides/reverse-proxy/
name : hedgedoc
services :
database :
image : postgres:17-alpine
environment :
- POSTGRES_USER=hedgedoc
- POSTGRES_PASSWORD=password
- POSTGRES_DB=hedgedoc
volumes :
- database:/var/lib/postgresql/data
restart : always
app :
# Make sure to use the latest release from https://hedgedoc.org/latest-release
image : quay.io/hedgedoc/hedgedoc:latest
environment :
- CMD_DB_URL=postgres://hedgedoc:password@database:5432/hedgedoc
- CMD_DOMAIN=localhost
- CMD_URL_ADDPORT=true
volumes :
- uploads:/hedgedoc/public/uploads
ports :
- "3000:3000"
restart : always
depends_on :
- database
volumes :
database :
uploads :
HedgeDoc 是在线协作文档库,不只是 markdown preview。为了 preview 自建它,会额外引入 Postgres、上传文件存储、反代、身份认证、备份和升级维护。
如果只是本地 markdown 预览,编辑器 preview、静态站预览、GitHub/Gitea 渲染都更轻。自建协作文档只有在“多人实时编辑 + 在线发布 + 权限控制”同时成立时才值得。
这份 compose 里的 password 和 localhost 也是典型试用配置,不应该保留成可部署服务。
iventoy
---
# https://github.com/garybowers/iventoy_docker
# https://hub.docker.com/r/szabis/iventoy
# https://github.com/garybowers/iventoy_docker
# iVentoy(包括这个Docker镜像)在装机时确实需要通过LAN(有线网)来工作,因为它是一个基于PXE的网络启动工具,主要依赖局域网内的DHCP和TFTP服务来引导客户端从服务器上获取ISO镜像并启动安装。
# 为什么必须插网线?
#
# PXE引导机制:iVentoy服务器(运行在Docker中)和目标机器(装机机)必须在同一个局域网(LAN)内。目标机通过网络卡(NIC)从iVentoy服务器请求引导文件和ISO数据。如果没有网线连接,目标机就无法接入LAN,也就无法PXE启动。
# 没有WiFi支持:PXE标准主要针对有线网络(Ethernet),无线WiFi在BIOS/UEFI的PXE模式下支持有限或不稳定,尤其在装机初期(还没安装驱动)。如果你用WiFi适配器,它可能需要额外的驱动,但iVentoy的ISO挂载和数据传输仍需稳定的LAN连接。
# ISO传输:你把ISO存到iVentoy服务器上,目标机通过网络“挂载”它作为虚拟光驱。如果断网(无网线),传输就会失败。
# 简单来说,因为在装机时,还没有wifi,所以必须要插着网线走LAN,才能从另一台机器上拿到这个iventoy上存着的iso。
version : '3.9'
services :
iventoy :
image : ziggyds/iventoy:latest # Or another suitable iVentoy image like garybowers/iventoy:latest
container_name : iventoy
restart : always
privileged : true # iVentoy requires privileged mode for network operations
ports :
- 26000:26000 # Web UI
- 16000:16000 # HTTP server port (can be changed in iVentoy settings)
- 10809:10809 # iVentoy internal service
- 67:67/udp # DHCP (if iVentoy is managing DHCP)
- 69:69/udp # TFTP
volumes :
- ./iso:/app/iso # Mount a local 'iso' folder to store your ISO files
- ./data:/app/data # Mount a local 'data' folder for iVentoy configuration
- ./log:/app/log # Mount a local 'log' folder for iVentoy logs (optional)
environment :
- AUTO_START_PXE=true # Automatically starts the PXE service on container startup
iVentoy/PXE 的价值在批量装机和频繁切 ISO。单机偶发刷机时,U 盘更稳定、更少变量。
PXE 依赖 DHCP/TFTP 和同一局域网,通常还要求有线网卡启动。容器需要 privileged,还可能和路由器 DHCP 抢 67/udp,这类服务更像临时实验工具,不适合常驻在 .cntr。
如果未来重新使用,应该把它当临时装机环境启动,而不是作为长期 homelab service。
litellm
---
# Compose project name. 这会影响默认 network/volume 名称,例如
# litellm_default、litellm_litellm_postgres_data。
name : litellm
services :
litellm :
# 带 database extras 的官方 LiteLLM 镜像;本地试玩先用 main-stable。
# 长期主力或生产环境建议改成固定版本 tag 或 digest,避免不可预期升级。
image : ghcr.io/berriai/litellm-database:main-stable
container_name : litellm
restart : unless-stopped
command :
# 容器内读取下面只读挂载进去的 LiteLLM 配置。
- "--config=/app/config.yaml"
- "--port=4000"
# 本地开发先给 2 个 worker;如果只想减少资源占用,可以改成 1。
- "--num_workers=2"
ports :
# 只绑定 localhost:浏览器和本机 agent 可访问,但不会暴露到局域网。
- "127.0.0.1:4000:4000"
extra_hosts :
# Linux/NixOS 下让容器可以通过 host.docker.internal 访问宿主机服务。
# 如果 CPA 跑在宿主机或另一个本机 compose 端口上,CPA_BASE_URL 可以写成:
# http://host.docker.internal:<port>/v1
- "host.docker.internal:host-gateway"
environment :
# DATABASE_URL 是给 LiteLLM 用的连接串;postgres 是下面 service 的 DNS 名。
# POSTGRES_PASSWORD 由 Nix/session 提供,缺失时让 compose config/up 直接失败。
# 允许在 Admin UI/DB 中保存和管理模型配置。
STORE_MODEL_IN_DB : "True"
# Admin UI/API 的主密钥。LiteLLM 要求 master key 通常以 sk- 开头。
# 用于加密数据库中的敏感字段;已经创建模型/keys 后不要随意更换。
LITELLM_LOG : "INFO"
# 下面这些 provider key 都从 Nix/session 透传。默认空字符串的目的:
# 没配置某个 provider 时,stack 仍能启动,只是调用对应模型会失败。
OPENAI_API_KEY : "${OPENAI_API_KEY:-}"
ANTHROPIC_API_KEY : "${ANTHROPIC_API_KEY:-}"
# MetAPI 这里按 OpenAI-compatible provider 接入,通常 base url 要带 /v1。
# CPA/AutoTeam 这类服务负责 ChatGPT/Codex OAuth 账号池和保活;
# LiteLLM 只把它当 OpenAI-compatible API 使用,不直接管理 OAuth token。
# LiteLLM 对外暴露的是 model_name;客户端只需要传这些短名字。
# 真正调用哪个 provider/model,由每个条目的 litellm_params.model 决定。
model_list :
- model_name : deepseek-chat
litellm_params :
# DeepSeek 官方 provider。客户端请求 model=deepseek-chat 时会走这里。
model : deepseek/deepseek-chat
# LiteLLM 的 os.environ/VAR 语法:运行时从容器环境变量读取密钥。
api_key : os.environ/DEEPSEEK_API_KEY
- model_name : claude-sonnet
litellm_params :
# Anthropic 官方 provider。Claude Code 可以把这个名字当默认 Sonnet 模型用。
model : anthropic/claude-sonnet-4-5-20250929
api_key : os.environ/ANTHROPIC_API_KEY
- model_name : codex-gpt
litellm_params :
# CPA/AutoTeam 负责 ChatGPT Team/Codex OAuth 账号池;
# LiteLLM 这里只按 OpenAI-compatible endpoint 调用它。
# 如果 CPA 暴露的模型名不是 gpt-5.3-codex,只需要改这一行。
model : openai/gpt-5.3-codex
# CPA_BASE_URL 通常应带 /v1,例如 http://host.docker.internal:8317/v1。
api_base : os.environ/CPA_BASE_URL
api_key : os.environ/CPA_API_KEY
general_settings :
# Proxy/Admin UI 主密钥;也可作为管理员 API key 调用 /key/generate 等接口。
master_key : os.environ/LITELLM_MASTER_KEY
# 开启数据库后,virtual key、spend logs、UI 模型管理才有持久状态。
database_url : os.environ/DATABASE_URL
litellm_settings :
# 给 Claude Code/agent 长请求留足时间;太短容易误判 provider 超时。
request_timeout : 600
# 单个模型组失败后先重试 2 次,仍失败才进入 fallback。
num_retries : 2
# claude-sonnet 连续失败后,依次回退到 MetAPI 和 CPA。
# 注意 fallback 只在请求失败后触发,不会做负载均衡。
fallbacks : [{ "claude-sonnet" : [ "metapi-gpt-5.5" , "codex-gpt" ]}]
LiteLLM 不只是 SDK,它确实可以作为 OpenAI-compatible proxy/model routing service:模型别名、fallback、virtual key、spend log、provider 统一封装都是它的强项。
但它是否能替代 AxonHub,取决于 Codex/Pi/Claude 这类 agent 对 Responses API、streaming、tool calls、错误语义和 provider 特性的兼容。当前配置里 codex.nix 和 pi-agent.nix 已经明确使用 AxonHub 的 responses/openai-responses 路径;LiteLLM 更像通用 LLM 网关,不一定能无痛覆盖这条事实路径。
这份 compose 是“后路实验件”,不是生产可复现方案。删除它的意义是避免保留硬编码密钥和半成品备用网关。将来如果 AxonHub 不好用,应该重新做一轮 LiteLLM evaluation:固定镜像版本、密钥走 sops、验证 Responses/tool calls、再决定是否替换默认 provider。
metapi
---
services :
metapi :
image : 1467078763/metapi:latest
env_file :
- .env
ports :
- "4000:4000"
volumes :
- ./data:/app/data
environment :
AUTH_TOKEN : ${LLM_MetAPI}
PROXY_TOKEN : ${LLM_MetAPI}
CHECKIN_CRON : "0 8 * * *"
BALANCE_REFRESH_CRON : "0 * * * *"
PORT : ${PORT:-4000}
DATA_DIR : /app/data
TZ : ${TZ:-Asia/Shanghai}
restart : unless-stopped
MetAPI 这里承担两类职责:API proxy/token 入口,以及 check-in/balance refresh 这种账号状态维护。它和 LiteLLM/AxonHub/CPA 的边界容易重叠。
如果 AxonHub 已经作为统一 LLM provider,MetAPI 作为独立 service 的价值会下降;如果只是为了某个上游账号池保活,应尽量把职责收进主网关或明确成单独的运行任务,不要留下一个没人确认调用路径的端口服务。
自动 check-in 类服务通常最脆:上游页面/API 变化、风控、token 失效都会让它从“省事”变成“持续排障”。只有高频依赖时才值得保留。
miniflux
---
# https://miniflux.app/docs/docker.html#docker-compose
# https://github.com/electh/nextflux/blob/main/compose.yml
name : miniflux
services :
miniflux :
image : miniflux/miniflux:latest
container_name : miniflux
ports :
- "5254:8080"
depends_on :
db :
condition : service_healthy
environment :
- DATABASE_URL=postgres://miniflux:secret@db/miniflux?sslmode=disable
- RUN_MIGRATIONS=1
- CREATE_ADMIN=1
- DISABLE_HSTS=1
- ADMIN_USERNAME=admin
- ADMIN_PASSWORD=<redacted-default-password>
healthcheck :
test : [ "CMD" , "/usr/bin/miniflux" , "-healthcheck" , "auto" ]
restart : unless-stopped
db :
image : postgres:17-alpine
container_name : miniflux-db
environment :
- POSTGRES_USER=miniflux
- POSTGRES_PASSWORD=<redacted-default-password>
- POSTGRES_DB=miniflux
volumes :
- miniflux-db:/var/lib/postgresql/data
nextflux :
image : electh/nextflux:latest
container_name : nextflux
ports :
- 3000:3000
depends_on :
miniflux :
condition : service_healthy
restart : unless-stopped
Miniflux 是很好的轻量 RSS reader,但它本质上是长期个人数据服务:订阅源、已读状态、收藏、API token 都需要备份和迁移策略。
如果 RSS 工作流已经迁移到别处,保留这份 compose 只会增加“也许我还有一个 reader”的认知负担。nextflux 也说明这不是单纯 Miniflux,而是一套额外前端/扩展方案。
未来如果重启 RSS 服务,应先决定数据归属:是作为 k8s app、NixOS service,还是本地临时 compose。长期使用时必须去掉默认账号和默认数据库密码。
n8n
---
# # https://mynixos.com/nixpkgs/options/services.n8n
# # https://mynixos.com/nixpkgs/package/n8n
# # [基于 n8n 的开源自动化:以滴答清单同步 Notion 为例 | 少数派会员 π+Prime](https://sspai.com/prime/story/automation-n8n)
#
# n8n 配置
N8N_VERSION=latest
N8N_PORT=5678
N8N_BASIC_AUTH_USER=admin
N8N_BASIC_AUTH_PASSWORD=<redacted-default-password>
# n8n PostgreSQL 配置
N8N_PGSQL_PORT=15432
N8N_PGSQL_USERNAME=n8n
N8N_PGSQL_PASSWORD=<redacted-default-password>
N8N_PGSQL_DATABASE=n8n
n8n 的价值在个人自动化,但它会迅速变成状态中心:workflow、credential、webhook URL、执行记录、数据库都要长期维护。
如果只是探索自动化,删掉 compose 没损失;如果真作为生产自动化,应该在 NixOS/k8s 中完整声明 Postgres、secret、域名、反代、备份和升级策略。半成品 .env 只会留下默认密码和不可复现状态。
这类工具很容易把“偶尔自动化一下”变成新的平台维护工作。删除的判断标准应该是:是否有仍在跑的 webhook/cron/workflow,而不是工具本身是否强大。
netdata
---
# https://learn.netdata.cloud/docs/netdata-agent/installation/docker
name : netdata
services :
netdata :
image : netdata/netdata
container_name : netdata
pid : host
network_mode : host
restart : unless-stopped
cap_add :
- SYS_PTRACE
- SYS_ADMIN
security_opt :
- apparmor:unconfined
volumes :
- netdataconfig:/etc/netdata
- netdatalib:/var/lib/netdata
- netdatacache:/var/cache/netdata
- /:/host/root:ro,rslave
- /etc/passwd:/host/etc/passwd:ro
- /etc/group:/host/etc/group:ro
- /etc/localtime:/etc/localtime:ro
- /proc:/host/proc:ro
- /sys:/host/sys:ro
- /etc/os-release:/host/etc/os-release:ro
- /var/log:/host/var/log:ro
- /var/run/docker.sock:/var/run/docker.sock:ro
- /run/dbus:/run/dbus:ro
# options.modules.services.netdata = {
# enable = mkEnableOption "Netdata metrics collector";
#
# listenAddress = mkOption {
# type = types.str;
# default = "127.0.0.1";
# description = "Address Netdata binds to (defaults to localhost so it is only reachable via the reverse proxy).";
# };
#
# listenPort = mkOption {
# type = types.port;
# default = 19999;
# description = "TCP port Netdata listens on.";
# };
#
# ingress = mkOption {
# type = types.nullOr (mylib.ingressOption "Netdata");
# default = null;
# description = "Expose Netdata through the shared reverse proxy.";
# };
# };
#
# services.netdata = {
# enable = true;
# package = pkgs.netdata.override {withCloudUi = true;};
# config.global."memory mode" = "ram";
# config.web."bind to" = "${cfg.listenAddress}:${listenPortStr}";
# };
Netdata 的 Docker 部署基本等于给容器大量宿主机视野:host network、host pid、SYS_PTRACE、SYS_ADMIN、apparmor:unconfined、/proc、/sys、rootfs、Docker socket。它作为监控 agent 可以理解,但安全边界已经很薄。
如果需要长期主机监控,更适合明确地作为 NixOS service 或受控 agent 部署,至少它的权限、监听地址、反代入口、插件范围都在系统配置里可审计。
这段旧 Nix 模块草稿里的关键思想值得保留:默认只监听 127.0.0.1,通过反代暴露;插件按需要打开;服务权限显式收敛。删除 compose 是合理的,因为它和 Beszel/Nezha/Uptime Kuma 形成了重复监控栈。
nezha
---
# https://mynixos.com/nixpkgs/package/nezha-agent
# https://mynixos.com/nixpkgs/options/services.nezha-agent
# https://mynixos.com/nixpkgs/package/nezha-theme-admin
# https://github.com/hamster1963/nezha-dash
# https://mynixos.com/nixpkgs/package/nezha-theme-nazhua
# https://mynixos.com/nixpkgs/package/nezha
# https://github.com/nezhahq/nezha
version : '3.8'
services :
nezha-server :
image : nezha/dashboard:latest
container_name : nezha-server
ports :
- "8008:8008" # Or your desired port
environment :
- NZ_DEBUG=false
- NZ_GRPC_PORT=5555 # For agent communication
# Add other necessary environment variables for database connection, etc.
volumes :
- ./data/nezha-server:/dashboard/data # Persist data
nezha-agent :
image : nezha/agent:latest
container_name : nezha-agent
environment :
- NZ_SERVER=nezha-server:5555 # Connect to the server container
- NZ_KEY=YOUR_AGENT_SECRET_KEY # Replace with your actual key
# Add other environment variables as needed
network_mode : host # Or connect to a custom network if preferred
Nezha 和 Beszel 属于同一类 server-agent 监控系统。它轻量、中文生态强,但 agent 加入、密钥、账号初始化和 UI 状态仍然是运行态问题。
当监控目标只有少量 VPS 时,Nezha 很好用;当目标是把 homelab 变成声明式系统时,server 下发 token、UI OAuth、主题和 DB 状态都会变成不可复现部分。
这份 compose 还包含 YOUR_AGENT_SECRET_KEY 和 “Add other necessary environment variables” 这种模板占位,说明它不是现役部署事实。
ntfy
---
# https://docs.ntfy.sh/install/#docker
name : ntfy
services :
ntfy :
image : binwiederhier/ntfy
container_name : ntfy
command :
- serve
environment :
- TZ=UTC # optional: set desired timezone
user : UID:GID # optional: replace with your own user/group or uid/gid
volumes :
- /var/cache/ntfy:/var/cache/ntfy
- /etc/ntfy:/etc/ntfy
ports :
- 80:80
healthcheck : # optional: remember to adapt the host:port to your environment
test : [ "CMD-SHELL" , "wget -q --tries=1 http://localhost:80/v1/health -O - | grep -Eo '\"healthy\"\\s*:\\s*true' || exit 1" ]
interval : 60s
timeout : 10s
retries : 3
start_period : 40s
restart : unless-stopped
init : true # needed, if healthcheck is used. Prevents zombie processes
ntfy 是很好的轻量通知中枢,适合把脚本、监控、CI 的事件推到手机或桌面。但只有在它被多个系统实际调用时,才值得作为基础服务维护。
当前 compose 仍有 UID:GID、TZ=UTC、直接占用 80:80、宿主 /etc/ntfy 这类模板痕迹。没有配套 topic、认证、反代、TLS、调用方,就只是一个未落地的推送服务。
如果以后需要通知,先看是否已有 Bark/Telegram/ServerChan/Resend 之类路径。ntfy 的优势是自托管和简单 HTTP API,代价是又多一个公开入口与数据目录。
portainer
---
version : '3.8'
services :
portainer :
image : portainer/portainer-ce:latest
command : -H unix:///var/run/docker.sock --data /data
ports :
- "8000:8000"
- "9443:9443"
volumes :
- /var/run/docker.sock:/var/run/docker.sock
- portainer_data:/data
restart : always
volumes :
portainer_data :
Portainer 的问题不是功能不足,而是它天然鼓励 UI 改 Docker 状态。对于 dotfiles/Nix/compose 管理的机器,这会制造第二套事实来源。
挂载 /var/run/docker.sock 基本等价于授予宿主机 root 级控制能力。作为临时排障 UI 可以接受,作为常驻备用服务不划算。
如果只是偶尔看容器,docker ps/logs/inspect、docker compose ps/logs/config 更符合当前仓库的声明式风格。
s-ui
---
name : s-ui
services :
s-ui :
image : alireza7/s-ui
container_name : s-ui
hostname : "s-ui"
volumes :
- "./db:/app/db"
- "./cert:/app/cert"
tty : true
restart : unless-stopped
ports :
- "2095:2095"
- "2096:2096"
networks :
- s-ui
entrypoint : "./entrypoint.sh"
networks :
s-ui :
driver : bridge
s-ui 属于代理/节点管理面板类工具。它的便利点是 UI 配置,风险也正是 UI 配置:证书、入站、用户、流量策略都会沉到面板 DB 里。
如果目标是可复现基础设施,应优先用已有的 NixOS sing-box/mihomo 模块或明确的配置文件。面板适合临时管理多用户节点,不适合作为 dotfiles 中长期保留的隐藏状态。
这份 compose 还挂了 ./db 和 ./cert,说明真正重要的数据不在 Git 里;一旦不再运行,就应该删掉 service recipe,避免误以为还有可恢复配置。
uptime
---
# https://github.com/louislam/uptime-kuma/blob/master/compose.yaml
name : uptime
services :
uptime-kuma :
image : louislam/uptime-kuma:2
restart : unless-stopped
volumes :
- ./data:/app/data
ports :
# <Host Port>:<Container Port>
- "3001:3001"
Uptime Kuma 很适合小规模黑盒监控:HTTP ping、TCP ping、证书过期、通知集成。但它也是 UI 状态型服务,监控项、通知渠道、状态页都在数据目录里。
当前已经删掉 Netdata/Nezha/Beszel 这些监控栈,Uptime Kuma 继续保留会重复制造“另一个监控系统”。如果没有明确的外部通知链路和被监控目标清单,删除更干净。
如果未来需要公网可见的 uptime 页面,应该从“谁需要看、告警去哪、状态数据是否要备份”开始设计,而不是先保留 compose。
qinglong
# https://linux.do/t/topic/483523
# https://linux.do/t/topic/32503/75
# shufflewzc/faker2:京东脚本库助力池版,提供强大的京东自动化功能。
# shufflewzc/faker3:京东脚本库内部互助版,适合团队或互助小组使用。
# 6dylan6/jdpro:另一个京东脚本库,与faker形成互补,持续更新,适用于不同需求。
# leafTheFish/DeathNote:提供多个APP签到类脚本,JS加密保护,安全可靠。
# smallfawn/QLScriptPublic:包含近百个APP、小程序签到类脚本,部分经过JS加密处理。
# lzwme/ql-scripts:个人维护的基于需求的自用青龙脚本集,以TypeScript编写,实用性强。
# https://qinglong.online/en/guide/getting-started/installation-guide/docker-compose
# https://github.com/Sitoi/dailycheckin
# https://qd-today.github.io/qd/
# https://github.com/qd-today/qd
# https://github.com/einverne/dockerfile/tree/master/qiandao
services :
web :
image : whyour/qinglong:latest # 基于 Debian 的版本:whyour/qinglong:debian
volumes :
- ./data:/ql/data
ports :
- "5700:5700"
environment :
QlBaseUrl : '/' # 部署路径非必须,以斜杠开头和结尾,比如 /test/
restart : unless-stopped
青龙本质上是 cron + 脚本仓库 + env/cookie 管理 + UI + 日志 + 通知 的打包。它胜在简单,尤其适合大量社区签到脚本,因为很多脚本默认就是按青龙环境写的。
但长期保留它的代价也很明确:脚本、cookie、token、日志和执行状态都会沉到 /ql/data 与 UI 里,和当前 dotfiles/Nix/GitHub Actions 的声明式方向冲突。自动签到类脚本还经常涉及账号凭据,使用公益站或第三方托管时尤其要谨慎。
如果只是少量仍有价值的脚本,更适合迁到 self-hosted GitHub Actions、systemd timer 或普通 cron:workflow/脚本/依赖进入 Git,secrets 走 GitHub Secrets、sops 或 host env。现有青龙脚本不一定 1:1 可搬,依赖青龙 API、sendNotify.js、/ql/data/scripts 或 UI env 管理的脚本需要改造;但普通 Node/Python 脚本通常可以直接迁。
删除青龙的判断不是“它没用”,而是当前可薅的收益已经不足以支撑一个独立平台。未来如果重新需要签到自动化,应优先迁移少量有效脚本,而不是恢复整套青龙生态。
peinture
---
services :
# Imagine Server 应用
app :
image : ghcr.io/amery2010/imagine-server:v1.3.0
# 如果你想绝对固定版本,改成 digest 更稳:
# image: ghcr.io/amery2010/imagine-server@sha256:fc6ae8723582fd9fce18d4f311ed81b3a1fbadd07fa6883ed90897e78e7cf8ba
container_name : imagine-server
ports :
- "3000:3000"
environment :
- NODE_ENV=production
- PORT=3000
- REDIS_URL=redis://redis:6379
- API_TOKEN=${API_TOKEN}
- ENCRYPTION_KEY=${ENCRYPTION_KEY}
- ADMIN_TOKEN=${ADMIN_TOKEN}
- HUGGINGFACE_TOKENS=${HUGGINGFACE_TOKENS}
- GITEE_TOKENS=${GITEE_TOKENS}
- MODELSCOPE_TOKENS=${MODELSCOPE_TOKENS}
- A4F_TOKENS=${A4F_TOKENS}
- GEMINI_TOKENS=${GEMINI_TOKENS}
- GROK_TOKENS=${GROK_TOKENS}
- OPENAI_TOKENS=${OPENAI_TOKENS}
- MODELSLAB_TOKENS=${MODELSLAB_TOKENS}
- GEMINI_API_BASE=${GEMINI_API_BASE}
- GROK_API_BASE=${GROK_API_BASE}
- OPENAI_API_BASE=${OPENAI_API_BASE}
- S3_ENDPOINT=${S3_ENDPOINT}
- S3_REGION=${S3_REGION}
- S3_ACCESS_KEY_ID=${S3_ACCESS_KEY_ID}
- S3_SECRET_ACCESS_KEY=${S3_SECRET_ACCESS_KEY}
- S3_BUCKET_NAME=${S3_BUCKET_NAME}
- S3_CDN_URL=${S3_CDN_URL}
depends_on :
redis :
condition : service_healthy
restart : unless-stopped
networks :
- imagine-server-network
# Redis 存储
redis :
image : redis:8-alpine
container_name : imagine-server-redis
ports :
- "6379:6379"
volumes :
- redis-data:/data
restart : unless-stopped
networks :
- imagine-server-network
command : redis-server --appendonly yes
networks :
imagine-server-network :
driver : bridge
volumes :
redis-data :
driver : local
# =========================
# Basic
# =========================
API_TOKEN=
ENCRYPTION_KEY=
ADMIN_TOKEN=
# =========================
# Provider Tokens
# 留空表示不用该渠道
# 多个值如果项目支持,一般按逗号分隔
# =========================
HUGGINGFACE_TOKENS=
GITEE_TOKENS=
MODELSCOPE_TOKENS=
A4F_TOKENS=
GEMINI_TOKENS=
GROK_TOKENS=
OPENAI_TOKENS=${LLM_MetAPI}
MODELSLAB_TOKENS=
# =========================
# Custom API Base
# 不需要自定义时可留空
# =========================
GEMINI_API_BASE=
GROK_API_BASE=
OPENAI_API_BASE=https://api.lucc.dev/v1
# =========================
# S3 / Object Storage
# 不用对象存储时可留空
# =========================
S3_ENDPOINT=https://${CF_ACCOUNT}.r2.cloudflarestorage.com
S3_REGION=auto
S3_ACCESS_KEY_ID=${CF_R2_AK}
S3_SECRET_ACCESS_KEY=${CF_R2_SK}
S3_BUCKET_NAME=seed-flow
S3_CDN_URL=https://sf.lucc.dev
### 路线 B:BYOK + 自建前端 / 网关
这条路线更像:
- 前端或后端是你自己搭的
- 但真正的推理算力来自外部 provider
- 你通过自己的 API key / token 去调用别人的模型
这类方案更接近:
- Peinture
- imagine-server
它们更像是一种“**自建的使用层**”,而不是“**自建的推理层**”。
:::warning 这是一个很关键的区分
“我有一个自己部署的网站”
不等于
“我真正掌控了模型和推理”。
前者可能只是自建 UI / API 网关;后者才是本地推理栈。
:: :
---
## 为什么我最后会停在 Peinture + imagine-server
在当前阶段,这套组合对我有很强的吸引力,原因很现实:
- 它是 WebUI
- 它是 image-first
- 它支持 BYOK
- 我不需要自己持有 GPU
- 我可以自己控制 provider key
- 我可以自己决定把哪些东西上传到对象存储
这套组合给我的感觉不是“终局”,而是一个非常合理的 **阶段性方案**。
它解决的是:
- 我现在就想玩
- 但我又不想立刻买 GPU
- 我也不想一开始就进本地推理深坑
- 我更想先把“生成 — 保存 — 复用”这条链条跑通
> Peinture 更像前台,imagine-server 更像后台。
> 它们组合起来,能形成一个 BYOK 的 image-first 过渡平台。
但这个判断成立的前提是:我必须接受它不是终局。
它的问题也很明确:
- 它不是成熟的 Prompt 管理系统
- 它不是完整的 Recipe 数据库
- 它更像“前端 + 网关 + 对象存储”的组合
- 真正的长期资产沉淀,不是靠它内置的数据库能力,而是靠底层对象存储
Peinture / imagine-server 不是随手实验,它对应的是 AI image 工具链里的一个明确阶段:BYOK + image-first 自建前端/网关。这条路线的价值是不用立刻买 GPU,也不用直接进入 ComfyUI/InvokeAI/SwarmUI 的本地推理维护成本,就能先验证“生成 -> 保存 -> 复用”的工作流。
但它不是终局。真正有长期价值的不是这套服务本身,而是生成结果和 metadata sidecar 组成的 recipe 资产:图代表结果,metadata 代表过程。只保存 prompt 不够,保存 image + model/provider + params + seed/reference + storage metadata 才接近可迁移、可复用的资产。
当前删除 .cntr/peinture 的原因是:它作为阶段性方案的知识点值得保留,但运行服务已经没有现役引用,也没有接入 Caddy/tailnet/secrets 的正式部署边界。继续保留 compose 会让 .cntr 承担一个未使用的过渡平台。
如果未来重启 AI image 工作流,应重新比较三条路线:轻度探索用第三方平台;想掌握 token/provider/前端体验时用 BYOK 网关;持续深玩、需要控制权/复现性/自定义节点时再走本地 GPU 或全控制云端 ComfyUI 工作台。
golang
# 旧状态摘录:这个目录原本不是 Go runtime,而是混合的本地微服务依赖栈。
networks :
backend :
driver : ${NETWORKS_DRIVER:-bridge}
name : devenv_backend
volumes :
mysql_data :
name : devenv_mysql_data
redis_data :
name : devenv_redis_data
clickhouse_data :
name : devenv_clickhouse_data
prometheus_data :
name : devenv_prometheus_data
grafana_data :
name : devenv_grafana_data
nightingale_mysql_data :
name : devenv_nightingale_mysql_data
n8n_pgsql_data :
name : devenv_n8n_pgsql_data
services :
# # 开发环境 - Golang
# golang:
# build:
# context: ./golang
# container_name: devenv_golang
mysql :
image : mysql:${MYSQL_VERSION:-5.7}
container_name : devenv_mysql
redis :
image : redis:${REDIS_VERSION:-7-alpine}
container_name : devenv_redis
volumes :
- ./configs/redis.sh:/usr/local/bin/redis-optimize.sh:ro
caddy :
image : stefanprodan/caddy:${CADDY_VERSION:-latest}
depends_on :
- prometheus
- grafana
- alertmanager
- pushgateway
jaeger :
image : jaegertracing/all-in-one:${JAEGER_VERSION:-1.28}
dtm :
image : yedf/dtm:latest
volumes :
- ./configs/dtm-config.yml:/app/dtm/configs/config.yaml:ro
# DTM 的有效知识点:Go 微服务场景里,DTM 可以通过 etcd 做服务发现。
ms :
Driver : dtm-driver-gozero
Target : etcd://etcd:2379/dtmservice
EndPoint : dtm:36790
这次没有删除 .cntr/golang,而是把它从历史全家桶改造成共享 Go 微服务依赖栈。保留目录名 golang 是为了避免和 devenv、devbox 这些成熟工具产生命名歧义;这里不做 shell/toolchain 管理,只提供 container dependencies。
旧配置的问题是职责漂移:Go app 容器已经被注释掉,实际只剩 MySQL、Redis、etcd、Jaeger、DTM 等依赖;同时残留 Caddy、Prometheus/Grafana、Nightingale、n8n、ClickHouse 配置和 init SQL。docker compose config 已经无法通过,因为 caddy 依赖未定义的监控服务。
综合判断是:devenv/devbox 适合定义项目开发环境,Docker Compose 仍然更适合作为跨项目共享的 container 依赖栈。新的 .cntr/golang 使用 Compose profiles 按需启动 db、cache、ms、trace、analytics,避免默认 up 起全套。
结论
这轮清理之后,.cntr 的边界更清楚了:它应该保存可运行、可复现、仍然有现役意义的 Compose service,而不是保存每一个曾经研究过、部署过、也许以后会用到的自托管工具。
对个人基础设施来说,真正难的不是部署一个 service,而是长期解释它为什么还在。只要一个服务没有调用方、没有状态备份、没有明确入口、没有维护计划,它就会从“备用方案”变成“认知噪音”。这种情况下,删除 compose 反而是更诚实的做法。
但删除不等于丢掉经验。像 Beszel/Nezha 里的 agent join 模型、Cloudflare Tunnel/frp/Tailscale 的边界、Qinglong 与 Actions Runner 的替代关系、Peinture/imagine-server 的 BYOK image-first 阶段判断,都是值得留下的知识点。服务可以删,知识点要留下。
golang 是另一个方向的例子。它没有被删除,而是被重新定义成共享 Go 微服务依赖栈。这说明清理不是“越少越好”,而是让每个目录只承担一个清楚的职责:该删除的删除,该保留的保留,该改造的改造。
最终原则可以压缩成一句话:.cntr 应该是基础设施入口,不应该是自托管兴趣清单。