
1. 为什么把维基萌博客塞进容器从一次翻车说起1.1 传统部署的三座大山我最早是把维基萌博客系统直接装在物理服务器上的。那台机器是台老古董系统是 CentOS 7PHP 版本停留在 5.4离系统要求的 PHP 8.x 差了十万八千里。为了把环境凑齐我前前后后折腾了一整个周末先编译升级 PHP再手动安装 pdo_mysql、opcache、redis、gd 这些扩展中途还因为 MySQL 版本太旧导致数据表导入报错最后发现上传目录权限不对图片传上去全是 500。这个经历让我深刻体会到PHP 这类应用最大的问题从来不是程序本身而是运行环境。同一个博客系统换一台服务器装出来的结果可能完全不同。系统库版本不同、扩展缺失、open_basedir 限制、SELinux 拦截任何一个环节出错页面都会以千奇百怪的方式死给你看。而且这种环境问题极难排查日志里往往只有一句含糊的“500 Internal Server Error”。1.2 容器化到底帮我省了什么事后来我决定把整个系统搬进 Docker这才真正体会到什么叫“一次构建到处运行”。容器化之后PHP 版本、扩展、Nginx 配置、Redis、MySQL 全部被固化在镜像和编排文件里一个新环境从零到能跑起来只需要执行一条docker compose up -d。这个体验上的改变是巨大的。最让我满意的是两点。第一是迁移成本变得极低老服务器退役时我把数据库导出、代码目录打包新机器上装好 Docker 后逐个恢复前前后后不到二十分钟博客就完整地跑起来了连文章的图片路径都不用改。第二是升级和回滚变得非常安全发布新版本前先备份数据然后拉取新镜像重建容器如果发现问题只需把镜像 tag 指回上一个版本再docker compose up -d即可。相比传统方式里“升级一时爽回滚火葬场”的窘境容器的不可变特性给了我极大的安全感。1.3 给还没上车的朋友容器不是魔法说句公道话Docker 不是万能药。它解决的是环境一致性和部署效率问题但数据备份、网络规划、安全加固这些事它一点都不会帮你省。我见过有人以为容器里跑 MySQL 就永远不丢数据了结果一删容器数据跟着没了——原因很简单数据卷没挂出来。如果你能接受这个前提那容器化部署维基萌博客系统就是一条性价比极高的路线。这套博客系统本身带有维基式的词条整理能力又有传统博客的文章发布、评论、标签功能非常适合做二次元资料站、设定整理站或者个人知识库。接下来我按实际操作顺序把完整流程拆给你看。2. 部署前的三张清单镜像、目录、端口2.1 镜像选择官方镜像还是自己构建部署第一步是选择镜像。如果项目方提供了官方 Docker 镜像优先直接用官方镜像因为版本匹配、扩展齐全、甚至 Nginx 配置都是调好的。如果项目方没提供那就得基于php:8.2-fpm这类官方基础镜像自己构建。我自己用的构建思路大致是这样的先用docker-php-ext-install安装 PHP 扩展再用 PECL 安装 redis 扩展最后拷贝业务代码进去FROM php:8.2-fpm RUN docker-php-ext-install pdo_mysql mysqli opcache \ php -r copy(https://pecl.php.net/get/redis-6.0.2.tgz, /tmp/redis.tgz); \ pecl install /tmp/redis.tgz \ docker-php-ext-enable redis COPY ./src /var/www/html RUN chown -R www-data:www-data /var/www/html WORKDIR /var/www/html注意这几行只是个参考骨架真实构建时你得按项目的具体说明来调整。核心要点是PHP 版本必须满足系统要求pdo_mysql和redis这两个扩展几乎必备代码目录的属主一定要改成www-data否则后续 Nginx 解析 PHP 时会出现权限问题。2.2 宿主机目录规划在创建容器之前我建议先在宿主机上规划好目录结构避免容器越跑越多、目录散落各处。我的习惯是统一放在/data/wikimoe下面mkdir -p /data/wikimoe/{mysql,redis,www,backup}四个目录各司其职mysql放数据库文件redis放 Redis 持久化数据www放博客代码和上传文件backup放定期备份产物。之所以把数据库和代码目录都挂到宿主机上是因为容器本身是“一次性”的哪天镜像坏了、容器删了只要这些宿主目录还在整站数据就不会丢。2.3 端口规划只暴露必要端口端口规划上我吃过不少亏所以现在原则很明确只暴露必要的端口且尽量只绑定到本机回环地址。服务容器内部端口宿主机映射说明Nginx80127.0.0.1:8080只在本机暴露由宿主机 Nginx 反代PHP-FPM9000不映射仅容器网络内可访问MySQL3306不映射仅容器网络内可访问Redis6379不映射仅容器网络内可访问这样做的好处很明显数据库和 Redis 完全不对公网开放即使宿主机被扫描也没有直接从外部连进去的路。博客对外服务只走 80/443后续接 HTTPS 也很干净。3. 核心编排docker-compose.yml 从零写到跑通3.1 为什么选 Compose 而不是 docker run老老实实说第一次我图省事用三个docker run把 MySQL、Redis、应用分别拉起来。结果遇到两个问题一是每次重启机器后要手动按顺序启动经常忘记先启动数据库二是容器内的网络通信变得很别扭每次都要查 IP。后来换成了 Docker Compose这些问题迎刃而解。Compose 的优势在于它把容器、网络、卷、环境变量的声明都写进一个文件docker compose up -d一条命令就能拉起整个站还能通过depends_on控制启动顺序。对于多容器应用来说Compose 就是最顺手的编排工具。3.2 compose 文件逐段拆解我的docker-compose.yml长这样你可以直接参考services: mysql: image: mysql:8.0 container_name: wikimoe-mysql restart: unless-stopped environment: MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD} MYSQL_DATABASE: wikimoe MYSQL_USER: wikimoe MYSQL_PASSWORD: ${MYSQL_BLOG_PASSWORD} TZ: Asia/Shanghai command: - --character-set-serverutf8mb4 - --collation-serverutf8mb4_unicode_ci - --default-time-zone08:00 volumes: - /data/wikimoe/mysql:/var/lib/mysql healthcheck: test: [CMD-SHELL, mysqladmin ping -h localhost -u root -p$$MYSQL_ROOT_PASSWORD] interval: 10s timeout: 5s retries: 10 redis: image: redis:7-alpine container_name: wikimoe-redis restart: unless-stopped command: redis-server --appendonly yes volumes: - /data/wikimoe/redis:/data healthcheck: test: [CMD, redis-cli, ping] interval: 10s timeout: 5s retries: 10 app: image: wikimoe/blog:latest container_name: wikimoe-app restart: unless-stopped depends_on: mysql: condition: service_healthy redis: condition: service_healthy environment: TZ: Asia/Shanghai DB_HOST: mysql DB_PORT: 3306 DB_NAME: wikimoe DB_USER: wikimoe DB_PASSWORD: ${MYSQL_BLOG_PASSWORD} REDIS_HOST: redis volumes: - /data/wikimoe/www:/var/www/html nginx: image: nginx:1.25-alpine container_name: wikimoe-nginx restart: unless-stopped depends_on: - app ports: - 127.0.0.1:8080:80 volumes: - /data/wikimoe/www:/var/www/html - /data/wikimoe/nginx.conf:/etc/nginx/conf.d/default.conf:ro有几个细节需要重点说明。一是MYSQL_*这些敏感信息没有写死在 yaml 里而是通过.env文件引入这样docker-compose.yml可以放心提交到 Git密码文件单独管理。二是我故意用了container_name固定名字方便用docker exec进容器时不用猜名字。三是healthcheck很重要它让应用容器真正等到数据库就绪后再启动而不是靠depends_on那种“只保证容器启动了、不保证内部服务可用”的弱依赖。对应的.env文件大概是MYSQL_ROOT_PASSWORD这里改成强密码 MYSQL_BLOG_PASSWORD这里改成另一个强密码3.3 首次启动与初始化执行docker compose up -d之后第一次启动会经历镜像拉取、网络创建、容器逐一启动的过程。等一会儿后用docker compose ps查看状态如果 MySQL 的输出里(healthy)出现了说明数据库已经就绪。接下来是博客系统的初始化安装。这个系统第一次访问时通常会跳转到安装向导要求填写数据库连接信息。这里有一个极其关键的坑数据库地址不能填127.0.0.1要填服务名mysql。因为在 Compose 创建的自定义网络里容器之间通过服务名互相访问填入127.0.0.1指向的是应用容器自己那里根本没有 MySQL。我当时就卡在这一步十分钟一直提示数据库连接失败换成mysql之后瞬间通过。3.4 验证部署安装完成后先验证容器状态docker compose ps docker compose logs -f app然后在本机探一下 Nginx 容器是否正常响应curl -I http://127.0.0.1:8080如果返回200 OK说明整条链路已经通了博客站点已经跑在容器里。4. 反向代理与 HTTPS让博客真正面向读者4.1 Nginx 放容器还是宿主机这时博客虽然能访问了但只是通过http://127.0.0.1:8080距离“公网可访问”还差一步。我建议在最外层用宿主机上的 Nginx 做反向代理而不是再套一个 Nginx 容器。原因很简单证书管理方便。次让我把证书续期、自动重载这些逻辑放进容器里权限和路径都要多绕几道而放在宿主机上用 acme.sh 一把梭省心很多。4.2 一份可用的宿主机 Nginx 配置宿主机 Nginx 的站点配置大概是这样的server { listen 80; server_name blog.example.com; location / { proxy_pass http://127.0.0.1:8080; proxy_set_header Host $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_set_header那几行必须带上真实 IP 和协议否则博客后台看到的访客 IP 全是127.0.0.1而且如果程序做了 HTTPS 相关的 URL 生成X-Forwarded-Proto传不进去会导致页面混合内容报错。如果博客系统有 WebSocket 或实时通知类功能还需要在location /里额外加proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection upgrade;是否加这行看你的版本是否需要我在实际使用中加了以防万一毕竟这类功能出了问题很隐蔽。4.3 HTTPS 证书申请与自动续期证书这块我用的是 acme.sh签发过程很简单curl https://get.acme.sh | sh /root/.acme.sh/acme.sh --issue --nginx -d blog.example.com /root/.acme.sh/acme.sh --install-cert -d blog.example.com \ --key-file /etc/nginx/ssl/blog.key \ --fullchain-file /etc/nginx/ssl/blog.crt \ --reloadcmd systemctl reload nginx脚本会自动加定时任务到期前自动续签续签成功后自动重载 Nginx整套流程不需要手工干预。唯一的注意事项是签发前确保域名已经解析到这台服务器并且 80 端口可访问否则 ACME 验证会失败。5. 实测踩坑记录虚拟化、权限、时区、镜像源5.1 Docker Desktop 启动失败Virtualization support not detected如果你是在 Windows 上用 Docker Desktop 学习或测试大概率会撞到这样一个报错Docker Desktop failed to start because virtualisation support wasnt detected。这个提示的真正含义是 Windows 的虚拟化功能没有开启不是 Docker 本身坏了。排查路径我建议按顺序来先打开任务管理器在“性能”标签里看“虚拟化”是否显示“已启用”。如果没有需要进 BIOS/UEFI 开启 Intel VT-x 或 AMD-V。第二步是确认 Windows 功能里的“适用于 Linux 的 Windows 子系统”和“虚拟机平台”这两项是否勾选了尤其是想用 WSL2 后端的话缺一不可。开启并重启之后Docker Desktop 基本就能正常起来。这一步在服务器上大概率不会遇到但很多人在自己电脑上先折腾卡得死死的不明所以。5.2 permission denied while trying to connect to the docker api另一个高频报错是permission denied while trying to connect to the docker daemon socket。原因很简单当前用户不在docker用户组里没有权限访问 Docker 的/var/run/docker.sock。解决办法是把用户加进 docker 组然后重新登录一次sudo usermod -aG docker $USER newgrp docker执行完再跑docker ps基本就好了。有一点我一定要提醒千万不要图省事去执行sudo chmod 777 /var/run/docker.sock这会向本机所有用户开放 Docker 控制权等于变相给了 root 权限风险极大。5.3 MySQL 容器权限与初始化脚本不执行MySQL 容器挂载宿主机目录后最常见的两个问题都和权限有关。第一是挂载目录权限不足容器内 mysql 用户无法在目录里创建数据文件导致启动失败解决方法是确保宿主目录属主和属组是 MySQL 容器内的用户 UID 1000 或 999或者在特殊情况下给目录设置适当权限。第二是初始化脚本不执行。MySQL 官方镜像有个规则只有数据卷为空的时候/docker-entrypoint-initdb.d/目录下的脚本才会执行。如果你像我一样先手动挂载了一个已有数据的目录再怎么往那个目录里丢init.sql都不会生效。正确做法是第一次启动前建一个空目录挂上去或者不用初始化脚本直接把.sql文件导入运行中的数据库docker exec -i wikimoe-mysql mysql -uwikimoe -p密码 wikimoe backup.sql5.4 时区问题容器默认 UTC文章发布时间差 8 小时这个坑比较隐蔽但影响不小。Docker 官方的基础镜像时区默认是 UTC导致博客后台发文章时间戳比北京时间慢 8 小时。如果你不留意会以为是程序 bug。解决思路分两层。第一层是在 compose 的环境变量里给所有容器设置TZAsia/Shanghai。第二层是 MySQL 自身也要设置时区我在command里加了--default-time-zone08:00。PHP 侧的date.timezone配置也要同步改成Asia/Shanghai。三个地方缺一不可否则如果程序是从数据库读时间、再由 PHP 格式化输出仍可能出现偏差。5.5 镜像下载慢配置镜像源国内服务器拉 Docker 官方镜像的速度相信大家都体验过一个几百 MB 的镜像下到天荒地老。解决办法是修改/etc/docker/daemon.json配置镜像加速{ registry-mirrors: [https://docker镜像加速地址] }修改后重启 Dockersudo systemctl daemon-reload sudo systemctl restart docker注意镜像加速地址请选择可信任的服务商提供的不要随便填一个来路不明的地。另外这个配置只管镜像下载不影响容器运行改完不需要重建容器。6. 数据备份、版本升级与整站迁移6.1 全量备份的组合命令网上聊 Docker 部署的人多但真正把备份讲清楚的人少。这里我给出我实际在用的备份命令组合数据库和文件分开处理docker exec wikimoe-mysql sh -c exec mysqldump --all-databases -uroot -p$MYSQL_ROOT_PASSWORD /data/wikimoe/backup/wikimoe_$(date %F).sql tar -czf /data/wikimoe/backup/wikimoe_files_$(date %F).tar.gz /data/wikimoe/www /data/wikimoe/nginx.conf第一行备份的是 MySQL 全部数据库包括系统库和业务库恢复时不容易漏。第二行备份的是博客代码目录、上传文件、Nginx 容器内的配置模板。至于 Redis 里的缓存数据即使丢了也只是冷缓存后续访问会重建所以我不把它纳入必须备份范围。备份恢复也不是什么难事docker exec -i wikimoe-mysql mysql -uroot -p$MYSQL_ROOT_PASSWORD wikimoe_2025-01-01.sql执行前建议先停掉博客应用容器再恢复避免恢复过程中有新的写请求冲突。6.2 升级的正确顺序先备份再动镜像博客系统发布新版本后升级步骤如下cat /data/wikimoe/backup/wikimoe_$(date %F).sql docker compose pull app docker compose up -ddocker compose pull app只拉取应用服务的新镜像不动 MySQL 和 Redis这样数据层毫无波澜。拉取完成后docker compose up -d只会重建镜像发生变化的服务。升级完成后我会第一时间打开首页和后台确认文章列表、上传、搜索都正常。如果升级后发现问题回滚的标准操作是docker compose stop app docker compose rm -f app docker tag wikimoe/blog:上一个版本 wikimoe/blog:latest docker compose up -d这里的前提是你在镜像仓库里保留了旧版本的 tag或者本地还缓存着旧镜像。所以升级前给当前版本打一个 tag 是值得养成的好习惯。6.3 三分钟迁移到新服务器迁移的场景一般发生在换服务器或者从家用机搬到云服务器时。我的迁移流程已经固定的在新服务器上安装好 Docker 和 Compose把/data/wikimoe/www目录和docker-compose.yml、.env传过去目录结构保持一致。接着启动 MySQL 容器不启动博客应用把备份的 SQL 导入注意密码要和之前一样。最后直接docker compose up -d。迁移全程最花时间的反而是传文件真正执行命令的时间基本在三分钟以内。这也是 Docker 部署最让我舒服的地方——换了环境但体验上跟拷贝个文件夹差不多。7. 资源限制、安全加固与长期运维7.1 给容器设定 CPU/内存上限很多人跑 Docker 博客只管up -d不管资源上限。MySQL 和 PHP-FPM 都是内存敏感型进程一旦流量异常或者程序出现 bug内存可能被吃到宿主干干净净然后触发系统 OOM把宿主机上的其他服务也波及了。我建议在 compose 里对每个服务加上额度限制services: mysql: mem_limit: 2g cpus: 2.0 app: mem_limit: 1g cpus: 1.0 redis: mem_limit: 512m cpus: 0.5mem_limit和cpus是 Compose V2 直接支持的字段简单有效。限制死之后即使某个容器出了问题它也只能在额度内折腾不会把宿主机带崩。性能确实会有上限但一个个人博客用这配置完全够跑。7.2 非 root 运行和只读文件系统安全加固这件事不需要做得特别复杂但有几条是值得做的。第一是尽量保证容器内的应用不以 root 身份运行前面 Dockerfile 里chown www-data就是为了这个。第二是如果博客系统确实没有太多上传写盘需求可以考虑把代码目录挂载为只读但维基萌博客这类系统有用户上传图片、生成缓存的需求只读挂载会带来兼容问题所以我在实际部署中没有强上而是靠宿主机权限和备份兜底。7.3 长期运维的节奏部署完成只是开始长期运维更考验人的习惯。我给自己定了几条不成文的规则每周自动跑一次数据库备份脚本日志定期用docker system prune清理无用的悬空镜像和停止容器但一定避开无人值守时执行prune -a它有可能会把你想保留的旧版本镜像也一并删掉。系统升级不追新大版本发布后等论坛反馈一周再动稳字当头。到现在为止这套基于 Docker 的维基萌博客已经稳定跑了半年多中间经历过三次版本升级、一次跨机房迁移没有一次数据丢失也没有一次需要重新配置环境。回头再看当初被 PHP 编译折腾得焦头烂额的自己我只想说一句早点用 Docker少走半年弯路。如果你也正准备部署这套系统或者想把其他 PHP 应用容器化上面的流程和坑应该能帮你省下不少时间。