PAYATHON 2026

Nginx 反向代理无法通过 Unix socket 连接 Puma

数据小陈

结论

这是两个不同的问题:

  1. 301 Location: https://appthing/ 并不是 Unix socket 连接失败。既然能收到 Puma 返回的 301,说明 Nginx 当时已经连上了上游。这个重定向通常由应用的 HTTPS 强制跳转逻辑触发,其中的 appthing 来自 Nginx 默认发送给上游的 Host。
  2. 502 Bad Gateway 和日志里的 (13: Permission denied) 才与 Unix socket 权限有关。即使 socket 本身的权限是 777,只要 Nginx worker 用户无法进入路径中的某一级目录,就无法连接 socket。

Nginx 的 upstream 配置本身没有明显问题:

upstream appthing {
    server unix:/home/deployer/myapp/shared/sockets/puma.sock fail_timeout=0;
}

需要检查的是目录访问权限、Nginx worker 的运行用户、安全策略,以及应用强制跳转到 HTTPS 的原因。

为什么 socket 是 777,Nginx 仍然报 Permission denied

访问 Unix socket 时,系统不只检查 socket 文件的权限。Nginx worker 还必须拥有路径中每一级目录的执行权限,也就是目录的 x 权限:

/home
/home/deployer
/home/deployer/myapp
/home/deployer/myapp/shared
/home/deployer/myapp/shared/sockets

例如,即使 puma.sock 显示为:

srwxrwxrwx deployer deployer puma.sock

但 /home/deployer 的权限是 0700,由 www-data 或 nginx 运行的 Nginx worker 仍然无法穿过该目录。

先确认 Nginx worker 使用哪个用户运行:

ps -eo user,pid,ppid,cmd | grep '[n]ginx'

也可以查看 nginx.conf 中的 user 指令:

user www-data;

不同发行版使用的用户可能是 www-data、nginx 或其他用户。

接着逐级检查目录权限:

namei -l /home/deployer/myapp/shared/sockets/puma.sock

还可以直接模拟 Nginx worker 用户访问这些路径:

sudo -u www-data test -x /home/deployer
sudo -u www-data test -x /home/deployer/myapp/shared/sockets
sudo -u www-data test -r /home/deployer/myapp/shared/sockets/puma.sock

请将 www-data 替换为实际的 Nginx worker 用户。

推荐的权限处理方式

不要长期将 socket 设置为 777。更合适的方式是让 Puma 和 Nginx 通过同一个组访问 socket。

例如,创建一个专用运行目录,并将其所有者和所属组设置为 deployer:www-data:

sudo install -d -o deployer -g www-data -m 0770 /run/myapp

让 Puma 在该目录中创建 socket:

unix:///run/myapp/puma.sock

同时通过服务配置确保 Puma 创建的 socket 可供组成员读写,例如设置:

umask 0007

最终权限可以是:

drwxrwx--- deployer www-data /run/myapp
srw-rw---- deployer www-data /run/myapp/puma.sock

然后修改 Nginx 配置:

upstream appthing {
    server unix:/run/myapp/puma.sock fail_timeout=0;
}

/run 下的文件通常会在系统重启后消失,因此应通过 systemd 的 RuntimeDirectory=、启动脚本或部署流程创建运行目录。

如果仍要把 socket 放在 /home/deployer 下,就必须确保 Nginx worker 对每一级父目录都有穿越权限。可以通过组权限或 ACL 处理,不建议直接把整个 home 目录改为全局可访问。

还需要检查 SELinux 或 AppArmor

如果传统 Unix 权限没有问题,但日志仍然出现 (13: Permission denied),还要检查强制访问控制策略。

在启用 SELinux 的系统上,可以查看近期的拒绝记录:

sudo ausearch -m AVC -ts recent

也可以检查 SELinux 状态:

getenforce

Ubuntu 等系统还可能受到 AppArmor 限制:

sudo journalctl -k | grep -i apparmor

不要直接永久关闭这些安全机制。应根据审计日志,为实际使用的 socket 路径配置对应策略,或者把 socket 移到 Nginx 已获准访问的运行目录。

为什么会重定向到 https://appthing/

使用以下配置时:

proxy_pass http://appthing;

如果没有显式设置 Host,Nginx 默认传给上游的 Host 通常取自 $proxy_host。这里的值就是 upstream 名称 appthing。

