Nginx 反向代理无法通过 Unix socket 连接 Puma
结论
这是两个不同的问题:
301 Location: https://appthing/并不是 Unix socket 连接失败。既然能收到 Puma 返回的301,说明 Nginx 当时已经连上了上游。这个重定向通常由应用的 HTTPS 强制跳转逻辑触发,其中的appthing来自 Nginx 默认发送给上游的Host。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 重定向。
备注:内容仅供参考。
