在 NAS 上用 Coolify 部署自建服务:十条约定

1520 字
8 分钟
在 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 里的服务名注册成了网络别名:

Terminal window
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 就是这种。挨个试一遍:

Terminal window
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: 1024

deploy.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]

代价是绑在一起重启,对本来就串行的流水线可以忽略。

排错顺序#

Terminal window
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 的那个服务上。查它自己的日志是查不出东西的——它还没跑过。

文章分享

如果这篇文章对你有帮助,欢迎分享给更多人!

在 NAS 上用 Coolify 部署自建服务:十条约定
https://blog.always1ov.com/posts/coolify-nas-deploy-conventions/
作者
薯饼
发布于
2026-09-15
许可协议
CC BY-NC-SA 4.0