这是服务器迁移后投诉率最高的故障:页面排版、文字、菜单都正常,唯独图片全是裂图,或者 CSS/JS 加载不出来,页面变成一堆裸 HTML。更让人困惑的是,把图片地址单独复制到浏览器里能打开,但嵌在页面里就是不出来。也有的情况是:首页图片正常,后台新上传的图片却传不上去,报“目录不可写”。
这类问题本质上逃不出四类原因:文件没搬全、路径变了、权限/属主不对、页面里的资源链接还是老地址。本文按“从浏览器到磁盘”的顺序给出一套分层排查方法,每一层都有可复制的命令与明确的判断标准,并覆盖 Nginx 静态资源配置、SELinux、冷热迁移差异等常见坑。
一、先在浏览器里把问题定性(30 秒)
不要一上来就登服务器。打开浏览器开发者工具(F12)→ Network 面板,刷新页面,看图片请求的状态码。状态码直接决定了后面走哪条排查路径:
| 状态码 | 含义 | 对应原因 | 下一步 |
|---|---|---|---|
| 404 Not Found | 服务器上找不到这个文件 | 文件没拷贝全,或 Nginx root 指向错目录 | 查磁盘上文件是否存在、查 root 配置 |
| 403 Forbidden | 文件在,但 Nginx 无权读 | 属主/权限不对,或 SELinux 拦截,或 autoindex 未开且无 index | 查 ls -l、nginx 运行用户、getenforce |
| 500 / 502 | 动态脚本出错或未配 FastCGI | 图片走的是 PHP 动态输出(缩略图、水印) | 查 PHP-FPM 与 error_log |
| 空白 / failed / CORS error | 请求被浏览器拦或协议不匹配 | HTTPS 页面加载 HTTP 资源(混合内容),或跨域 | 查控制台 Mixed Content 提示 |
| (failed) net::ERR_NAME_NOT_RESOLVED | 域名解析不到 | 资源写死老域名、DNS 未生效 | 查页面源码里的绝对地址 |
| 旧图片(内容变了但图没变) | 命中缓存 | CDN / 浏览器缓存未刷新 | 刷新 CDN 缓存、加版本号 |
另外一个快速验证方法:直接 curl 那个图片地址,看响应头。
curl -I http://www.example.com/uploads/2024/01/demo.jpg
# 关注这几行
# HTTP/1.1 200 OK
# Content-Type: image/jpeg
# Content-Length: 102400
# 只看状态码,批量检查资源
for u in /uploads/a.jpg /static/style.css /static/app.js; do
echo -n "$u -> "; curl -s -o /dev/null -w "%{http_code}\n" http://www.example.com$u
done
二、第一类:文件根本没搬全(404)
迁移时最常见的是只搬了程序代码,漏了 uploads、attachments、data 这类附件目录——它们往往体积最大,用 git clone 或“只打包 wwwroot”的方式迁移时极易遗漏。
# 1. 新老机器对比文件数量与体积(在两台机器上分别执行) find /data/www/site -type f | wc -l du -sh /data/www/site du -sh /data/www/site/uploads # 2. 按目录看体量分布,快速发现“少了哪一块” du -sh /data/www/site/* | sort -hr | head -20 # 3. 在新机器上确认目标文件是否真的存在 ls -l /data/www/site/uploads/2024/01/demo.jpg find /data/www/site -name 'demo.jpg'
2.1 正确的迁移搬运命令
推荐用 rsync 而不是 scp/FTP:可断点续传、可保留权限属主、可先 --dry-run 预览。
# 先预览,不做任何改动(强烈建议第一步) rsync -avz --dry-run -e 'ssh -p 22' /data/www/site/ root@新机IP:/data/www/site/ # 正式同步:保留权限、属主、软链接、时间戳 rsync -avzHP --delete -e 'ssh -p 22' /data/www/site/ root@新机IP:/data/www/site/ # 大附件目录单独走一次,并限速避免打满带宽 rsync -avzHP --bwlimit=20480 -e 'ssh -p 22' /data/www/site/uploads/ root@新机IP:/data/www/site/uploads/
参数说明:-a 归档模式(保留权限/时间/软链)、-v 显示细节、-z 压缩传输、-H 保留硬链接、-P 显示进度并支持断点续传、--delete 让目标与源严格一致(首次使用务必先 dry-run,误用会删文件)。
三、第二类:Nginx 配置的 root / alias 指错了地方
文件在磁盘上,但 Nginx 找错了目录,同样返回 404。迁移后目录结构常常从 /var/www/html 变成 /data/www/site,配置没同步改就出事。
# 看站点实际用的 root 和错误日志路径 nginx -T | grep -nE 'root|alias|error_log|access_log|server_name' # 实时跟踪错误日志,看 Nginx 到底去哪里找文件 tail -f /var/log/nginx/error.log # 典型输出:open() "/data/www/site/uploads/a.jpg" failed (2: No such file or directory)
错误日志里的路径就是 Nginx 的真实查找路径,把它复制出来 ls 一下,立刻能判断是文件缺失还是 root 配错。
3.1 root 与 alias 的区别(高频踩坑点)
| 指令 | 拼接规则 | 示例 | 访问 /img/a.jpg 实际路径 |
|---|---|---|---|
| root | root 路径 + 完整 URI | location /img/ { root /data/www; } | /data/www/img/a.jpg |
| alias | alias 路径 + 去掉 location 前缀后的部分 | location /img/ { alias /data/pics/; } | /data/pics/a.jpg |
| root 写在 server 层 | 继承给所有 location | server { root /data/www/site; } | /data/www/site/img/a.jpg |
| alias 漏写尾部 / | 会把前缀当目录拼进去 | alias /data/pics; | /data/picsimg/a.jpg(错误) |
正确示例:
server {
listen 80;
server_name www.example.com;
root /data/www/site;
index index.php index.html;
access_log /var/log/nginx/www.access.log;
error_log /var/log/nginx/www.error.log;
# 静态资源:加缓存头、关闭日志,减少 IO
location ~* \.(jpg|jpeg|png|gif|webp|ico|svg)$ {
expires 30d;
add_header Cache-Control "public";
access_log off;
try_files $uri =404;
}
location ~* \.(css|js|woff2?|ttf)$ {
expires 7d;
access_log off;
}
# 附件目录独立存放时用 alias
location /uploads/ {
alias /data/attachments/;
expires 30d;
access_log off;
}
location ~ \.php$ {
fastcgi_pass unix:/run/php/php8.1-fpm.sock;
include fastcgi_params;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
}
}
四、第三类:权限与属主(403)
403 是迁移后第二高发的问题,尤其是从 Windows/宝塔 迁到 Linux,或者两台机器 Web 运行用户不同(www-data vs nginx vs apache)时。
# 1. 确认 Nginx / PHP-FPM 的运行用户
ps aux | grep -E 'nginx|php-fpm' | grep -v grep | awk '{print $1}' | sort -u
grep -nE '^\s*user' /etc/nginx/nginx.conf
grep -nE '^\s*(user|group)\s*=' /etc/php/8.1/fpm/pool.d/www.conf
# 2. 看文件与目录的属主权限
ls -ld /data/www /data/www/site /data/www/site/uploads
ls -l /data/www/site/uploads/demo.jpg
# 3. 沿路径逐层检查(每一级目录都必须有 x 执行权限才能进入)
namei -l /data/www/site/uploads/demo.jpg
4.1 标准权限修复
# 假设 Web 用户为 www-data
chown -R www-data:www-data /data/www/site
# 目录 755,文件 644(安全且够用)
find /data/www/site -type d -exec chmod 755 {} \;
find /data/www/site -type f -exec chmod 644 {} \;
# 需要写入的目录(上传、缓存、session)单独放宽
chmod -R 775 /data/www/site/uploads /data/www/site/runtime /data/www/site/cache
chown -R www-data:www-data /data/www/site/uploads
权限原则:目录最小 755、文件最小 644,只有必须写的目录才给 775,永远不要用 777。777 意味着任何本地用户都能改你的网页文件,是实打实的安全风险。
4.2 SELinux(CentOS / RHEL / AlmaLinux 专属坑)
权限全对但依然 403?八成是 SELinux。它拦的不是文件权限,而是安全上下文。
getenforce # 是否 Enforcing ls -Z /data/www/site | head # 看上下文 ausearch -m AVC -ts recent | head -20 # 查看被拒绝的记录 grep -i 'denied' /var/log/audit/audit.log | tail -20 # 修复:给目录打上 httpd 可读上下文 semanage fcontext -a -t httpd_sys_content_t '/data/www/site(/.*)?' restorecon -Rv /data/www/site # 上传目录需要可写 semanage fcontext -a -t httpd_sys_rw_content_t '/data/www/site/uploads(/.*)?' restorecon -Rv /data/www/site/uploads
不要图省事直接 setenforce 0 永久关闭 SELinux。正确做法是改上下文或布尔值(如对外访问网络用 setsebool -P httpd_can_network_connect 1)。
五、第四类:页面里的资源链接还是老地址
文件、权限、配置全对,图片还是不显示——去页面源码里看资源地址的域名。很多 CMS(织梦、PBootCMS、Discuz、WordPress)会把站点域名和附件域名写进数据库,迁移后 IP/域名变了,页面里输出的还是老地址。
# 1. 直接看页面输出的 HTML 源码里图片地址是什么
curl -s http://www.example.com/ | grep -oE 'src="[^"]+"' | head -20
curl -s http://www.example.com/ | grep -oE 'https?://[^"'"'"' ]+' | sort -u | head -30
# 2. 在数据库里找写死的老域名/老 IP
mysqldump -uroot -p --default-character-set=utf8mb4 dbname > /tmp/db.sql
grep -oE 'https?://[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}' /tmp/db.sql | sort | uniq -c | sort -rn | head
grep -oE 'https?://old-domain\.com' /tmp/db.sql | wc -l
# 3. 在程序文件里找写死的路径
grep -rn '/home/olduser/www' /data/www/site --include='*.php' | head
grep -rn 'old-domain.com' /data/www/site --include='*.php' --include='*.html' | head
5.1 安全的批量替换方法
数据库替换域名必须注意序列化字段(WordPress 的 wp_options、部分 CMS 的配置字段把字符串长度写进了数据里,直接 SQL REPLACE 会导致数据损坏)。两种安全做法:
- 专用工具:WordPress 用
wp-cli的wp search-replace 'http://old.com' 'https://new.com' --dry-run,它会自动处理序列化。 - 程序内置功能:很多 CMS 后台有“站点域名设置/附件域名”配置项,只改配置项最安全,不要动数据库。
- 万不得已才用 SQL:
UPDATE 表 SET 字段 = REPLACE(字段, '老地址', '新地址') WHERE 字段 LIKE '%老地址%';,且必须先备份、先小范围测试。
# 备份永远是第一步 mysqldump -uroot -p --single-transaction --routines --default-character-set=utf8mb4 dbname \ | gzip > /backup/db_$(date +%F).sql.gz # wp-cli 方式(推荐 WordPress) wp search-replace 'http://old.com' 'https://www.new.com' --dry-run --all-tables wp search-replace 'http://old.com' 'https://www.new.com' --all-tables
5.2 用 Nginx 做临时兜底
如果一时无法批量替换,可以用 subs_filter 模块在输出时替换,或用反向代理把老路径代理到新位置,作为过渡方案:
# 附件目录改名/换位置的临时兼容
location ~ ^/old-uploads/(.*)$ {
alias /data/www/site/uploads/$1;
}
# 老域名 301 到新域名(有利于 SEO)
server {
listen 80;
server_name old-domain.com www.old-domain.com;
return 301 https://www.new-domain.com$request_uri;
}
六、第五类:伪静态、大小写与编码问题
- 伪静态没配:迁移后 Nginx 缺少
try_files/rewrite规则,导致图片走动态路由时 404。PBootCMS、ThinkPHP、Laravel 都需要这一类规则。 - 大小写敏感:Linux 区分大小写,Windows 不区分。从 Windows 迁来的站点,
IMG_001.JPG与img_001.jpg是两个文件,页面里写错大小写就全部 404。 - 文件名含中文/空格:迁移时编码转换错误(GBK ↔ UTF-8)会让文件名变成乱码。用
ls | cat -A或convmv检查与转换。 - 软链接失效:
rsync带-a会保留软链,但如果目标目录不存在,链接会变成红色失效状态,用find -xtype l找出来。
# 典型伪静态规则(Laravel / ThinkPHP 类)
location / {
try_files $uri $uri/ /index.php?$query_string;
}
# 找失效软链接
find /data/www/site -xtype l
# 检查文件名编码
ls /data/www/site/uploads | cat -A | head
七、迁移后图片问题排查清单
- 浏览器 F12 → Network 看图片状态码,按第 1 节表格定性。
- 用
curl -I单独请求图片地址,确认服务端实际返回。 - 看
tail -f /var/log/nginx/error.log,日志里的路径就是 Nginx 的真实查找路径。 - 按日志路径
ls -l,确认文件是否真的存在 → 不存在则补 rsync。 - 文件存在但 403 → 查属主权限 + SELinux 上下文。
- 确认
nginx -T里的 root / alias 是否符合预期,注意 alias 尾部的/。 - 查页面源码与数据库里是否残留老域名、老 IP、老绝对路径。
- 检查伪静态规则是否与老环境一致(尤其 Apache
.htaccess迁移到 Nginx 时)。 - 清理 CDN 缓存与浏览器缓存,必要时给静态资源加版本号(
style.css?v=20241014)。 - 上传功能单独验证:用 Web 用户身份测试写文件(
sudo -u www-data touch /data/www/site/uploads/test.txt)。
小结
迁移后图片无法加载,看似琐碎,其实有清晰的分层:状态码定性 → 文件是否搬全 → Nginx root/alias 是否正确 → 权限属主与 SELinux → 页面与数据库里的资源链接 → 伪静态与编码。按这个顺序走,不需要靠运气。
避坑建议:
- 迁移一律用
rsync -avzHP,先--dry-run,慎用--delete。 - 迁移前对程序目录、附件目录、数据库各做一份备份,并留存一份
du -sh与find | wc -l的基线数据,迁移后逐项对比。 - 目录 755、文件 644,只有上传/缓存目录给写权限,杜绝 777。
- CentOS 系先查 SELinux 再怀疑权限,
ausearch -m AVC能直接给出答案。 - 域名替换优先用 CMS 后台配置项或
wp-cli,避免 SQL 直接 REPLACE 破坏序列化数据。 - 老域名必须做 301 跳转到新域名,否则搜索引擎会认为站点换了,历史权重流失。
- 从 Windows 迁到 Linux 时,务必排查文件名大小写与中文编码。
关键词回顾:网站迁移、图片无法加载、Nginx root/alias、403 权限、SELinux 上下文、rsync 迁移、数据库域名替换、伪静态规则。
企业QQ咨询




