Skip to content

从域名定位到 Docker:找到 Linux 上实际运行的 Node 服务

假设现在只有一台服务器的 SSH 权限和一个域名 www.example.com。不知道 Nginx 配置文件在哪里,不知道 Node 监听哪个端口,也不知道服务是否运行在 Docker 中。定位顺序应固定为:域名对应的 Nginx server -> Nginx 上游 -> 监听端口 -> 主机进程或 Docker 容器 -> 工作目录和挂载目录。

不要先搜索 / 下的所有配置文件,也不要先猜端口或容器名。历史配置、备份文件和停止的容器都会干扰判断;每一步都应由上一步的实际输出决定。

WARNING

本文所有“示意输出”仅说明命令的常见输出格式和下一步判断方法,不是任何目标服务器的真实日志。实际 PID、时间、路径、镜像名和端口必须以当前服务器输出为准。

先确定请求链路

text
www.example.com
  -> Nginx 生效的 server 块
  -> proxy_pass 或 upstream
  -> 本机端口、Unix Socket 或远程地址
  -> Node 进程,或 Docker 发布端口
  -> 容器状态、启动命令、工作目录和挂载目录

本文以 Nginx 最终指向本机 127.0.0.1:3000 为主线。若配置指向 Unix Socket 或另一台主机,后文也给出分支处理方式。

第一步:用域名找到生效的 Nginx 配置

nginx -T 会检查并输出主配置及所有 include 展开的配置,因此它反映的是 Nginx 实际加载的内容。

bash
sudo nginx -T 2>&1 \
  | grep -nE -C 8 'server_name[[:space:]].*www\.example\.com'

示意输出:

text
184:# configuration file /etc/nginx/conf.d/www.example.com.conf:
185:server {
186:    listen 443 ssl http2;
187:    server_name www.example.com;
188:
189:    location / {
190:        proxy_pass http://127.0.0.1:3000;
191:    }
192:}

输出中的 # configuration file 给出实际加载的文件;proxy_pass 给出下一个定位对象。这里的上游是本机 3000 端口。

如果 proxy_pass 使用上游名称,例如 proxy_pass http://web_backend,继续定位该上游组:

bash
sudo nginx -T 2>&1 \
  | grep -nE -C 5 'upstream[[:space:]]+web_backend'

示意输出:

text
42:upstream web_backend {
43:    server 127.0.0.1:3000;
44:}

此时同样得到 3000,再进入端口定位步骤。若这里出现多个 server,应根据权重、备份节点和健康检查配置继续判断,不能任选一个端口。

如果配置是 Unix Socket:

nginx
proxy_pass http://unix:/run/web/app.sock;

这不是 TCP 端口,直接跳到 Unix Socket 分支

第二步:通过端口判断服务运行在主机还是 Docker

将上一步得到的端口写入变量:

bash
port=3000
sudo ss -ltnp "sport = :$port"

示意输出:

text
State  Recv-Q Send-Q Local Address:Port Peer Address:Port Process
LISTEN 0      4096   127.0.0.1:3000      0.0.0.0:*     users:(("docker-proxy",pid=2814,fd=4))

重点查看输出中的进程名和 PID:

监听者说明下一步
nodeNode 直接运行在主机进入 主机 Node 分支
docker-proxyDocker 将宿主机端口发布给容器进入 Docker 分支
没有输出服务未监听该端口,或 Nginx 并未转发到本机 TCP 端口回到 Nginx 配置检查上游类型

docker-proxy 只是端口转发进程,不是应用本身。不能从它的 PID 推断 Node 项目目录,必须继续定位容器。

主机 Node 分支

若上一步的 ss 输出显示 node,假设其 PID 为 1234,先确认它实际以什么命令运行:

bash
pid=1234
sudo ps -o pid,ppid,user,lstart,cmd -p "$pid"

示意输出:

text
  PID  PPID USER  STARTED                      CMD
 1234     1 deploy Mon Jul 24 09:18:42 2026     /usr/bin/node dist/server.js

查看进程当前工作目录:

bash
sudo readlink -f "/proc/$pid/cwd"

示意输出:

text
/srv/web-api/current

查看 Node 入口文件和启动参数:

bash
sudo cat "/proc/$pid/cmdline" | tr '\0' '\n'

示意输出:

text
/usr/bin/node
dist/server.js

三个命令分别提供启动命令、进程当前工作目录和 Node 入口文件。/proc/<PID>/cwd 是运行时目录;入口脚本可能位于其下的 dist/build/ 目录。

再检查进程是否由 systemd 托管:

bash
sudo cat "/proc/$pid/cgroup"

示意输出:

text
0::/system.slice/web-api.service

若输出包含 web-api.service,读取服务定义中的工作目录与启动命令:

bash
sudo systemctl show web-api.service \
  -p MainPID \
  -p WorkingDirectory \
  -p ExecStart \
  -p FragmentPath

示意输出:

text
ExecStart={ path=/usr/bin/node ; argv[]=/usr/bin/node dist/server.js ; ignore_errors=no ; start_time=[n/a] ; stop_time=[n/a] ; pid=0 ; code=(null) ; status=0/0 }
FragmentPath=/etc/systemd/system/web-api.service
MainPID=1234
WorkingDirectory=/srv/web-api/current

查看主 Unit 与 drop-in 覆盖文件:

bash
sudo systemctl cat web-api.service

示意输出:

ini
# /etc/systemd/system/web-api.service
[Service]
User=deploy
WorkingDirectory=/srv/web-api/current
ExecStart=/usr/bin/node dist/server.js
Restart=always

WorkingDirectoryExecStart/proc/<PID>/cwd 应一起判断。它们不一致时,前者是部署声明,后者是进程当前实际目录。

Docker 分支

