ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

飞牛NAS上用Docker Compose部署PHP+Nginx+MySQL+Redis环境

飞牛NAS上用Docker Compose部署PHP+Nginx+MySQL+Redis环境 把跑在一台普通电脑上的 PHP 开发环境整个搬进飞牛 NAS是我折腾完 fnOS 之后做的第一件正经事。飞牛 NAS 自带 Docker 运行时和 Compose 插件不需要额外装 Docker Desktop 这类工具直接用 Docker 把 Nginx、PHP 8.2Alpine、Redis、MySQL 四件套编排起来一个目录就能管理全部配置和数据。这套环境既能当内网开发服务器跑 PHP 项目也能把 WordPress、Typecho 这类个人站点长期跑在 NAS 上还不会把 NAS 系统本身搞脏。这篇文章我会从镜像选型、目录规划、compose 编排、配置拆解到联调排错完整过一遍。适合手里有飞牛 NAS、又想在它上面跑 PHP 应用的人也适合第一次接触 Docker 组合部署的新手。整套方案用到的都是常见镜像照着做基本不会翻车。1. 部署之前这套方案为什么值得搭1.1 核心组合解决的问题很多 NAS 用户遇到的实际需求其实很典型有一个 PHP 写的网站或程序想在 NAS 上跑可能需要 Nginx 做 Web 入口PHP-FPM 处理动态请求MySQL 存业务数据Redis 做缓存或 Session 存储。手动在系统里装这一套不是不行但会牵扯到依赖库、进程管理、开机自启、路径权限等一系列问题而且 NAS 系统每次升级都可能把环境搞挂。Docker 的好处是把这四样东西分别装进独立容器彼此之间通过网络互相访问宿主机只负责提供 CPU、内存和磁盘。飞牛 NAS 的 Docker 管理界面能直接看容器状态和日志SSH 进系统后用命令行操作也很顺手。这套组合搭好后复制整个目录到另一台机器上再执行一次docker compose up -d环境就原地复活这是传统安装方式很难做到的。1.2 为什么选 Alpine 镜像标题里特意写了 PHP 8.2 (Alpine)因为 Alpine Linux 出身的镜像体积非常小。php:8.2-fpm-alpine解压后只有一百多 MBnginx:alpine也就几十 MBredis:alpine更夸张十几 MB。相比之下MySQL 官方镜像因为自带一堆工具和国际化字符集体积明显偏大但它是整个环境里唯一绕不开的大块头。小体积带来的直接好处是拉取快、占磁盘少、开机省内存。对于家里那台主内存不算充裕的 NAS能省一点是一点。不过也要先泼盆冷水轻量级指的是镜像体积小、进程干净不是指内存占用能低到忽略不计。MySQL 启动后随配置不同会吃几百 MB 内存这点要提前知道。1.3 部署方式的整体思路我的方案是一个自定义 Docker 网络bridge 类型把 Nginx、PHP、Redis、MySQL 四个容器都放进去容器之间用服务名互相解析PHP 连接 MySQL 和 Redis 时直接写mysql、redis这种名字不用关心容器 IP 会不会变宿主机只需要对外开放一个 Nginx 端口其他容器端口按需决定是否映射出来。这种设计的优势很实际IP 变化不影响内部连接、端口冲突少、还可以用server_name在同一台 Nginx 上跑多个域名站点。后面我会把每一步配置原样贴出来。2. 开始前的目录与端口规划2.1 文件目录结构Docker 部署最忌讳什么都放容器里。容器是临时性的更新镜像后原容器会被替换所以数据、配置、代码都要挂载到宿主机的独立目录。我在飞牛 NAS 的存储卷下建了一个专门目录结构如下/vol1/docker/lnmp/ ├── docker-compose.yml ├── www/ # 网站代码Nginx 和 PHP 共享 │ └── public/ ├── nginx/ │ └── conf.d/ │ └── default.conf # 站点配置 ├── php/ │ ├── Dockerfile # 自定义 PHP 镜像 │ └── php.ini # 覆盖默认 PHP 配置 ├── mysql/ │ ├── conf/ │ │ └── my.cnf # MySQL 字符集等配置 │ └── data/ # MySQL 数据文件 └── redis/ └── data/ # Redis 持久化数据这个目录规则建议从头就定好。www是代码目录Nginx 和 PHP 两个容器必须都挂载它而且要保证容器内路径一致不然会出现Nginx 能找到文件、PHP-FPM 却找不到的诡异问题。mysql/data和redis/data分别是数据库和缓存的数据落盘目录在飞牛文件管理器里先建好即可。2.2 端口规划NAS 上通常已经跑了系统管理界面和其他服务端口很容易撞车。我在案例里这样规划服务容器内端口宿主机映射端口说明Nginx808080对外统一入口也是代码调试入口PHP-FPM9000不映射只让 Nginx 在内部网络访问Redis6379不映射默认只在内部使用不暴露到外网MySQL33063307映射给桌面工具远程连接用这里有个关键点PHP-FPM 和 Redis 都不映射到宿主机因为它们只服务内部应用。MySQL 映射成 3307 而不是 3306是为了避免和 NAS 上其他 MySQL 或相关服务冲突。如果只跑 PHP 应用、不打算用 Navicat 之类的工具连库那 MySQL 端口也可以完全不用映射内网里访问不到数据库反而更安全。2.3 网络配置的选择Docker 有多种网络模式这里使用自定义 bridge 网络。默认的 bridge 网络虽然也能用但自定义网络的容器间可以通过服务名直接互相ping这一点对 compose 编排尤其重要。在 compose 文件里四个服务都声明挂在同一个网络下Nginx 配置里就可以直接写fastcgi_pass php:9000这种写法PHP 代码里连接数据库也可以直接写mysql:3306。用服务名而不是 IP 的好处是容器重启、重建后 IP 可能变化但服务名永远不变配置一次写死后续维护省心。这也是 Docker 内部服务通信的推荐做法。3. 关键配置逐项拆解3.1 docker-compose 完整编排文件直接给出我在用的docker-compose.yml密码部分做了替换实际部署时记得全部改掉services: nginx: image: nginx:1.27-alpine container_name: fn-nginx restart: unless-stopped ports: - 8080:80 volumes: - ./nginx/conf.d:/etc/nginx/conf.d:ro - ./www:/var/www/html depends_on: - php networks: - lnmp php: build: ./php image: lnmp-php:8.2 container_name: fn-php restart: unless-stopped volumes: - ./www:/var/www/html - ./php/php.ini:/usr/local/etc/php/conf.d/zz-overrides.ini:ro networks: - lnmp redis: image: redis:7-alpine container_name: fn-redis restart: unless-stopped command: [redis-server, --requirepass, RedisChangeMe2024, --appendonly, yes] volumes: - ./redis/data:/data networks: - lnmp mysql: image: mysql:8.0 container_name: fn-mysql restart: unless-stopped ports: - 3307:3306 environment: MYSQL_ROOT_PASSWORD: RootChangeMe2024 MYSQL_DATABASE: appdb MYSQL_USER: appuser MYSQL_PASSWORD: AppChangeMe2024 volumes: - ./mysql/data:/var/lib/mysql - ./mysql/conf/my.cnf:/etc/mysql/conf.d/my.cnf:ro networks: - lnmp networks: lnmp: name: lnmp-netrestart: unless-stopped是 NAS 场景下的推荐配置开机后容器会自动跟着 Docker 服务拉起不用手动一个个启动。depends_on只保证 Nginx 在 PHP 之后创建并不保证 PHP 进程已经完全就绪所以真正联调时 Nginx 偶尔会出现一次 502后面我会专门讲怎么处理。3.2 PHP 容器镜像定制官方php:8.2-fpm-alpine镜像默认不带pdo_mysql、mysqli、redis这些扩展直接拿来用连数据库都连不上。我在php/Dockerfile里做了一个很小幅度的定制FROM php:8.2-fpm-alpine RUN apk add --no-cache $PHPIZE_DEPS \ docker-php-ext-install pdo_mysql mysqli opcache \ pecl install redis \ docker-php-ext-enable redis \ apk del $PHPIZE_DEPS逐行解释一下。apk add --no-cache $PHPIZE_DEPS是给 Alpine 容器装上编译 PHP 扩展需要的工具链$PHPIZE_DEPS是官方镜像预留的环境变量包含 gcc、make、autoconf 等。docker-php-ext-install是官方镜像自带的扩展安装脚本编译安装pdo_mysql、mysqli、opcache。pecl install redis编译安装 phpredis 扩展PHP 代码里new Redis()就靠它。最后apk del $PHPIZE_DEPS把编译工具链删掉保持镜像我乐意的轻量状态。这套 Dockerfile 只在第一次build时编译一次之后容器重建都会直接复用构建好的镜像不用担心每次启动都要重新编译。如果以后想升级 PHP 8.3只要把第一行的8.2改成8.3再重新 build 一次就行。3.3 Nginx 站点配置Nginx 的配置文件放在nginx/conf.d/default.conf挂载进容器后会自动生效。我最常用的基础配置长这样server { listen 80; server_name default.local; root /var/www/html/public; index index.php index.html; location / { try_files $uri $uri/ /index.php?$query_string; } location ~ \.php$ { include fastcgi_params; fastcgi_pass php:9000; fastcgi_index index.php; fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; } location ~ /\. { deny all; } }这里最容易踩坑的是root路径。注意它写的是容器内的路径/var/www/html/public不是宿主机上的/vol1/docker/lnmp/www/public。因为 Nginx 容器看到的文件系统是挂载进去的那一层宿主机路径对它没有意义。同理fastcgi_param SCRIPT_FILENAME必须用$document_root拼出容器内路径PHP-FPM 才能正确找到文件。try_files那行是给使用了路由的 PHP 框架准备的比如 ThinkPHP、Laravel、CodeIgniter。如果只跑一个简单的单入口 PHP 文件把public改成对应目录即可。最后的location ~ /\.是拦掉.git、.env这类点开头的文件和目录防止源码泄露。3.4 MySQL 与 Redis 配置MySQL 的字符集配置挂在mysql/conf/my.cnf[mysqld] character-set-serverutf8mb4 collation-serverutf8mb4_0900_ai_ci default-time-zone08:00 max_connections200 [client] default-character-setutf8mb4utf8mb4是现在唯一推荐的中文存储字符集能完整支持 emoji 和生僻字。default-time-zone08:00让数据库时间和 NAS 本地时间保持一致避免 PHP 程序写入的时间戳在查询时出现 8 小时偏差。挂载进容器时我加了:ro参数配置目录以只读方式挂载防止容器内误改。Redis 的配置没有用单独的 config 文件直接在 compose 的command里写了关键参数--requirepass设置密码--appendonly yes开启 AOF 持久化。Redis 作为缓存可以容忍丢数据但如果你的应用把它当临时存储用AOF 能有效减少重启后的数据丢失。生产环境一般会搭配主从或集群家里 NAS 单机部署把 AOF 开着就够了。4. 实操部署与联调验证4.1 启动完整环境所有配置文件就位后进入项目目录执行cd /vol1/docker/lnmp docker compose up -d --build--build是为了把刚才的 PHP Dockerfile 构建成lnmp-php:8.2镜像。第一次执行会拉取 Nginx、Redis、MySQL 镜像网速正常的话几分钟内能完成。执行完用docker ps查看四个容器是否都处于Up状态再用docker images对比一下镜像体积你会直观感受到 Alpine 系镜像有多省空间。飞牛 NAS 的 Docker 图形界面也能看到这些容器。我习惯 SSH 进去用命令行因为日志查看、重启单个服务都比点界面快。但如果你不熟悉命令行完全可以在文件管理器里把 yml 和配置写完然后在 Docker 应用里创建 Compose 项目效果一样。启动后先在浏览器访问http://NAS的IP:8080如果看到 403 或 404基本都是www目录里还没有public/index.php。先放一个最简单的探针文件进去?php phpinfo();刷新页面能看到 PHP 版本信息和扩展列表说明 Nginx、PHP-FPM、挂载目录这一整条链路已经通了。看到pdo_mysql和redis都在扩展列表里说明刚才的 Dockerfile 构建成功。4.2 验证 PHP 连接 MySQL 和 Redis链路通之后验证 PHP 能不能连上另外两个服务。我写一个临时测试文件www/public/test.php?php echo h2PHP 运行正常/h2; echo PHP 版本 . PHP_VERSION . br; $redis new Redis(); $redis-connect(redis, 6379); $redis-auth(RedisChangeMe2024); $redis-set(demo, hello redis . date(Y-m-d H:i:s)); echo Redis 读取结果 . $redis-get(demo) . br; $pdo new PDO( mysql:hostmysql;port3306;dbnameappdb;charsetutf8mb4, appuser, AppChangeMe2024 ); echo MySQL 当前时间 . $pdo-query(SELECT NOW())-fetchColumn();这里我把数据库连接串里的主机名写成mysqlRedis 写成redis它们不是魔法就是 compose 网络里对应的服务名。能在同一网络下访问是因为刚才所有容器都挂在了lnmp-net这个自定义网络里。测试页里redis-connect和$pdo两个连接都成功整个环境就算真正跑通了。记得测完立刻把test.php删掉phpinfo()泄露的配置细节对攻击者很有参考价值不要留在站点目录里。4.3 多站点与自定义域名配置这套 Nginx 容器天然支持在同一端口上跑多个站点原理是server_name分流。比如我想在同一个 8080 端口上跑 A 站和 B 站就再写一个nginx/conf.d/site2.conf内容大致是另一份server块把server_name site2.home、root /var/www/html/site2改掉即可。Nginx 会依据请求头里的 Host 字段决定把请求交给哪个server块。客户端这边需要手动把域名解析到 NAS。在 Windows 的C:\Windows\System32\drivers\etc\hosts或 macOS/Linux 的/etc/hosts里加两行192.168.1.100 default.local 192.168.1.100 site2.home把192.168.1.100换成你 NAS 的实际内网 IP。之后访问http://default.local:8080和http://site2.home:8080就会分别进入对应站点。如果你偏好用不同端口区分环境也可以让 Nginx 的某个server块改成listen 8081;再在 compose 的 nginx 端口映射里加一行- 8081:8081两种方式都能用。我的建议是用域名方式因为重换端口不用改客户端 hosts。5. 常见问题排查与避坑笔记5.1 高发问题速查表实际部署中大家踩的坑高度集中我整理了一张速查表现象常见原因解决办法浏览器访问返回 502Nginx 连不到 PHP-FPM检查fastcgi_pass是否写成php:9000用docker logs fn-php确认 PHP 容器没崩访问 PHP 文件返回 404Nginx 和 PHP 的根目录路径不一致检查root和SCRIPT_FILENAME是否都是容器内路径/var/www/html/...PHP 连 MySQL 报 Connection refusedMySQL 没起完或网络不在同一个用docker ps看 mysql 状态确认 compose 里两个服务挂在同一网络MySQL 容器反复重启数据目录权限不对检查/vol1/docker/lnmp/mysql/data属主必要时用命名卷替代 bind 挂载Redis 连接报 NOAUTHrequirepass 已设置但没 auth测试代码里补$redis-auth(密码)宿主机 3306 端口已被占用NAS 上已有数据库服务把 compose 里的宿主机端口改成 3307 这类不冲突的端口PHP 页面报缺少 pdo_mysql用的是基础镜像没装扩展确认 compose 的 php 服务用的镜像是lnmp-php:8.2而不是官方原版compose up 后容器瞬间退出配置语法错误或密码策略不满足用docker compose logs 服务名查看具体报错日志5.2 MySQL 数据目录权限的独家经验MySQL 官方镜像的初始化脚本会尝试把数据目录的属主改成 MySQL 用户UID 999。在普通 Linux 服务器上这个过程毫无存在感但在 NAS 上如果mysql/data是放在某些特殊文件系统或网络盘上可能会出现chown: operation not permitted导致容器一直启动失败。我的处理方法是先在宿主机上手动把mysql/data的所有者改成 999命令很简单chown -R 999:999 /vol1/docker/lnmp/mysql/data如果改完之后仍然报权限错误就不要用 bind 挂了改成用 Docker 命名卷。把 compose 里 MySQL 的 volumes 写成mysql-data:/var/lib/mysql并在文件末尾声明volumes: mysql-data:。命名卷由 Docker 管理文件权限基本不会再出这类问题。代价是数据不直接暴露在目录里备份时要额外用docker run --rm -v之类的方式导出来。我个人的习惯是数据重要的服务优先用命名卷图省事直接备份目录的话才用 bind 挂载。5.3 几条长期稳定运行的建议这套环境跑起来之后有几件事值得坚持做。第一所有密码不要写在 compose 文件里提交到版本库可以用环境变量或.env文件管理这样容器重建时不会因为密码改动引发混乱。第二NAS 的内网环境不代表安全不要把 8080、3307 这类端口直接映射到公网飞牛 NAS 自带的端口转发和防火墙规则值得花十分钟研究一下只开放真正需要的访问来源。第三也是我最想强调的升级镜像前先用docker compose stop停掉服务手动备份mysql/data和redis/data两个目录再执行docker compose pull docker compose up -d。MySQL 跨大版本升级尤其谨慎比如官方 8.0 镜像内部的数据文件格式和 5.7 不通用一旦启动升级就可能无法回退。备份永远比任何回滚方案都便宜。连接外部工具时Redis Desktop Manager 这类客户端连不上比较常见多数是因为我把 Redis 的 6379 端口没有映射到宿主机。如果你确实需要在电脑上直接连 Redis把 compose 里 redis 服务的ports加上- 6380:6379连接时填写6380和密码即可。最后再分享一个我日常用得最多的技巧把www目录通过飞牛 NAS 的 SMB 共享出来开发时电脑直接编辑 NAS 上的代码文件保存后刷新浏览器就能看到效果整个调试流程跟本地开发几乎没有区别。这套 Docker 环境的好处就在于此——代码、配置、数据全在 NAS 上电脑换一台也丝毫不影响继续开发。
返回列表