在 NAS 上用 Coolify 部署自建服务:十条约定
在 NAS 上用 Coolify 部署十几个服务之后沉淀下来的清单,每条都对应一次「为什么起不来」的排查。
其中一半是同一种毛病:写错了不报错,只是悄悄不生效——deploy.resources 在非 swarm 下白写,CIDR 多写一位少覆盖一大片,探针只探到进程还活着。这类问题不会在部署时拦住你,等到出事才发现防线一直是空的。
1. Coolify 面板不挂域名,只走 IP
Coolify 不在流量链路上——它只负责起停 NAS 上的容器,停掉面板网站照常跑。
所以别给它挂域名。一旦走域名,它就依赖 DNS、证书、网关,而这三样恰恰是出故障时你最需要面板去修的(网关那条链路的完整排查顺序另写了一篇)。绑在 http://192.168.1.10:8000 上,断网、证书过期、网关崩了都还能进去救。
2. 服务名才是网络别名
部署完发现容器被改名了:
tinyauth-a1b2c3d4e5f6g7h8但别慌,http://tinyauth:3000 仍然能用。Coolify 加后缀的同时,把 compose 里的服务名注册成了网络别名:
docker inspect <容器> --format '{{range $k,$v := .NetworkSettings.Networks}}{{$v.Aliases}}{{end}}'# [tinyauth-a1b2c3d4e5f6g7h8 tinyauth]别名来自 services: 下面那个 key,不是 container_name。 容器名随便改,服务名必须和引用它的名字一致。
services: redis: # ← 别名来源 container_name: myapp-redis # 随便3. 配置文件用启动脚本生成
compose 的 content: 写法(把文件内容内联进 compose)可能被 Coolify 吃掉,只剩一个指向不存在文件的 bind 挂载。
Docker 遇到不存在的挂载源会创建一个空目录挂进去,程序读不到配置直接退出——而报错通常指向完全另一个方向。
可靠的做法:
entrypoint: [/bin/sh, -c] command: - | set -e cat > /tmp/app.toml << EOF token = "$$TOKEN" EOF exec app -c /tmp/app.toml$$ 是 compose 转义,实际执行为单个 $。
例外:某些应用会回写配置文件(登录后把凭证存回 .env)。这种必须挂真实的可写文件,环境变量替代不了。
4. 健康检查有三种假失败
探针只查进程存在
test: ["CMD", "cloudflared", "--version"] # 隧道断了照样 healthy自查标准:服务真不可用时,这个探针会失败吗?
镜像里没有 wget/curl
alpine 有 wget 没 curl,slim 可能都没有。按基础镜像选:Python 用 python -c "import urllib.request...",Node 用 node -e,不确定就两个都写用 || 串起来。
探针路径不存在
日志里满屏 GET /health 404 就是这种。挨个试一遍:
docker exec <容器> sh -c 'for p in / /docs /openapi.json /healthz; do echo -n "$p => "; wget -qO- -S "http://127.0.0.1:8000$p" 2>&1 | grep HTTP | tail -1; done'全 404 就用 TCP 探测,端口在听即算健康:
test: ["CMD-SHELL", "python -c \"import socket;socket.create_connection(('127.0.0.1',8000),5)\" || exit 1"]这不是小事:一个写错的探针会让 depends_on 它的容器全部卡在 Created 起不来。
5. depends_on 只给网络调用加
| 关系 | 加吗 |
|---|---|
| A 用 HTTP 调 B / 连 B 的数据库 | 加,且要 condition: service_healthy |
| A 和 B 共享一个目录 | 不加 |
踩过的坑:处理器读下载器的输出目录,加了依赖之后,下载器因上游接口变动而 unhealthy,整条流水线被拖住——而处理器本可以处理目录里已有的文件。
另外 depends_on 只管首次启动顺序,不管运行期。
6. 四个 compose 写法
volumes: - ./data:/data # 相对路径,不用命名卷 ports: - 192.168.1.10:5000:5000 # 绑内网 IP,不占其他网卡也不误暴露 logging: # 网关开了 access log 不限制能吃几十 G driver: json-file options: { max-size: 10m, max-file: "3" } pull_policy: always # latest tag 的自建镜像,不加会一直用缓存 mem_limit: 4g # 不是 deploy.resources——非 swarm 下那个不生效 cpu_shares: 1024deploy.resources 写了不生效比不写更糟:你以为限住了,实际没有。
端口绑 IP 的代价是机器 IP 变了容器起不来,确认路由器上给 NAS 做了静态 IP 绑定。反过来的坑也踩过:改成 8080:8080 直接报 address already in use,因为宿主机已有服务占着 8080,只是绑在别的地址上。
7. 关键服务留一条抢救通道
走网关的服务额外开个端口映射。域名、证书、网关任何一个出问题时,还能 http://192.168.1.10:5555 直接进去。只给排障时真需要进的服务留。
8. 代理:先分清是翻墙还是换 IP
- HTTP_PROXY=http://192.168.1.10:10809 - http_proxy=http://192.168.1.10:10809 # 部分库只认小写 - NO_PROXY=localhost,127.0.0.1,::1,172.16.0.0/12,192.168.0.0/16,.douyin.com,.qq.com三条规则:
- 不用就删行,别留空。
HTTP_PROXY=http://${空变量}变成http://,所有出站直接失败 - NO_PROXY 必须含
172.16/12,否则容器访问同网段容器会绕到境外再回来 - 抓国内平台的服务要排除对应域名,走境外出口只会触发风控
最后一条反过来也成立:有些工具文档写「建议配代理池分散 IP」,但它抓的是国内平台,你手上只有境外节点——配了比不配更糟。出口节点本身怎么搭,见单节点 REALITY 的部署步骤。
9. CIDR 写错不会报错
- TRUSTED_PROXIES=192.168.0.0/12 # 实际覆盖 192.160~192.175,错的 - TRUSTED_PROXIES=192.168.0.0/16,172.16.0.0/12 # 对的别忘 172.16/12——反代从容器网络过来。
写错不会报错,只会让功能悄悄失效:所有请求的来源 IP 都被记成网关的 IP,限流和审计全废。
10. 一条流水线放一个项目
几个服务共享目录时,分开部署要用绝对路径,而 Coolify 分配的路径带随机 uuid,部署完才知道,还有顺序依赖。
合并成一个 compose 用相对路径最简单:
downloader: volumes: [./shared:/app/output] processor: volumes: [./shared:/input:rw]代价是绑在一起重启,对本来就串行的流水线可以忽略。
排错顺序
docker ps -a --format '{{.Names}}\t{{.Status}}' # 1. 谁没起来docker logs <容器> --tail 50 # 2. 看日志docker inspect <容器> --format '{{json .State.Health}}' # 3. unhealthy 看探针输出第四步最有用:状态是 Created 而不是 Exited,说明它压根没被启动,问题在它 depends_on 的那个服务上。查它自己的日志是查不出东西的——它还没跑过。
文章分享
如果这篇文章对你有帮助,欢迎分享给更多人!


