使用 Docker 迁移 Wordpress 网站

2025-10-18 19:44:48

一、起因

公司官网一直部署在第三方平台,近期遭遇黑客攻击,官网显示乱码。

于是决定迁移到自己的云服务器上,整个流程比较繁琐,本文记录过程,以备后用。

二、准备工作

从第三方平台下载 wordpress 源码和数据库备份;

看了一下源码,乱码主要存在哪个文件这会给整忘记了,当时是找到的,直接把乱码部分删掉就好了。

还发现一些恶意文件,主要在 wp-content/plugins 目录下,有很多以字符串.开头的二进制文件藏在代码里,恶意文件的名称为数字和小写字母组成的12位随机字符串,格式如 .vqef7iifvhfq,这玩意是挖矿木马,会占用机器的进程和资源。

写一个脚本全部删掉。

# find_dot_files.py
import os
import sys
import stat

def find_hidden_files(directory):
    """递归查找目录下所有以点号开头的隐藏文件"""
    hidden_files = []
    for root, dirs, files in os.walk(directory):
        for file in files:
            if file.startswith('.') and file not in [ '.DS_Store', '.gitignore', '.gitkeep', '.htaccess', '.ibase_pconnection' ]:
                full_path = os.path.join(root, file)
                hidden_files.append(full_path)
    return hidden_files

def save_to_txt(file_list, output_file='tmp.txt'):
    """将文件列表保存到文本文件"""
    with open(output_file, 'w') as f:
        for file_path in file_list:
            f.write(file_path + '\n')

def delete_files(file_paths):
    """
    批量删除指定文件路径列表中的所有文件
    
    :param file_paths: 文件路径列表,例如 ['/path/to/file1.txt', '/path/to/file2.log']
    :return: 包含删除结果的字典,格式为 {文件路径: 删除状态}
    """
    results = {}
    
    for file_path in file_paths:
        try:
            # 检查文件是否存在
            if not os.path.exists(file_path):
                results[file_path] = "文件不存在"
                continue
                
            # 检查是否为文件(非目录)
            if not os.path.isfile(file_path):
                results[file_path] = "路径指向目录而非文件"
                continue
                
            # 处理只读文件:修改权限为可写
            if not os.access(file_path, os.W_OK):
                os.chmod(file_path, stat.S_IWRITE)
                
            # 执行文件删除
            os.remove(file_path)
            
            # 二次验证是否删除成功
            if not os.path.exists(file_path):
                results[file_path] = "删除成功"
            else:
                results[file_path] = "文件仍存在(未知错误)"
                
        except PermissionError:
            results[file_path] = "权限不足(即使已尝试修改权限)"
        except FileNotFoundError:
            results[file_path] = "文件在删除过程中消失"
        except IsADirectoryError:
            results[file_path] = "路径指向目录(应使用shutil.rmtree)"
        except Exception as e:
            results[file_path] = f"未知错误: {str(e)}"
            
    return results

if __name__ == "__main__":
    # 获取命令行参数或使用当前目录
    target_dir = sys.argv[1] if len(sys.argv) > 1 else os.getcwd()
    
    if not os.path.isdir(target_dir):
        print(f"错误:{target_dir} 不是有效目录")
        sys.exit(1)
    
    # 查找并保存隐藏文件
    hidden_files = find_hidden_files(target_dir)
    print(f"找到 {len(hidden_files)} 个隐藏文件")

    delete_files(hidden_files)
    print(f"找到 {len(hidden_files)} 个隐藏文件,已删除")

运行命令自动删除,这里只递归删除 plugins 目录下的恶意文件。

$ python3 find_dot_files.py /Users/dkvirus/wordpress/wp-content/plugins

三、迁移步骤

3.1 本地测试

创建一个目录结构如下:

