OpenEuler 上 Docker Compose 部署 LNMP 实战指南

发布时间:2026/10/27 3:26:43
OpenEuler 上 Docker Compose 部署 LNMP 实战指南 1. 为什么要在 OpenEuler 上用 docker-compose 搭 LNMPLNMP 这套组合——Linux、Nginx、MySQL、PHP——在国内的 Web 后端圈子里几乎是绕不开的经典架构。传统做法是在一台物理机或者虚拟机上逐个装软件、配依赖、改配置文件装完一轮下来系统里散落着几十个包和一堆说不清来源的配置项换台机器重来一遍又得从头踩坑。容器化解决的正是这个环境不可复现的老大难问题而 docker-compose 则是在单机场景下把多个容器编排起来最轻量、最顺手的方式。OpenEuler 作为一款面向服务器场景的 Linux 发行版在包管理、内核调优、安全策略上和常见的发行版有一些差异尤其是它默认启用的安全加固策略会让不少习惯了其他发行版的人第一次部署时卡在权限和端口上。这篇内容就是围绕在 OpenEuler 上用 docker-compose 把 LNMP 跑起来这件事把方案选型的逻辑、每个配置项背后的原因、实操中真正会遇到的坑一次讲透。适合谁来读如果你已经对 Linux 基本命令不陌生知道 Nginx 是干什么的、MySQL 怎么连但还没系统地把 LNMP 容器化落地过那这篇正好对路。如果你是完全的新手也能跟着步骤走下来只是中间涉及的一些原理需要你多花点时间消化。整套方案的目标很明确一条docker-compose up -d命令拉起 Nginx、PHP-FPM、MySQL 三个服务配置文件全部落在宿主机上可版本管理数据持久化不丢重启机器后能自动恢复。我先把整体思路摆出来再往下拆细节。之所以选 docker-compose 而不是 Kubernetes是因为单机 LNMP 的规模根本用不上 K8s 那套调度体系compose 用一个 YAML 文件描述服务关系、网络、卷挂载学习成本和维护成本都低一个数量级。之所以选官方镜像而不是自己从 Dockerfile 构建是因为官方镜像的维护频率高、安全补丁及时自己构建反而容易在基础镜像上引入不必要的维护负担——当然 PHP 需要额外装扩展的情况除外这个后面会讲怎么处理。2. 整体架构设计与方案选型拆解2.1 三个容器如何分工与通信LNMP 拆成容器后最核心的问题是谁和谁说话、怎么说话。我的设计是三个独立容器nginx负责接收 HTTP 请求和静态资源php跑 PHP-FPM 处理动态脚本mysql存数据。Nginx 收到.php结尾的请求时不自己处理而是通过 FastCGI 协议转发给 PHP-FPM 的 9000 端口PHP 需要读写数据库时通过 MySQL 的 3306 端口连接。这里有个关键点容器之间不能用localhost互相访问。每个容器有自己独立的网络命名空间localhost指向的是容器自己。docker-compose 默认会创建一个 bridge 网络把同一个 compose 文件里的服务都接进去服务名直接就是可解析的主机名。也就是说PHP 容器里连数据库主机名写mysql就行Nginx 转发给 PHP 时写php:9000就行。这个机制是靠 Docker 内置的 DNS 实现的比手动配 IP 靠谱得多容器重建后 IP 变了也不影响。我见过不少人图省事用network_mode: host让所有容器共享宿主机网络这样确实能用localhost但端口冲突、服务隔离全部失效等于把容器当成了进程管理器用失去了容器化的大半意义。所以我的方案坚持用自定义 bridge 网络服务间靠服务名通信。2.2 数据持久化为什么必须做容器的一个基本特性是无状态——容器删了里面写的数据就没了。MySQL 的数据文件、Nginx 的网站根目录、PHP 的配置文件这些都必须落在宿主机上通过 volume 或 bind mount 挂进容器。我的做法是MySQL 数据用命名卷named volume交给 Docker 管理避免宿主机文件权限和容器内用户 UID 不匹配的麻烦网站代码和 Nginx/PHP 配置用 bind mount直接映射到宿主机目录方便用编辑器改、用 Git 管。这个区分不是随便定的。MySQL 的数据目录对文件权限极其敏感如果 bind mount 到宿主机容器内 mysql 用户UID 通常是 999和宿主机目录属主对不上启动时就会报权限错误。命名卷由 Docker 自己管理权限省心。而网站代码和配置文件需要人频繁修改bind mount 才能让改动即时生效不用进容器操作。2.3 镜像版本怎么选才不踩坑镜像 tag 的选择直接决定了后续维护的难度。我的原则是生产环境永远不用latest。latest意味着你每次pull都可能拿到一个行为不同的新版本某天重启容器突然挂了排查半天发现是上游镜像更新导致的这种亏吃过一次就记住了。具体到这套方案Nginx 用nginx:1.24-alpinealpine 版本体积小、攻击面小MySQL 用mysql:8.08.0 是目前的主流稳定版本5.7 已经停止维护PHP 用php:8.2-fpm-alpine8.2 是长期支持版本性能也比 7.x 有明显提升。这些版本号写死在 compose 文件里什么时候升级由你决定而不是被上游牵着走。提示alpine 镜像用的是 musl libc 而不是 glibc绝大多数 PHP 扩展没问题但个别依赖 glibc 的二进制扩展可能装不上。如果遇到这种情况把基础镜像换成php:8.2-fpmDebian 版即可代价是镜像大一些。2.4 OpenEuler 环境下的特殊考量OpenEuler 默认启用了 firewalld 和 SELinux 类似的强制访问控制策略这两样是容器部署时最容易卡住的地方。firewalld 会拦截 Docker 映射到宿主机的端口导致容器起来了但外部访问不通强制访问控制策略则可能阻止容器进程读取 bind mount 进来的宿主机文件。我的处理方式是部署前先确认 firewalld 状态把需要暴露的端口80、3306 如果对外的话放行或者临时把 Docker 所在的网段加入信任区域。至于访问控制bind mount 的目录要确保容器内进程有读权限必要时用:z或:Z挂载选项让 Docker 自动处理标签。这些细节在后面的实操章节会具体展开。3. 环境准备与 Docker 安装实操3.1 系统基础检查与依赖确认动手之前先摸清家底。登录 OpenEuler 后先确认系统版本和内核cat /etc/os-release uname -rOpenEuler 的版本号决定了后续软件源的选择内核版本则关系到 Docker 的兼容性——一般 4.x 以上的内核都没问题。接着检查 firewalld 和已有的容器运行时systemctl status firewalld which docker podmanOpenEuler 部分版本预装了 podman 而不是 docker两者命令有差异如果发现是 podman要么卸载后装 docker要么改用 podman-compose但本文的方案基于 docker 和 docker-compose所以建议统一到 docker 上。卸载 podman 前确认没有正在运行的容器依赖它。3.2 安装 Docker 引擎与 compose 插件OpenEuler 的软件源里可能没有最新版 Docker我习惯用官方仓库安装版本可控。先装依赖工具dnf install -y dnf-utils device-mapper-persistent-data lvm2然后添加 Docker 仓库。这里要注意不同网络环境下仓库地址的可达性不同如果官方源拉取慢可以换成国内镜像站提供的仓库地址具体地址以你所在环境能访问的为准。添加仓库后执行安装dnf install -y docker-ce docker-ce-cli containerd.io docker-compose-plugin装完后启动服务并设置开机自启systemctl enable --now docker systemctl status docker确认 Docker 正常后验证 compose 插件docker compose version注意新版 Docker 把 compose 做成了插件命令是docker compose中间空格而不是老的docker-compose连字符。两者功能基本一致但插件方式随 Docker 一起升级更省心。如果你习惯老命令可以额外装一个软链接但没必要。3.3 配置镜像加速与存储驱动拉取镜像的速度直接决定部署体验。编辑/etc/docker/daemon.json配置镜像加速地址和日志轮转策略{ registry-mirrors: [你的加速地址], log-driver: json-file, log-opts: { max-size: 100m, max-file: 3 } }日志轮转这一项很多人忽略结果容器跑几个月把磁盘写满服务莫名其妙挂掉。max-size和max-file限制单个日志文件 100MB、最多保留 3 个足够排查问题又不至于撑爆磁盘。改完配置重启 Dockersystemctl daemon-reload systemctl restart docker存储驱动方面OpenEuler 上 Docker 默认用 overlay2一般不需要改。可以用docker info | grep Storage确认一下如果是 overlay2 就保持默认。3.4 目录结构规划在宿主机上建一套清晰的目录结构后面所有配置和数据都往里放mkdir -p /opt/lnmp/{nginx/conf.d,nginx/logs,php/conf.d,mysql/conf,www}这套结构对应关系是nginx/conf.d放站点配置nginx/logs放访问和错误日志php/conf.d放 PHP 的额外配置mysql/conf放 MySQL 配置www是网站根目录。全部放在/opt/lnmp下备份和迁移时整个目录打包带走即可。注意www目录的属主建议设成容器内 PHP-FPM 运行用户的 UID。alpine 版 PHP 镜像里 www-data 的 UID 通常是 82Debian 版是 33。如果文件属主不对PHP 写文件时会报权限拒绝。可以用chown -R 82:82 /opt/lnmp/www处理以 alpine 为例。4. docker-compose 编排文件逐项拆解4.1 服务定义与网络配置compose 文件是整个方案的核心我把它拆成几块讲。先看整体骨架services: nginx: image: nginx:1.24-alpine container_name: lnmp-nginx ports: - 80:80 volumes: - ./www:/var/www/html:ro - ./nginx/conf.d:/etc/nginx/conf.d:ro - ./nginx/logs:/var/log/nginx depends_on: - php networks: - lnmp-net restart: unless-stoppedports把宿主机 80 映射到容器 80外部才能访问。volumes里网站目录挂成只读:ro因为 Nginx 只需要读静态文件不需要写只读挂载能降低被篡改的风险日志目录要可写所以不加:ro。depends_on保证启动顺序Nginx 在 PHP 之后启动避免 Nginx 起来时后端还没就绪导致 502。restart: unless-stopped让容器异常退出后自动拉起但手动 stop 的不再重启这个策略比always更符合预期。网络部分在文件末尾统一定义networks: lnmp-net: driver: bridge所有服务都接入lnmp-net服务名即主机名。4.2 PHP-FPM 服务的配置要点PHP 服务的配置比 Nginx 复杂一些因为涉及扩展安装和用户权限php: image: php:8.2-fpm-alpine container_name: lnmp-php volumes: - ./www:/var/www/html - ./php/conf.d:/usr/local/etc/php/conf.d networks: - lnmp-net restart: unless-stopped网站目录在 PHP 这边是可写的不加:ro因为 PHP 可能要生成缓存文件、上传文件。php/conf.d挂进去后里面放的.ini文件会被自动加载调 PHP 参数不用进容器改。官方 PHP 镜像默认只带少量扩展连 MySQL 的pdo_mysql都没有必须自己装。有两种做法一是在 compose 里用command临时装但每次重启都重装不优雅二是写个 Dockerfile 基于官方镜像构建。我选后者FROM php:8.2-fpm-alpine RUN docker-php-ext-install pdo_mysql mysqli opcachedocker-php-ext-install是官方镜像提供的脚本自动处理编译依赖。pdo_mysql和mysqli是连 MySQL 必需的opcache能显著提升 PHP 执行效率。构建命令docker build -t my-php:8.2 ./php然后在 compose 里把image换成my-php:8.2。这样扩展固化在镜像里重启不丢。4.3 MySQL 服务的持久化与初始化MySQL 的配置重点在数据卷和环境变量mysql: image: mysql:8.0 container_name: lnmp-mysql environment: MYSQL_ROOT_PASSWORD: 你的强密码 MYSQL_DATABASE: appdb MYSQL_USER: appuser MYSQL_PASSWORD: 你的应用密码 volumes: - mysql-data:/var/lib/mysql - ./mysql/conf:/etc/mysql/conf.d networks: - lnmp-net restart: unless-stoppedMYSQL_ROOT_PASSWORD是必须的不设容器直接退出。MYSQL_DATABASE、MYSQL_USER、MYSQL_PASSWORD三个配合容器首次启动时自动建库建用户并授权省去手动进容器执行 SQL 的步骤。注意这些初始化只在数据目录为空时执行一次如果卷里已有数据改环境变量不会生效。数据卷mysql-data在文件末尾声明volumes: mysql-data:用命名卷而不是 bind mount前面说过是为了规避权限问题。MySQL 8.0 默认的认证插件是caching_sha2_password老版本的 PHP 客户端可能不兼容如果遇到连接报错可以在mysql/conf下放一个配置文件调整[mysqld] default-authentication-pluginmysql_native_password character-set-serverutf8mb4 collation-serverutf8mb4_unicode_ci字符集设成utf8mb4是必须的否则中文和 emoji 存进去会乱码。4.4 Nginx 站点配置与 FastCGI 转发Nginx 的站点配置放在nginx/conf.d/default.confserver { listen 80; server_name _; root /var/www/html; index index.php index.html; 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_pass php:9000里的php就是 compose 里的服务名Docker DNS 会解析成 PHP 容器的 IPSCRIPT_FILENAME必须正确指向容器内的文件路径写错了 PHP 会报 File not foundtry_files让不存在的路径回落到index.php适配大多数 PHP 框架的路由。注意fastcgi_param里如果漏了SCRIPT_FILENAME或者路径和root对不上是 502 和 404 的高发原因。排查时先看 Nginx 错误日志再看 PHP-FPM 日志两边对照基本能定位。5. 启动、验证与常见故障排查5.1 一键启动与状态确认所有文件就位后在 compose 文件所在目录执行docker compose up -d-d是后台运行。首次执行会拉镜像、建网络、起容器耗时取决于网络。启动后确认状态docker compose ps三个服务都应该是Up状态。如果某个服务反复重启用docker compose logs 服务名看日志。MySQL 首次启动初始化数据目录需要十几秒期间健康检查可能失败属正常现象。验证 PHP 和 MySQL 连通性在www下建个info.php?php phpinfo(); $pdo new PDO(mysql:hostmysql;dbnameappdb, appuser, 你的应用密码); echo $pdo ? DB OK : DB FAIL;浏览器访问服务器 IP能看到 PHP 信息页且输出DB OK说明整条链路通了。测完记得删掉这个文件phpinfo暴露的信息太多不能留在生产环境。5.2 端口不通与权限拒绝的排查外部访问不通先确认容器端口映射docker compose ps看 PORTS 列有没有0.0.0.0:80-80/tcp。有映射但访问不了多半是 firewalld 拦了firewall-cmd --list-ports firewall-cmd --add-port80/tcp --permanent firewall-cmd --reloadPHP 报权限拒绝检查www目录属主和容器内用户 UID 是否一致前面提过用chown处理。Nginx 报 403除了权限还要看目录的执行位x目录没有x权限 Nginx 进不去。MySQL 连不上先确认 PHP 容器里能不能解析mysql主机名docker compose exec php ping mysql。能 ping 通但连不上看 MySQL 日志里有没有认证插件相关的报错按 4.3 节调整认证方式。5.3 常见问题速查表现象可能原因处理方式502 Bad GatewayPHP 容器未启动或 fastcgi_pass 地址错查 PHP 容器状态核对服务名404 File not foundSCRIPT_FILENAME 路径与 root 不一致检查 Nginx 配置中 root 和 fastcgi_param数据库连接超时服务不在同一网络或主机名写错确认 compose 网络配置用服务名连接容器启动即退出环境变量缺失如 MySQL 密码查docker compose logs定位缺失项中文乱码字符集非 utf8mb4调整 MySQL 配置并重建库磁盘占满日志未轮转配置 daemon.json 日志限制5.4 几个踩过的坑与实操心得第一个坑是镜像拉取超时。OpenEuler 环境下如果没配镜像加速拉 MySQL 这种大镜像经常卡住配好registry-mirrors后顺畅很多。第二个坑是PHP 扩展装完不生效原因是改了 Dockerfile 但 compose 里还引用旧镜像 tagdocker compose up -d不会自动重建镜像得先docker build再up或者加--build参数。第三个坑比较隐蔽MySQL 数据卷初始化后改密码无效。有人改了 compose 里的MYSQL_ROOT_PASSWORD重启发现密码没变因为初始化只在空数据目录时执行。要改密码得进容器用 SQL 改或者删卷重建数据会丢慎用。第四个坑是bind mount 的文件改了容器里不生效这通常是编辑器保存时用了原子写入先写临时文件再重命名导致挂载的 inode 变了容器还指着旧 inode。解决办法是重启对应容器或者改用目录挂载而非单文件挂载。我个人在实际操作中的体会是这套方案最值得坚持的两点是版本号写死和配置全部落盘。前者让你对升级有完全的控制权后者让整套环境可以像代码一样被 review 和回滚。容器化最大的价值不是跑起来方便而是跑起来之后还能被可靠地管理和复现这两点做到了LNMP 的容器化才算真正落地。后续如果要扩展比如加 Redis 做缓存、加 phpMyAdmin 做管理界面无非是在 compose 里多写两个服务、多接一个网络骨架不用动。

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询