先按发布端口查找容器,而不是按猜测的容器名搜索:

bash
port=3000
docker ps --filter "publish=$port" \
  --format 'table {{.ID}}\t{{.Names}}\t{{.Image}}\t{{.Ports}}'

示意输出:

text
CONTAINER ID   NAMES     IMAGE                  PORTS
a1b2c3d4e5f6   web-api   registry/web-api:1.4   127.0.0.1:3000->3000/tcp

得到容器名称后,后续命令使用变量:

bash
container=web-api

示意输出:

text
# 变量已设置;后续命令将操作 web-api 容器。

查看容器是否正在运行

bash
docker inspect "$container" \
  --format '状态={{.State.Status}} PID={{.State.Pid}} StartedAt={{.State.StartedAt}} RestartCount={{.RestartCount}}'

示意输出:

text
状态=running PID=8214 StartedAt=2026-07-24T01:20:11.582Z RestartCount=0

再查看 Docker 展示的状态和端口映射:

bash
docker ps --filter "name=^/${container}$" \
  --format 'table {{.Names}}\t{{.Status}}\t{{.Ports}}'

示意输出:

text
NAMES     STATUS          PORTS
web-api   Up 2 hours      127.0.0.1:3000->3000/tcp

状态为 runningUp 才表示容器正在运行。RestartCount 持续增加时,服务可能反复退出;先查看最近日志,而不是立刻重启容器:

bash
docker logs --tail 100 "$container"

示意输出:

text
2026-07-24T09:20:15.019Z info: server listening on 0.0.0.0:3000
2026-07-24T09:23:41.654Z info: GET /health 200 3ms

需要持续观察时再使用下面的命令,按 Ctrl+C 退出日志跟随,不会停止容器:

bash
docker logs --tail 100 --follow "$container"

示意输出:

text
2026-07-24T09:25:08.201Z info: GET /api/users 200 12ms
2026-07-24T09:25:11.887Z info: GET /health 200 2ms

查看容器启动的应用

先读取镜像配置声明的工作目录:

bash
docker inspect "$container" --format 'WorkingDir={{.Config.WorkingDir}}'

示意输出:

text
WorkingDir=/app

读取入口程序和命令参数:

bash
docker inspect "$container" --format 'Entrypoint={{json .Config.Entrypoint}} Cmd={{json .Config.Cmd}}'

示意输出:

text
Entrypoint=["node"] Cmd=["dist/server.js"]

再从宿主机查看容器进程。即使镜像没有安装 ps,这个命令仍可用于观察容器内 PID:

bash
docker top "$container" -eo pid,ppid,user,etime,args

示意输出:

text
PID     PPID    USER  ELAPSED  COMMAND
8214    8190    node  02:04:31 node dist/server.js

如果启动命令中有 node dist/server.js,它只说明容器内的入口文件。要确定宿主机上的代码或配置位置,需要继续看挂载。

找到宿主机挂载目录

bash
docker inspect "$container" \
  --format '{{range .Mounts}}{{.Type}}{{"\t"}}{{.Source}}{{"\t->\t"}}{{.Destination}}{{println}}{{end}}'

示意输出:

text
bind    /srv/web-api/current    ->      /app
volume  /var/lib/docker/volumes/web-api-data/_data  ->      /app/data

输出应按下面方式解释:

TypeSource 的含义是否通常是宿主机项目目录
bind宿主机实际路径可能是代码、配置或日志目录,需结合 Destination 判断
volumeDocker 管理的卷路径或卷名通常是数据目录,不应直接当作代码发布目录
tmpfs内存文件系统不是持久化部署目录

本例中 bind /srv/web-api/current -> /app,且 WorkingDir=/app,因此 /srv/web-api/current 是最有力的宿主机部署目录证据。

进入容器确认运行时目录

优先使用 sh,因为许多精简镜像没有 bash

bash
docker exec -it "$container" sh

示意输出:

text
/app #

进入后依次执行:

sh
pwd
readlink -f /proc/1/cwd
tr '\0' '\n' </proc/1/cmdline

示意输出:

text
/app
/app
node
dist/server.js

也可以不进入交互终端,直接读取 PID 1 的目录与启动参数:

bash
docker exec "$container" sh -c 'readlink -f /proc/1/cwd; tr "\0" "\n" </proc/1/cmdline'

示意输出:

text
/app
node
dist/server.js

如果镜像是 distroless,可能没有 sh。此时 docker exec 的示意错误形式如下:

text
OCI runtime exec failed: exec: "sh": executable file not found in $PATH

使用 docker inspectdocker top 和挂载信息完成定位,不要为了进入容器而修改镜像或安装工具。

Unix Socket 分支

当 Nginx 指向 /run/web/app.sock 时,检查 Socket 的监听者:

bash
sudo ss -lxnp | grep '/run/web/app.sock'

示意输出:

text
u_str LISTEN 0 511 /run/web/app.sock 13846 * 0 users:(("node",pid=1234,fd=21))

若监听者是 Node,按 主机 Node 分支 使用 PID 检查工作目录。若 Socket 由容器通过 bind mount 提供,ss 的结果和 Docker 容器信息需要结合 docker inspectMounts 判断。

收束:每一步要确认的事实

text
域名匹配哪个 server_name?
  -> Nginx 转发到哪个上游?
  -> 上游是本机端口、Unix Socket,还是远程地址?
  -> 本机端口由 node 还是 docker-proxy 监听?
  -> 容器实际是否 running,入口命令是什么?
  -> bind mount 是否把宿主机目录挂到应用工作目录?

只有这条证据链完整时,才能把某个目录称为“正在运行的服务部署目录”。单独看到 package.json、旧容器、停止容器或 Nginx 历史配置,都不足以证明它正在接收 www.example.com 的请求。

参考资料