|-- wordpress-docker
    |-- wordpress               # wordpress 源码
    |-- init.sql                # 数据库备份
    |-- docker-compose.yml      # docker-compose 配置文件
    |-- Dockerfile              # dockerfile
    |-- nginx.conf              # nginx 配置文件

wordpress 和 init.sql 就是上一步准备的源码和数据库备份。

nginx.conf 文件内容如下:
(啥也不用改)

server {
    listen 80;
    server_name localhost;
    root /var/www/html;
    index index.php index.html index.htm;

    location / {
        try_files $uri $uri/ /index.php?$args;
    }

    location ~ \.php$ {
        fastcgi_split_path_info ^(.+\.php)(/.+)$;
        fastcgi_pass wordpress-php:9000;  # 指向PHP-FPM容器
        fastcgi_index index.php;
        include fastcgi_params;
        fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
        fastcgi_param PATH_INFO $fastcgi_path_info;
    }

    location ~ /\.ht {
        deny all;
    }
}

Dockerfile 文件内容如下:
(啥也不用改)

FROM php:8.2-fpm
RUN docker-php-ext-install pdo_mysql mysqli

docker-compose.yml 文件内容如下:
<root_password><database_name><username><password> 需要自行修改)

注意:database_name 数据库名称需要和 init.sql 里面创建的数据库名称保持一致。

services:
  # 数据库服务
  mysql:
    image: mysql:8.0.0  # MySQL 8.0 官方镜像通常支持多架构
    container_name: wordpress-mysql
    restart: always
    healthcheck:
      test: ["CMD", "mysqladmin", "ping", "-h", "localhost", "-u", "root", "-p$$MYSQL_ROOT_PASSWORD"]
      interval: 5s
      timeout: 3s
      retries: 10
    environment:
      MYSQL_ROOT_PASSWORD: <root_password>  # 数据库 root 密码
      MYSQL_DATABASE: <database_name>       # 数据库名称
      MYSQL_USER: <username>                # 数据库管理员用户名(非 root 用户)
      MYSQL_PASSWORD: <password>            # 数据库管理员密码(非 root 用户)
    volumes:
      - mysql_data:/var/lib/mysql                          # 数据持久化
      - ./init.sql:/docker-entrypoint-initdb.d/init.sql    # 初始化 SQL 脚本
    ports:
      - "3306:3306"
    networks:
      - wordpress-network

  # PHP 处理服务
  php:
    build: .                                               # 使用 Dockerfile 构建镜像
    container_name: wordpress-php
    restart: always
    volumes:
      - ./wordpress:/var/www/html                          # 挂载 WordPress 源码
    depends_on:
      - mysql
    networks:
      - wordpress-network

  # Web 服务器
  nginx:
    image: nginx:alpine                                    # 轻量级Nginx镜像
    container_name: wordpress-nginx
    restart: always
    ports:
      - "80:80"                                            # 将本地 80 端口映射到容器
    volumes:
      - ./wordpress:/var/www/html                          # 静态文件和 PHP 代码
      - ./nginx.conf:/etc/nginx/conf.d/default.conf:ro     # Nginx配置
    depends_on:
      - php
    networks:
      - wordpress-network

# 定义网络和数据卷
networks:
  wordpress-network:
    driver: bridge

volumes:
  mysql_data:
    driver: local

运行命令启动服务:

# 构建 PHP 镜像
$ docker-compose build php
# 启动所有服务
$ docker-compose up -d

上面的步骤执行完,打开浏览器访问 http://localhost,理论上可以看到 wordpress 网站。

docker-compose 常用命令补充如下:

# 停止容器的同时,会移除为这些服务创建的容器和默认网络,但会保留数据卷
docker-compose down
# 删除在 docker-compose.yml 文件中定义的所有数据卷,其中的数据将无法恢复
docker-compose down -v
# 暂时停止服务,比如为了节省资源,并且计划稍后快速恢复
docker-compose stop   
# 启动已停止的服务  
docker-compose start  
# 重启服务
docker-compose restart
# 构建镜像
$ docker-compose build php
# 启动所有服务
docker-compose up -d

