1. 为什么我最终选了 Docker 这条路
先说结论:维基萌博客系统这套东西,我前后折腾过三种部署方式——宝塔面板直接跑、手动编译装环境、最后才换到 Docker 容器化部署。如果你现在问我推荐哪种,我毫不犹豫推荐 Docker,而且最好是 Docker Desktop 或者 Linux 服务器上的 Docker Engine 都行,关键是掌握一套思路,而不是死记某条命令。
维基萌博客系统本质上是一个以内容管理为核心的博客平台,底层依赖 PHP 运行环境、数据库(MySQL 或者兼容 MySQL 协议的关系型数据库)、Web 服务器(Nginx 为主),再加上一堆 PHP 扩展和缓存组件。这种组合装起来不难,难的是迁移、备份、版本升级和多环境复现。传统方式下,你今天在一台服务器上配好了,明天换一台机器,还得重新装一遍 PHP 扩展、重新改 Nginx 配置、重新初始化数据库,中途任何一个版本对不上,就是各种奇奇怪怪的报错。
Docker 解决的就是这个问题。它把整个运行环境打包成镜像,镜像里面 PHP 版本、扩展、配置、Nginx 规则全给你定死,走到哪儿都是一样的行为。我实际用下来最大的感受是:前期学习成本换来的是后期极大的省心,尤其是博客系统这种需要长期维护的项目,你不可能每次都靠记忆力去复现环境。
这篇文章我会按照我自己的实操路径来写,从镜像选择、目录规划、编排文件编写、数据库初始化,到上线后的备份和故障排查,一步步拆开讲。里面提到的细节都是我踩过坑之后总结出来的,希望你看完能直接照着操作,不用再走一遍弯路。
2. 部署前的思路整理:先想清楚再动手
2.1 这套博客系统的组成拆解
维基萌博客系统的部署,本质上可以拆成四个部分:
- Web 服务:Nginx,负责接收 HTTP 请求、处理静态资源、转发 PHP 请求给 PHP-FPM。
- 应用运行时:PHP-FPM,负责执行 PHP 代码。这个博客系统对 PHP 版本有要求,我当时用的是 PHP 8.1,实测稳定。
- 数据存储:MySQL 8.0,用于存放文章、用户、评论、配置等数据。
- 缓存与辅助:Redis,用于缓存热点数据、Session,减轻数据库压力。
这四个部分各有各的配置要求,如果用传统方式装,你得分别下载、安装、配置、启动,任何一环出了问题都不好排查。用 Docker 之后,四个容器各干各的活,通过内部网络互相通信,结构非常清晰。
2.2 网络模式的选择逻辑
Docker 容器之间的通信,最省事的方式是创建一个自定义 bridge 网络。我见过很多人直接让容器用 host 网络模式,或者靠端口映射把服务暴露到宿主机再互相访问,这样不是不行,但安全性和隔离性都差一些。
我创建了一个名为wm_blog_net的网络,所有容器都接入这个网络,它们之间通过服务名直接通信。比如 PHP-FPM 容器访问数据库,用的地址是mysql:3306,而不是127.0.0.1:3306。这个设计有两点好处:
- 容器 IP 会变,但服务名稳定,重启之后不用改配置。
- 内部流量不经过宿主机网卡,少一层转发,性能更好,也更安全。
提示:如果你用的是 Docker Desktop(Windows 或 macOS),网络模式同样适用,不需要额外配置。真正需要注意的是文件挂载路径,Windows 下挂载目录的权限和路径分隔符跟 Linux 有差异,后面我会专门说这个问题。
2.3 目录规划:让数据和安全解耦
容器本身是无状态的,重启之后容器内的文件系统改动会丢失(除非你提交成新镜像)。所以部署前必须做好目录挂载规划,把所有需要持久化的数据放到宿主机目录。
这是我当时的目录结构:
/home/wm_blog/ ├── docker-compose.yml ├── .env ├── nginx/ │ └── conf.d/ │ └── default.conf ├── php/ │ └── php.ini ├── mysql/ │ ├── data/ # 数据库数据文件 │ └── init/ # 初始化 SQL 脚本 ├── blog/ # 博客系统源码 └── logs/ ├── nginx/ └── php/核心原则是:数据文件、配置文件、日志文件全部挂载到宿主机。这样容器随便删、随便重建,数据一点不丢。我后来升级镜像版本的时候,就是直接改镜像版本号然后docker compose up -d,全程零数据损失。
3. 镜像选型与编排文件编写
3.1 镜像版本怎么选
镜像版本这块我踩过一个大坑。第一次部署的时候,我图省事直接用了mysql:latest,结果拉下来的是 MySQL 8.4,跟博客系统初始化脚本里的某些语法不兼容,折腾了两个小时才定位到是数据库版本的问题。后来我把所有镜像版本都固定下来,标注成明确的版本号,再没出过这种诡异问题。
我最终选定的版本组合:
| 服务 | 镜像 | 版本说明 |
|---|---|---|
| Nginx | nginx:1.27-alpine | Alpine 版体积小,约 25MB,跑起来很稳 |
| PHP | php:8.1-fpm | 官方 FPM 镜像,需要自行安装扩展 |
| MySQL | mysql:8.0 | 8.0 系列,兼容性好,初始化脚本能跑通 |
| Redis | redis:7-alpine | 轻量且稳定,主要做缓存和 Session |
这里多说一句:PHP 官方镜像不带扩展,你需要自己装。博客系统一般需要pdo_mysql、mysqli、redis、gd、zip这几个扩展。我当时写了一个 Dockerfile 来构建 PHP 镜像,你也可以用docker-php-ext-install命令在容器里手动装,但每次重建容器都得重来一遍,建议还是做成镜像。
3.2 Dockerfile 构建 PHP 运行时
这是我用的 PHP Dockerfile,直接贴出来,注释我写在后面:
FROM php:8.1-fpm # 安装系统依赖和 PHP 扩展 RUN apt-get update && apt-get install -y \ libzip-dev \ libpng-dev \ libjpeg-dev \ libfreetype6-dev \ && docker-php-ext-configure gd --with-freetype --with-jpeg \ && docker-php-ext-install -j"$(nproc)" pdo_mysql mysqli gd zip \ && pecl install redis \ && docker-php-ext-enable redis \ && apt-get clean # 复制自定义配置 COPY php.ini /usr/local/etc/php/conf.d/custom.ini WORKDIR /var/www/html几个关键点:
docker-php-ext-install是官方镜像自带的扩展安装脚本,装完自动生成.so文件并写入配置,省去手动编译的麻烦。gd扩展需要先配置--with-freetype --with-jpeg,否则后续处理图片验证码之类的功能会报 "Call to undefined function imagecreatefromjpeg"。pecl install redis需要容器有网络,构建镜像的时候要保证能访问外网。docker-php-ext-enable redis相当于自动在docker-php-ext-redis.ini里添加extension=redis.so一行,比手动编辑配置文件稳当。
3.3 docker-compose.yml 完整编排
下面是完整的编排文件,这是整个部署的核心,建议直接复制然后按需调整:
version: "3.8" services: nginx: image: nginx:1.27-alpine container_name: wm_nginx ports: - "8080:80" volumes: - ./blog:/var/www/html:ro - ./nginx/conf.d:/etc/nginx/conf.d:ro - ./logs/nginx:/var/log/nginx depends_on: - php networks: - wm_blog_net php: build: ./php image: wm_blog_php:8.1 container_name: wm_php volumes: - ./blog:/var/www/html - ./logs/php:/var/log/php depends_on: - mysql - redis networks: - wm_blog_net mysql: image: mysql:8.0 container_name: wm_mysql ports: - "3306:3306" environment: MYSQL_ROOT_PASSWORD: "${MYSQL_ROOT_PASSWORD}" MYSQL_DATABASE: "${MYSQL_DATABASE}" MYSQL_USER: "${MYSQL_USER}" MYSQL_PASSWORD: "${MYSQL_PASSWORD}" TZ: "Asia/Shanghai" volumes: - ./mysql/data:/var/lib/mysql - ./mysql/init:/docker-entrypoint-initdb.d:ro command: - --character-set-server=utf8mb4 - --collation-server=utf8mb4_unicode_ci networks: - wm_blog_net redis: image: redis:7-alpine container_name: wm_redis volumes: - ./redis/data:/data command: ["redis-server", "--appendonly", "yes"] networks: - wm_blog_net networks: wm_blog_net: driver: bridge.env 文件内容:
MYSQL_ROOT_PASSWORD=your_strong_root_password MYSQL_DATABASE=wm_blog MYSQL_USER=wm_user MYSQL_PASSWORD=your_strong_user_password注意:
.env文件不要提交到 Git 仓库,里面是数据库密码,属于敏感信息。我见过有网友把.env传上 GitHub,结果服务器被扫库,数据被删了勒索比特币。数据库端口3306尽量别暴露公网,如果只是本机或内网使用,把ports里的3306:3306注释掉,容器之间通过网络正常通信,不需要映射到宿主机。
4. Nginx 配置与 PHP 联调细节
4.1 Nginx 配置文件这样写才稳
Nginx 配置是整个部署过程中最容易出问题的地方。维基萌博客系统的伪静态规则依赖 PathInfo 模式,如果你直接把普通的 LNMP 配置套上去,大概率会出现 404 或者访问首页正常、访问文章页就白屏的情况。
我的 Nginx 配置文件/home/wm_blog/nginx/conf.d/default.conf:
server { listen 80; server_name your-domain.com; root /var/www/html/public; index index.php index.html; access_log /var/log/nginx/access.log; error_log /var/log/nginx/error.log; location / { try_files $uri $uri/ /index.php?$query_string; } location ~ \.php$ { fastcgi_pass php:9000; fastcgi_index index.php; fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; include fastcgi_params; fastcgi_param PATH_INFO $fastcgi_path_info; } location ~ /\.ht { deny all; } }几个细节说明:
root指向/var/www/html/public,这个取决于博客系统源码的入口目录。有的系统入口在根目录,有的是public子目录,你必须先确认清楚再写 root,否则会出现 "No input file specified" 这类经典报错。fastcgi_pass php:9000,这里的php是 docker-compose 里的服务名,Nginx 容器会通过自定义网络解析到这个服务名,从而找到 PHP-FPM 容器的 9000 端口。fastcgi_param PATH_INFO $fastcgi_path_info这行很重要。PathInfo 模式下,URL 里的路径信息需要显式传给 PHP,否则部分框架路由无法正常工作。
4.2 一个容易被忽略的权限问题
我在第一次部署时遇到一个诡异的问题:博客能安装,但是上传图片全部失败,日志里报的是权限不足。查了半天才定位到,Nginx 容器和 PHP 容器虽然挂载了同一个源码目录,但两个容器内的用户不同。Nginx 容器里跑的是nginx用户,PHP 容器里跑的是www-data用户,宿主机目录权限如果只给了某个 UID,另一边的进程就没法写入。
解决办法很简单,给源码目录设置成 755 或 775,并确保 PHP 容器内的www-data用户对上传目录有写权限。如果你用的是 Linux 服务器,可以直接操作:
chown -R www-data:www-data /home/wm_blog/blog chmod -R 755 /home/wm_blog/blog chmod -R 775 /home/wm_blog/blog/storage /home/wm_blog/blog/uploadsWindows 上用 Docker Desktop 的话,文件权限映射有自己的规则,一般不用特意处理,但如果上传时遇到 "Permission denied",优先检查目录读写权限,再检查是不是防病毒软件在中间拦截。
5. 数据库初始化与数据迁移
5.1 利用官方初始化机制导入数据
MySQL 官方镜像有一个非常方便的特性:容器首次启动时,会依次执行/docker-entrypoint-initdb.d/目录下的.sql脚本。这意味着你只需要把建库建表的 SQL、初始数据丢到这个目录,容器第一次启动就会自动导入,不需要手动进容器执行。
我的操作方式是这样的:
- 在原环境(比如你之前用宝塔跑着的博客)导出数据库:
mysqldump -u root -p wm_blog > backup.sql。 - 把
backup.sql放到/home/wm_blog/mysql/init/目录下。 - 执行
docker compose up -d启动 MySQL 容器,首次启动时自动导入。
这里的坑是:如果你已经启动过 MySQL 容器,/docker-entrypoint-initdb.d/下的脚本不会再执行。因为 MySQL 的数据目录/var/lib/mysql已经有初始化标记了。这种情况下,你需要手动导入,或者先删掉./mysql/data目录里的数据重新初始化,但删除前务必确认数据已备份。
警告:
rm -rf ./mysql/data是危险操作,相当于删库。我建议先把整个data目录改名备份,比如mv data data_bak,确认新库没问题再用再删。做运维动作一定要留后路,这是老生常谈,但真正做到的人不多。
5.2 数据库连接配置怎么写
博客系统安装完成后,数据库连接配置一般在源码的配置目录里,通常是.env或者config/database.php。你需要把数据库地址改成mysql,而不是127.0.0.1,因为博客应用跑在 PHP 容器里,访问的数据库是另一个容器,IP 会变,但服务名mysql是固定的。
举个例子,如果博客系统支持.env配置:
DB_HOST=mysql DB_PORT=3306 DB_DATABASE=wm_blog DB_USERNAME=wm_user DB_PASSWORD=your_strong_user_password改完之后,记得重启 PHP 容器让配置生效:
docker compose restart php如果配置不生效,多半是 PHP 有配置缓存(比如 Laravel 系的config:cache),需要在源码目录清理缓存。
6. 容器启动顺序与依赖处理
6.1 用 depends_on 控制启动顺序
docker-compose 里depends_on只是控制容器启动的先后顺序,不代表上游服务已经就绪。什么意思?就是说mysql容器启动了,但 MySQL 服务可能还在初始化中(首次启动要初始化数据目录、加载插件,这个过程可能持续几十秒),这时候 PHP 应用去连数据库,可能就连不上。
解决这个问题的常用办法有两个:
- 在应用启动脚本里加一个等待逻辑,循环检测数据库端口是否可连,通了再启动 PHP-FPM。
- 在 PHP 容器启动时手动写一个包装脚本,内置等待逻辑。
我的做法是写了一个entrypoint.sh:
#!/bin/bash echo "Waiting for MySQL to be ready..." until mysqladmin ping -h mysql -u wm_user -p${MYSQL_PASSWORD} --silent; do sleep 2 done echo "MySQL is ready, starting PHP-FPM..." exec php-fpm然后在 Dockerfile 里把默认的 entrypoint 替换成这个脚本:
COPY entrypoint.sh /usr/local/bin/entrypoint.sh RUN chmod +x /usr/local/bin/entrypoint.sh ENTRYPOINT ["/usr/local/bin/entrypoint.sh"]mysqladmin是 MySQL 客户端工具,php:8.1-fpm镜像里不一定自带,需要先apt-get install -y default-mysql-client。如果你不想装客户端,也可以用php -r "try { new PDO('mysql:host=mysql;port=3306', 'user', 'pass'); echo 'OK'; } catch (Exception $e) { exit(1); }"做检测,效果一样。
6.2 Redis 连接同样要做降级处理
缓存组件有一点好:就算 Redis 挂了,应用一般不会直接死掉,顶多是性能下降或者 Session 失效。所以 Redis 的依赖处理可以宽松一点,没必要写进 entrypoint 的等待逻辑。我在实际部署里只把 MySQL 等待写死,Redis 就让它自我恢复,容器重启后自动重新连接即可。
7. 上线后必做的三件事:备份、监控、更新
7.1 数据备份的傻瓜方案
很多人部署完博客就撒手不管了,直到数据丢失才追悔莫及。Docker 部署有一个天然优势:数据都在宿主机目录里,备份起来非常直接。我用的备份方案是写一个 cron 脚本,每天凌晨打包数据库和源码目录。
数据库备份脚本/home/wm_blog/backup.sh:
#!/bin/bash BACKUP_DIR=/home/wm_blog/backups DATE=$(date +%Y%m%d_%H%M%S) mkdir -p "$BACKUP_DIR" docker exec wm_mysql mysqldump -u root -p${MYSQL_ROOT_PASSWORD} wm_blog > "$BACKUP_DIR/db_$DATE.sql" tar -czf "$BACKUP_DIR/files_$DATE.tar.gz" /home/wm_blog/blog # 保留最近 7 天备份 find "$BACKUP_DIR" -name "*.sql" -mtime +7 -delete find "$BACKUP_DIR" -name "*.tar.gz" -mtime +7 -delete然后用 crontab 每天凌晨 2 点执行:
crontab -e 0 2 * * * /bin/bash /home/wm_blog/backup.sh这个方案我已经用了大半年,从来没出过问题。重要的是备份完要测试一次恢复流程,别等到真要用的时候发现备份文件是坏的。我一般每个月会手动验证一次:把备份 SQL 导入一个临时数据库,确认表结构和数据行数没问题。
7.2 日志查看与问题定位
容器化部署后,日志查看方式变成docker logs。我之前排查过一个数据库连接卡死的问题:
docker logs wm_php --tail 100 docker logs wm_mysql --tail 100 docker logs wm_nginx --tail 100三个容器的日志对比着看,基本能定位问题在哪一层。Nginx 日志主要看访问状态码和 PHP 转发是否正常,PHP 日志看有没有 PHP 报错和警告,MySQL 日志看连接数是否爆掉、有没有慢查询警告。
如果日志太多,可以先docker logs wm_nginx --since 10m只看最近 10 分钟,避免刷屏式输出影响分析。
7.3 镜像更新升级的正确姿势
博客系统官方发了新版本,怎么用 Docker 升级?我的经验是:
- 先备份(数据库 + 源码,这一步不可跳过)。
- 把新版源码文件解压到
/home/wm_blog/blog,保留原配置文件、上传目录。 - 重启 PHP 容器:
docker compose restart php。 - 刷新博客页面,确认功能正常。
- 如果出问题,把旧源码备份直接覆盖回来,再重启一次即可回滚。
比传统部署方式方便的地方在于:Nginx 配置、PHP 扩展、数据库版本这些运行环境全部固化在镜像里,升级源码时不用担心环境不匹配。真正需要升级基础镜像的时候(比如 PHP 版本升级),改一下镜像版本号,然后docker compose up -d重建容器即可,数据都在宿主机目录,不会丢失。
8. 常见问题速查与避坑指南
8.1 网络不通问题
容器之间网络不通,是最常见的坑。表现形式:PHP 容器连不上 MySQL,报Connection refused或者Host is unreachable,但明明 MySQL 容器在运行。
排查思路:
- 确认两个容器是否在同一网络:
docker network inspect wm_blog_net查看接入的容器列表。 - 确认 MySQL 容器内的实际端口:
docker exec wm_mysql netstat -tlnp | grep 3306。 - 用 PHP 容器 ping MySQL:
docker exec wm_php ping mysql。 - 检查 MySQL 是否监听所有地址:默认
bind-address=*是没问题的,但如果你自定义了配置改成127.0.0.1,容器外就访问不到。
如果用的是 Windows Docker Desktop,偶尔会遇到 "failed to connect to the docker api at npipe" 这类报错,通常是 Docker Desktop 服务没正常启动,重启 Docker Desktop 一般能解决。还有一种是 Windows 下把容器端口映射到宿主机后,防火墙拦截导致访问不了,记得在 Windows 防火墙里放行对应端口。
8.2 容器启动失败排查
docker compose up -d之后发现某个容器没起来,第一步永远是看日志:
docker compose logs nginx docker compose logs mysql几个高频原因:
- 端口被占用:比如宿主机 8080 已经被其他程序占用,Nginx 容器起不来。换个端口映射,或者停掉冲突程序。
- 挂载目录权限不对:宿主机目录不存在或者没有权限,容器启动直接报
mkdir: cannot create directory。 - 配置文件语法错误:Nginx 配置写错,容器反复重启(restart 策略导致)。用
docker exec wm_nginx nginx -t检查语法。
8.3 数据库乱码问题
博客文章出现中文乱码,大部分原因是数据库字符集设置不对。MySQL 8.0 默认字符集是utf8mb4,但如果你用的是旧版本的数据库导入,可能会有latin1的残留。
解决方式,在 docker-compose 里我已经加了启动参数:
command: - --character-set-server=utf8mb4 - --collation-server=utf8mb4_unicode_ci导入数据之前,确保.sql文件头部有:
SET NAMES utf8mb4; SET CHARACTER SET utf8mb4;导入后检查表字符集:
SHOW CREATE TABLE your_table_name;如果已经存在的表字符集不对,可以用一条命令批量转换:
ALTER TABLE your_table_name CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;8.4 Docker 镜像拉取慢的问题
国内网络环境下,直接从 Docker Hub 拉镜像经常慢到怀疑人生,甚至直接超时。我处理过最快的办法是配置镜像加速器。Docker Desktop 在设置界面有 "Docker Engine" 配置项,可以编辑 JSON 加registry-mirrors。
Linux 服务器上则编辑/etc/docker/daemon.json:
{ "registry-mirrors": ["https://docker.m.daocloud.io"] }改完重启 Docker 服务:
systemctl restart docker注意:镜像加速器不是永远的。有时候加速器本身也不稳定,我实际使用中发现,多配几个备选地址比只配一个靠谱。如果某天拉取速度突然变慢,可以先
docker search看看镜像是否正常,再换一个加速地址。
8.5 容器环境下 PHP 扩展缺失
博客系统安装完成后进入首页,提示缺少某某函数或者某个扩展没启用,这种报错在 Docker 环境里特别常见,因为你用的 PHP 镜像是最精简的,很多扩展默认没装。
我遇到过的典型报错:
Call to undefined function mysqli_connect():缺mysqli扩展。Class "Redis" not found:缺redis扩展。Call to undefined function imagecreatefrompng():缺gd扩展。
解决办法就是回到 Dockerfile,把缺的扩展补上,然后重新构建镜像:
docker compose build php docker compose up -d扩展装完后,可用下面命令验证:
docker exec wm_php php -m | grep redis docker exec wm_php php -m | grep gd docker exec wm_php php -m | grep pdo_mysql9. 我踩过的一些坑,写在最后
部署这套博客系统到现在,我最大的体会是:Docker 不是万能药,但它把环境差异带来的问题压缩到了极致。以前在 A 服务器上跑得好好的,换到 B 服务器就各种报错,这种问题在 Docker 场景下基本绝迹。
另外想提醒一点:任何部署方案,网上都有大量教程,但别人的配置永远是参考,不是答案。你能踩的坑,往往就藏在环境差异和版本差异里。比如我在 Ubuntu 上安装 Docker 时遇到过一次服务启动失败,排查下来是 Docker 源配置的问题;而同样的步骤在 CentOS 上完全是另一套操作逻辑。所以遇到问题的时候,先冷静定位,再系统排查,别急着把整个编排文件推倒重来。
如果你也想用 Docker 部署维基萌博客系统,我的建议是:先把官方文档看一遍,再用这篇文章的编排文件搭一套测试环境,跑通之后再往生产环境迁移。数据迁移那块一定要提前演练,等你需要用的时候再摸索,那种压力我经历过,不好受。
最后再分享一个小技巧:每次修改完 docker-compose.yml 或者配置文件后,执行docker compose config可以校验编排文件格式是否正确,这个命令能提前发现一大半语法错误,省得反复重启容器试错。熟用docker compose config和docker logs,你的 Docker 部署之路会顺很多。