网站迁移服务器后图片无法加载怎么办?路径、权限与资源链接排查

这是服务器迁移后投诉率最高的故障:页面排版、文字、菜单都正常,唯独图片全是裂图,或者 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 实际路径
rootroot 路径 + 完整 URIlocation /img/ { root /data/www; }/data/www/img/a.jpg
aliasalias 路径 + 去掉 location 前缀后的部分location /img/ { alias /data/pics/; }/data/pics/a.jpg
root 写在 server 层继承给所有 locationserver { 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 会导致数据损坏)。两种安全做法:

  1. 专用工具:WordPress 用 wp-cli 的 wp search-replace 'http://old.com' 'https://new.com' --dry-run,它会自动处理序列化。
  2. 程序内置功能:很多 CMS 后台有“站点域名设置/附件域名”配置项,只改配置项最安全,不要动数据库。
  3. 万不得已才用 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

七、迁移后图片问题排查清单

  1. 浏览器 F12 → Network 看图片状态码,按第 1 节表格定性。
  2. 用 curl -I 单独请求图片地址,确认服务端实际返回。
  3. 看 tail -f /var/log/nginx/error.log,日志里的路径就是 Nginx 的真实查找路径。
  4. 按日志路径 ls -l,确认文件是否真的存在 → 不存在则补 rsync。
  5. 文件存在但 403 → 查属主权限 + SELinux 上下文。
  6. 确认 nginx -T 里的 root / alias 是否符合预期,注意 alias 尾部的 /。
  7. 查页面源码与数据库里是否残留老域名、老 IP、老绝对路径。
  8. 检查伪静态规则是否与老环境一致(尤其 Apache .htaccess 迁移到 Nginx 时)。
  9. 清理 CDN 缓存与浏览器缓存,必要时给静态资源加版本号(style.css?v=20241014)。
  10. 上传功能单独验证:用 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 迁移、数据库域名替换、伪静态规则。