可能问题一:访问地址自动重定向

访问 http://localhost,浏览器地址栏会自动跳转另一个网址 https://your_domain.com

解决方法:

进入数据库容器
(用 docker-compose.yml 中定义的数据库用户名和密码替换下面的 <username><password>

$ docker exec -it wordpress-mysql mysql -u <username> -p<password>

查看表字段,可以看到 siteurl 和 home 两个字段是你的域名访问地址。
(用 docker-compose.yml 中定义的数据库名称替换下面的 <database_name>

> show databases;       # 查看所有数据库
> use <database_name>;  # 用 docker-compose.yml 中使用的数据库 <database_name>
> SELECT option_name, option_value FROM <database_name>.wp_options WHERE option_name IN ('siteurl', 'home');    # 查看 wp_options 表中 siteurl 和 home 两个字段的值
+-------------+------------------+
| option_name | option_value     |
+-------------+------------------+
| home        | https://your_domain.com |
| siteurl     | https://your_domain.com |
+-------------+------------------+
2 rows in set (0.02 sec)

使用如下命令将这两个字段值修改为 http://localhost 即可。
(用 docker-compose.yml 中定义的数据库名称替换下面的 <database_name>

> UPDATE <database_name>.wp_options 
SET option_value = 'http://localhost' 
WHERE option_name IN ('siteurl', 'home');

可能问题二:访问 http 自动变成 https

访问 http://localhost,自动变成了 https://localhost

原因:wordpress 使用了 wp force ssl 插件。

解决方法:/wp-content/plugins/ 目录,找到对应的SSL插件文件夹并将其重命名(例如,在文件夹名后加 -old)

验证: curl -I http://localhost # 应返回 HTTP 200,非 301/302

可能问题三:访问返回 HTTP/1.1 403 Forbidden

原因:Nginx 没有权限访问 wordpress 源码目录。

解决:修改文件访问权限。

$ chmod -R 755 ./wordpress-docker

3.2 服务器上部署

在本地测试没问题,就可以弄到服务器上部署,并且通过域名进行访问,以下所有操作都是在远程 Linux 服务器上进行的。

新机器如果没有安装 Docker,可以参考《Linux 安装 Docker》 进行安装。

提前将 wordpress 源码和数据库备份复制到服务器上。

创建一个目录结构如下:

|-- wordpress-docker
    |-- wordpress               # wordpress 源码
    |-- init.sql                # 数据库备份
    |-- docker-compose.yml      # docker-compose 配置文件
    |-- Dockerfile              # dockerfile
    |-- nginx.conf              # nginx 配置文件
    |-- certs
        |-- website.crt            # 证书
        |-- website.key            # 私钥

wordpress 和 init.sql 就是上一步准备的源码和数据库备份。

certs 目录存放证书,需要 https,里面存放证书和私钥两个文件。

编辑 nginx.conf 文件,内容如下:
(需要将 <your_domain.com> 替换为你的域名)

server {
    listen 80;
    server_name <your_domain.com>;  # 替换为你的域名
    return 301 https://$host$request_uri;  # 访问 http 跳转到 https
}

server {
    listen 443 ssl;
    server_name <your_domain.com>;  # 替换为你的域名

    # SSL证书配置
    ssl_certificate /etc/nginx/certs/website.crt;  # 证书路径(容器内)
    ssl_certificate_key /etc/nginx/certs/website.key;  # 私钥路径(容器内)
    ssl_session_timeout  5m;    # 缓存有效期
    ssl_ciphers ECDHE-RSA-AES128-GCM-SHA256:ECDHE:ECDH:AES:HIGH:!NULL:!aNULL:!MD5:!ADH:!RC4;    # 加密算法
    ssl_protocols TLSv1 TLSv1.1 TLSv1.2;    # 安全链接可选的加密协议
    ssl_prefer_server_ciphers on;   # 使用服务器端的首选算法

    root /var/www/html;
    index index.php index.html index.htm;

    location / {
        try_files $uri $uri/ /index.php?$args;
    }

    location ~ \.php$ {
        fastcgi_split_path_info ^(.+\.php)(/.+)$;
        fastcgi_pass wordpress-php:9000;  # 指向PHP-FPM容器
        fastcgi_index index.php;
        include fastcgi_params;
        fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
        fastcgi_param PATH_INFO $fastcgi_path_info;
    }

    location ~ /\.ht {
        deny all;
    }
}

编辑 Dockerfile 文件,内容如下:

FROM php:8.2-fpm
RUN docker-php-ext-install pdo_mysql mysqli

编辑 docker-compose.yml 文件,内容如下:

services:
  # 数据库服务
  mysql:
    image: mysql:8.0.0  # MySQL 8.0 官方镜像通常支持多架构
    container_name: wordpress-mysql
    restart: always
    healthcheck:
      test: ["CMD", "mysqladmin", "ping", "-h", "localhost", "-u", "root", "-p$$MYSQL_ROOT_PASSWORD"]
      interval: 5s
      timeout: 3s
      retries: 10
    environment:
      MYSQL_ROOT_PASSWORD: <root_password>  # 数据库 root 密码
      MYSQL_DATABASE: <database_name>       # 数据库名称
      MYSQL_USER: <username>                # 数据库管理员用户名(非 root 用户)
      MYSQL_PASSWORD: <password>            # 数据库管理员密码(非 root 用户)
    volumes:
      - mysql_data:/var/lib/mysql                          # 数据持久化
      - ./init.sql:/docker-entrypoint-initdb.d/init.sql    # 初始化SQL脚本[6](@ref)[8](@ref)
    ports:
      - "3306:3306"
    networks:
      - wordpress-network

  # PHP 处理服务
  php:
    build: .                                               # 使用PHP 8.2 FPM镜像
    container_name: wordpress-php
    restart: always
    volumes:
      - ./wordpress:/var/www/html                          # 挂载WordPress源码
    depends_on:
      - mysql
    networks:
      - wordpress-network

  # Web 服务器
  nginx:
    image: nginx:alpine                                    # 轻量级Nginx镜像
    container_name: wordpress-nginx
    restart: always
    ports:
      - "80:80"                                            # 将本地80端口映射到容器
      - "443:443"
    volumes:
      - ./wordpress:/var/www/html                          # 静态文件和PHP代码
      - ./nginx.conf:/etc/nginx/conf.d/default.conf:ro     # Nginx配置[2]
      - ./certs:/etc/nginx/certs:ro
    depends_on:
      - php
    networks:
      - wordpress-network

# 定义网络和数据卷
networks:
  wordpress-network:
    driver: bridge

volumes:
  mysql_data:
    driver: local

运行命令启动服务:

# 构建 PHP 镜像
$ docker-compose build php
# 启动所有服务
$ docker-compose up -d

前往域名注册平台,将域名解析到服务器对应的 IP 地址。(可能需要几分钟生效)

访问你的域名 https://your_domain.com 不出意外就可以看到网站了。

这里不需要像在本地测试那样修改 wp_options 表中的 siteurl 和 home 字段了,因为本身就是要通过域名进行访问的。

可能问题一:压根没法访问

通常买一台云服务器,默认关闭绝大多数端口,需要前往云服务控制台手动放开对应的端口号。

检查服务器是否放开了 80 和 443 端口。

可能问题二:看到 HTTP/1.1 403 Forbidden 页面

通过域名访问返回 HTTP/1.1 403 Forbidden,原因是 Nginx 没有权限访问 wordpress 源码目录。

$ chmod -R 755 ./wordpress-docker

返回首页

本文总阅读量  次
皖ICP备17026209号-3
总访问量: 
总访客量: