从‘它怎么又挂了’到‘服务真稳’:我是如何用Docker给老旧PHP项目续命的

每次凌晨三点被报警短信吵醒,看着屏幕上熟悉的"500 Internal Server Error",我都恨不得把那个十年前写的PHP项目一把火烧了。这个祖传代码库就像个定时炸弹,明明上周还能跑,今天突然就崩了——因为某个系统库悄悄升级了,或者某个神秘的PHP扩展突然罢工。直到我把整个环境塞进Docker容器,这场持续五年的噩梦才真正结束。

1. 解剖这只"老古董":环境依赖全图谱

接手遗留项目就像考古,第一铲子必须挖清楚它到底依赖哪些"文物级"组件。我创建了一个depcheck.sh脚本,用以下命令全面扫描环境:

# 抓取PHP版本及加载的扩展
php -v
php -m

# 检查系统库依赖
ldd $(which php) | grep -E 'libssl|libcurl|libxml'

# 分析项目使用的特殊函数(需要pecl安装的扩展)
grep -r "mysql_pconnect\|mcrypt_\|xdebug_" src/

扫描结果让我倒吸凉气:这个项目需要:

  • PHP 5.4.16(2013年发布)
  • 已废弃的mysql扩展(不是mysqli!)
  • 神秘的ionCube Loader解密模块
  • 系统自带的OpenSSL 1.0.1(存在严重漏洞)

更可怕的是,项目根目录下还有个readme.txt写着:"需先执行sudo apt-get install libfreetype6-dev libjpeg62-turbo-dev",却没人知道哪些功能依赖这些库。

2. 打造时间胶囊:精准构建Docker镜像

锁定环境版本就像制作标本,必须用正确的"防腐剂"。我的Dockerfile从选择基础镜像开始就充满玄机:

# 使用官方存档仓库的旧版镜像
FROM php:5.4.16-apache

# 安装指定版本系统库(精确到小版本号)
RUN apt-get update && apt-get install -y \
    libssl1.0.0=1.0.1t-1+deb8u12 \
    libcurl3=7.38.0-4+deb8u18 \
    --no-install-recommends

# 编译安装老版本扩展(注意配置参数差异)
RUN docker-php-ext-configure gd --with-freetype-dir=/usr/include/ \
    --with-jpeg-dir=/usr/include/ \
    && docker-php-ext-install gd mysql

遇到最棘手的ionCube加载器,解决方案是手动下载历史版本:

# 在Dockerfile中添加
ADD https://downloads.ioncube.com/loader_downloads/ioncube_loaders_lin_x86-64_5.4.tar.gz /tmp/
RUN tar zxvf /tmp/ioncube_loaders_lin_x86-64_5.4.tar.gz -C /usr/local/lib/ \
    && echo "zend_extension=/usr/local/lib/ioncube/ioncube_loader_lin_5.4.so" > /usr/local/etc/php/conf.d/00-ioncube.ini

注意:老版本PHP的Docker镜像通常不再更新安全补丁,建议仅在隔离网络中使用

3. 驯服文件系统:权限与日志的陷阱

容器化后最意外的坑来自文件系统。我们的项目有这些"坏习惯":

  • /var/www/html/uploads 需要www-data用户写权限
  • /var/log/app 目录会被PHP代码直接写入
  • cache/ 目录下的临时文件需要777权限

解决方案是在Dockerfile中加入智能初始化脚本:

COPY entrypoint.sh /usr/local/bin/
RUN chmod +x /usr/local/bin/entrypoint.sh
ENTRYPOINT ["entrypoint.sh"]

entrypoint.sh内容示例:

#!/bin/bash
# 动态处理权限问题
chown -R www-data:www-data /var/www/html/uploads
mkdir -p /var/log/app && chown www-data /var/log/app

# 保持原日志文件不被覆盖
if [ ! -f /var/log/app/error.log ]; then
    touch /var/log/app/error.log
    chown www-data /var/log/app/error.log
fi

exec "$@"

4. 从单机到集群:现代化部署方案

虽然容器化了老系统,但我们可以用现代编排工具管理它。这是我的docker-compose.yml配置精髓:

version: '3.7'
services:
  legacy_app:
    build: .
    ports:
      - "8080:80"
    volumes:
      - ./src:/var/www/html
      - app_logs:/var/log/app
    environment:
      - DB_HOST=mysql_legacy
      - TZ=Asia/Shanghai
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost/health.php"]
      interval: 30s

  mysql_legacy:
    image: mysql:5.5
    volumes:
      - mysql_data:/var/lib/mysql
    environment:
      - MYSQL_ROOT_PASSWORD=this_is_legacy_code_anyway

volumes:
  app_logs:
  mysql_data:

关键优化点:

  • 使用健康检查自动恢复(虽然应用老,但编排工具新)
  • 分离数据卷避免容器销毁丢失重要日志
  • 保持原有时区配置(老代码里可能有硬编码的时间计算)

5. 那些年我们踩过的坑:经验备忘录

三年间我记录了这些典型问题及解决方案:

现象 根本原因 解决方案
页面乱码 容器缺少中文语言包 Dockerfile添加RUN apt-get install -y locales && locale-gen zh_CN.UTF-8
上传文件失败 php.ini中upload_max_filesize配置过小 创建自定义php.ini覆盖容器默认配置
定时任务不执行 crontab服务未启动 使用supervisord管理多进程
性能突然下降 容器内存限制过小 调整docker run的-m 512m参数

最讽刺的是,有次部署失败仅仅因为Docker Hub下架了某个旧镜像。后来我学会了自建私有镜像仓库,把确认可用的镜像全部备份到本地。

现在这个"老古董"不仅稳定运行,还能通过GitLab CI实现自动化部署。每次看到监控面板上平稳的直线,都会想起那些凌晨三点debug的夜晚。或许这就是工程师的浪漫——用新技术延续旧系统的生命,就像给老爷车装上电动引擎。

更多推荐