如果 Rails 应用、Rack 中间件或其他应用逻辑启用了 HTTPS 强制跳转,上游可能会据此生成:

Location: https://appthing/

proxy_redirect off; 不会关闭应用返回的重定向。它只决定 Nginx 是否改写上游响应中的 Location 和 Refresh 头。

可以补充完整的代理请求头配置:

upstream appthing {
    server unix:/run/myapp/puma.sock fail_timeout=0;
}

server {
    listen 8099;
    server_name 192.168.33.10;

    location / {
        proxy_pass http://appthing;

        proxy_set_header Host $http_host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;

        proxy_redirect off;
    }
}

修改后,先验证配置,再重新加载实际运行的 Nginx:

sudo nginx -t
sudo nginx -s reload

随后先不要使用 -L,直接查看第一跳响应:

curl -is http://192.168.33.10:8099/

如果响应仍然重定向到 HTTPS,就需要检查应用自身的配置,例如 Rails 的 config.force_ssl、Rack 中间件、主机规范化逻辑或自定义重定向代码。

如果外层使用 HTTPS,而 Nginx 到 Puma 之间使用 HTTP,应把客户端的真实协议传给应用。例如,在 HTTPS server 中设置:

proxy_set_header X-Forwarded-Proto $scheme;

如果当前入口本身就是普通 HTTP,$scheme 的值就是 http。不要为了绕过应用逻辑而无条件将其伪造成 https,除非外层 TLS 确实由其他可信代理终止。

如何直接测试 Puma 的 Unix socket

如果 curl 支持 --unix-socket,可以直接访问:

curl --unix-socket /home/deployer/myapp/shared/sockets/puma.sock \
     -i http://localhost/

测试时,URL 仍然要写成 HTTP URL。--unix-socket 指定连接方式,http://localhost/ 则用于构造 HTTP 请求,包括请求路径和 Host。

如果应用依赖特定的 Host,可以明确指定:

curl --unix-socket /home/deployer/myapp/shared/sockets/puma.sock \
     -i \
     -H 'Host: 192.168.33.10:8099' \
     -H 'X-Forwarded-Proto: http' \
     http://localhost/

要准确复现 Nginx 的权限环境,应使用 Nginx worker 用户执行测试:

sudo -u www-data curl \
    --unix-socket /home/deployer/myapp/shared/sockets/puma.sock \
    -i http://localhost/

可以根据结果判断问题所在:

  • 返回 200、301、302、404 或其他有效 HTTP 响应,说明 Puma 正在监听并处理请求。
  • 返回 Permission denied,说明 socket、父目录权限或安全策略有问题。
  • 返回 Connection refused,说明 socket 文件可能已经失效,当前没有进程监听。
  • 返回 No such file or directory,说明路径不正确,或者 Puma 没有创建 socket。
  • 连接成功后收到无效的 HTTP 响应,说明监听该 socket 的程序可能不支持 HTTP,或者用于测试的 socket server 没有正确实现 HTTP 协议。

较旧版本的 curl 可能不支持 --unix-socket,此时可以使用 socat:

printf 'GET / HTTP/1.1\r\nHost: localhost\r\nConnection: close\r\n\r\n' |
    socat - UNIX-CONNECT:/home/deployer/myapp/shared/sockets/puma.sock

部分 nc 实现也支持 Unix socket:

printf 'GET / HTTP/1.1\r\nHost: localhost\r\nConnection: close\r\n\r\n' |
    nc -U /home/deployer/myapp/shared/sockets/puma.sock

wget 通常没有与 curl --unix-socket 对应的通用选项,所以这里优先使用 curl 或 socat。

如何确认确实有进程监听该 socket

在 ps 输出中看到 Puma 和 socket 路径,只能说明 Puma 的启动参数看起来正确,不能完全证明该 socket 当前可以连接。

可以检查 Unix socket 的监听状态:

sudo ss -xlpn | grep puma.sock

某些系统也可以使用:

sudo lsof -U | grep puma.sock

最可靠的验证方式仍然是直接发送 HTTP 请求:

curl --unix-socket /home/deployer/myapp/shared/sockets/puma.sock \
     -i http://localhost/

排查时,先用 curl 直接测试 Puma,再以 Nginx worker 用户重复测试。如果后一次测试失败,就修复目录权限或安全策略。确认连接成功后,再处理应用返回的 HTTPS 重定向。

备注:内容仅供参考。