自建服务 .cntr Compose 清理复盘

TLDR

这轮清理的核心结论是:.cntr 不应该继续承担”自托管服务收藏夹”的角色,它应该只保存仍有运行价值、边界清楚、可以复现的 Compose service。

服务可以删,知识点要留下。所以这份文档仍然保留了大量原始 YAML、旧笔记和逐项判断;它不是一篇压缩后的选型文章,而是一份带复盘结论的清理记录。

本轮处理里,大部分 service 被删除;golang 没删,而是从历史全家桶改造成共享 Go 微服务依赖栈。这也是本次复盘里最重要的反例:旧东西不一定都要删,有些应该重新定义职责。

背景

.cntr 最初是方便本地或 VPS 快速启动服务的 Compose 集合。问题是,随着实验过的工具越来越多,它逐渐从”现役服务目录”变成了”自托管服务收藏夹”:监控、面板、穿透、通知、自动化、AI image、RSS、网盘、开发依赖都堆在一起。

这种目录最容易产生两类负担。第一类是运行负担:即使服务没有启动,配置里仍然可能保留端口、默认密码、token 占位、数据卷和反代假设。第二类是认知负担:每次看到一个 service,都需要重新判断它到底是正在使用、曾经使用、未来可能使用,还是只是一个 recipe。

所以这轮清理不是单纯删文件,而是把 .cntr 重新收敛成更接近基础设施事实的目录:保留运行入口,删除兴趣清单,捞回知识点。

判断标准

这轮判断主要按下面几条规则走:

  • 是否仍在现役链路里。如果没有调用方、反代、备份、告警或明确入口,就不能因为”也许以后有用”继续保留。
  • 是否只是 recipe。包含 <YOUR_TOKEN>passwordlocalhostAdd 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/tcp53/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 里的 passwordlocalhost 也是典型试用配置,不应该保留成可部署服务。

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.nixpi-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_PTRACESYS_ADMINapparmor: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:GIDTZ=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/inspectdocker 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 是为了避免和 devenvdevbox 这些成熟工具产生命名歧义;这里不做 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 按需启动 dbcachemstraceanalytics,避免默认 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 应该是基础设施入口,不应该是自托管兴趣清单。