Docker实战:从2048到LAMP,掌握容器部署与排障

发布时间:2026/9/9 10:06:13
Docker实战:从2048到LAMP,掌握容器部署与排障 上一节课我们把Docker的基础概念过了一遍镜像、容器、仓库、常用命令。这一节我想换个方式不再一个个命令讲语法而是直接上真实案例。用两个项目把手练熟——第一个是轻量级的game2048网页游戏让你在5分钟内感受一次打包、到处运行的滋味第二个是一套完整的LAMP/Web应用堆栈把Apache、PHP、MySQL全部容器化玩明白多容器协作、数据持久化和编排。这两个案例覆盖了日常开发里80%的容器操作拉镜像、起容器、端口映射、数据卷挂载、自定义网络、Compose编排。把这些跑通你再回去看那些枯燥的docker run参数会觉得每一条都有实际落点。后半部分我还会额外聊两个社区里高频踩坑的问题Java容器异常重启后JVM日志去哪了以及停止的容器和残留卷怎么清干净。这些是我在实际服务器上真刀真枪排障攒下的经验不写到文档里但比文档有用。1. 为什么拿game2048当第一个容器化应用1.1 一个网页游戏背后藏着的Docker核心操作选game2048不是因为它好玩——虽然确实挺上头——而是因为它足够简单。它是一个纯静态网页应用不需要数据库不需要后端服务只有一个Nginx或者Apache在跑返回一堆HTML、CSS、JavaScript。这种结构把一个容器化的核心链路压缩到了最短镜像从哪里来怎么运行一个容器宿主机和容器之间端口怎么打通容器怎么查看、停止、删除你看这一圈下来Docker日常最常用的操作基本全过了一遍。如果你一上来就部署微服务、Kafka集群这类重东西光排障就劝退一大半人。拿2048做教学还有一个隐含优势它的结果反馈极其直观。浏览器打开就能看到界面滑块能滑动分数能变化。你做对了它就是好的做错了页面直接打不开。这种所见即所得的正反馈对学习动力很重要不像数据库容器启动了半天你都不知道它到底起来没有还得去看日志。1.2 镜像、容器、仓库三者关系跑之前先想明白在敲第一条命令前我强烈建议先花两分钟把这张关系图刻在脑子里镜像Image是一个只读的模板你可以把它理解成一台电脑出厂时的系统镜像——里面预装了操作系统、运行时、应用代码但它是死的任何修改都不写入镜像本身。容器Container是镜像的运行实例。镜像每运行一次就在镜像上面套一层可写层你的修改、产生的新文件、运行时的状态全部落在这层可写层里。容器可以启动、停止、删除。仓库Registry是存放镜像的地方Docker Hub就是最大的公共仓库。docker pull从仓库拉镜像到本地docker run用本地镜像起容器。为什么必须先想明白这个因为这直接关系到后面几个高频错误的理解为什么我改了容器里的文件容器一删就全没了因为可写层跟着容器走容器删除可写层也被回收。所以后面部署LAMP的时候MySQL的数据必须挂载到宿主机目录或者数据卷里否则删了容器等于删了数据库。这个可写层生命周期概念是理解Docker一切行为的地基。2. 5分钟跑起game2048顺便把这几个参数吃透2.1 拉镜像与运行容器-d和-p到底在干嘛废话不多说直接开跑。先确认Docker已经装好、daemon正在运行docker version能看到Client和Server两段都返回版本号就说明环境OK。然后拉取2048镜像。我这边用的是jvanderl/2048很多教程也用它你在搜索时看到别的作者维护的2048镜像行为基本一致。docker pull jvanderl/2048这个镜像很小只有几十MB。拉完之后看一眼本地镜像列表docker images接下来是核心一步——运行容器docker run -d -p 8080:80 --name game2048 jvanderl/2048这一条命令里有两个参数是理解重点-d后台运行。不加这个容器会挂在前台你的终端会被应用的实时日志刷屏一旦你CtrlC退出容器也跟着停。加了-d容器在后台跑终端还给你。-p 8080:80端口映射。冒号左边是宿主机端口右边是容器内部端口。容器内部跑着一个Nginx监听80端口。但容器有自己独立的网络命名空间宿主机上是访问不到容器IP的必须通过-p把宿主机的8080端口和容器的80端口打通——就像你在小区门卫室留了个访客条外来访问到了8080门卫自动帮你把请求转交给80。跑完之后验证一下docker ps你会看到一列信息容器ID、镜像、创建时间、状态、端口映射、名称。0.0.0.0:8080-80/tcp就表示映射生效了。浏览器打开http://localhost:80802048的方块界面应该立刻出现在眼前。2.2 验证、查看日志、停止与清理起了容器不代表一切正常我习惯先用命令行验证再开浏览器curl -I http://localhost:8080看到HTTP响应码200说明Nginx正常返回页面。这时候再看一眼容器日志docker logs game2048能看到Nginx访问日志、启动日志。docker logs本质上是把容器内进程的stdout、stderr抓出来给你看。这个命令在排障时极其重要——后面部署复杂应用第一件事就是看docker logs。玩够了之后停止容器docker stop game2048注意stop只是优雅停止进程容器还在可写层数据也还在。再启动就docker start game2048。彻底不需要了才删docker rm game2048删的时候有个细节如果容器还在运行docker rm会报错需要加-f强制删但强制删的代价是没机会优雅关闭进程。我建议养成先stop再rm的习惯尤其是后面MySQL这类有状态服务优雅停机很重要。到这里第一个项目就完整跑通了。你实际操作的时间可能不超过5分钟。别觉得简单这5分钟里你已经完成了镜像拉取、容器生命周期管理、端口映射、日志查看这四类Docker操作。接下来我们上强度部署一个完整的LAMP应用。3. LAMP拆开再看为什么它比单容器复杂得多3.1 LAMP四个字母的职责边界LAMP是Linux Apache MySQL PHP的组合缩写是经典的Web应用架构。你写的PHP代码跑在Apache里Apache把动态请求交给PHP解释执行PHP遇到数据读写就去连MySQL最终生成的HTML页面由Apache返回给浏览器。容器化部署LAMP第一反应可能是我把这四个东西装进一个容器里不就得了能做但非常不建议。为什么因为容器设计哲学是一个容器只跑一个主进程。把Apache、PHP、MySQL全塞进一个容器你会立刻遇到几个现实问题日志混在一起Apache的访问日志、PHP的错误日志、MySQL的慢查询日志全在同一个容器里排查问题全靠肉眼硬翻。资源没法单独管MySQL吃内存Apache吃CPU挤在一起你没法给MySQL单独设内存上限。更新牵一发动全身MySQL要升版本整个容器得重建Apache和PHP也没法幸免。所以正确做法是拆成多个容器每个容器各自干好一件事容器之间通过网络通信。这也正是Docker最核心的使用方式——用单容器做单元用网络做胶水用数据卷做共享存储拼出完整应用。3.2 容器化后依赖变成了网络问题在传统环境里Apache和MySQL装在同一台机器上PHP代码用localhost:3306就能连上MySQL。容器化之后Apache和MySQL是两台独立的机器各自有IP。那问题来了PHP容器怎么知道MySQL容器的地址你有两种方案查MySQL容器的IP把它硬编码到项目配置里。这个方案极其脆弱——容器删了重建IP就变了配置立刻失效。把两个容器放进同一个自定义网络用容器名当主机名互相访问。这是正解。Docker内置了DNS解析功能只要两个容器在同一个自定义网络里直接ping mysql或者mysqli_connect(mysql, ...)就能通。这就好比你不用记朋友家的门牌号只要你们在同一个小区叫名字就能找到。后面我们的PHP代码连接数据库用的主机名就是mysql而不是一长串IP。4. 手动部署一套完整LAMP五个关键步骤4.1 建自定义网络让容器用名字互通在一台全新的机器上我先创建一个自定义网络。为什么不用默认的bridge网络因为默认bridge不提供基于容器名的DNS解析自定义网络才提供。命令很简单docker network create lamp-net创建完可以用docker network ls确认一下。以后这个网络里的所有容器都可以用容器名互相访问。4.2 先启动数据库把数据卷和初始化脚本准备好MySQL是整套应用的状态核心绝对不能裸跑。我先建一个数据卷用来持久化MySQL的数据文件docker volume create mysql-data然后准备初始化脚本。MySQL官方镜像有一个非常好用的机制/docker-entrypoint-initdb.d目录。容器首次启动时会自动按文件名顺序执行这个目录下的.sql、.sh文件。也就是说你只需要把建库建表的SQL准备好挂载进去MySQL第一次启动时帮你自动执行。先写一个初始化SQL这里给它起名为init.sqlCREATE DATABASE IF NOT EXISTS appdb; USE appdb; CREATE TABLE IF NOT EXISTS users ( id INT AUTO_INCREMENT PRIMARY KEY, username VARCHAR(50) NOT NULL, email VARCHAR(100) NOT NULL ); INSERT INTO users (username, email) VALUES (alice, aliceexample.com); INSERT INTO users (username, email) VALUES (bob, bobexample.com);然后启动MySQL容器docker run -d \ --name lamp-mysql \ --network lamp-net \ -e MYSQL_ROOT_PASSWORDroot123 \ -e MYSQL_DATABASEappdb \ -e MYSQL_USERappuser \ -e MYSQL_PASSWORDapppass \ -v mysql-data:/var/lib/mysql \ -v /absolute/path/to/init.sql:/docker-entrypoint-initdb.d/init.sql:ro \ mysql:8.0这几个-e环境变量是重点MYSQL_ROOT_PASSWORDroot用户的密码。MYSQL_DATABASE容器首次启动时自动创建的数据库名。MYSQL_USER/MYSQL_PASSWORD创建一个普通用户并授权访问MYSQL_DATABASE指定的库。生产环境绝不要用root去连业务数据库这是底线。-v mysql-data:/var/lib/mysql做了数据持久化/var/lib/mysql是MySQL存储数据文件的目录它被映射到mysql-data卷上。这样一来哪怕容器被删除数据也还在卷里。新容器只要挂载同一个卷数据就原样回来。关于首个容器启动耗时不要急MySQL初始化数据需要一点时间。用docker logs -f lamp-mysql盯着日志看到ready for connections就说明数据库就绪了。4.3 启动PHP/Apache容器并挂载站点代码PHP和Apache我选择合用一个官方镜像php:8.2-apache——这个镜像内置了Apache和PHP省得用两个容器再配置代理。但这引出一个必须处理的坑官方镜像默认不带mysqli扩展PHP代码连不上MySQL。需要自己写Dockerfile扩展。写一个简单的DockerfileFROM php:8.2-apache RUN docker-php-ext-install mysqli pdo pdo_mysql构建这个定制镜像docker build -t php-web:8.2 .构建过程会下载源码、编译扩展需要几分钟。构建成功后本地镜像列表里多了一个php-web:8.2。这个官方镜像为基础自己加扩展的做法是容器化应用最常见的镜像定制方式。你之后加Redis扩展、改PHP配置都是往Dockerfile里加指令。然后准备网站代码。新建一个site目录写一个index.php内容很简单目的就是打通三层链路?php $host lamp-mysql; $db appdb; $user appuser; $pass apppass; $conn new mysqli($host, $user, $pass, $db); if ($conn-connect_error) { die(连接失败: . $conn-connect_error); } echo 数据库连接成功br; $result $conn-query(SELECT id, username, email FROM users); while ($row $result-fetch_assoc()) { echo $row[id] . . $row[username] . . $row[email] . br; }注意第3行连接主机名直接写lamp-mysql就是刚才启动的MySQL容器名。因为两个容器都在lamp-net网络里这个名字会被自动解析成MySQL容器的IP。启动PHP/Apache容器docker run -d \ --name lamp-web \ --network lamp-net \ -p 8080:80 \ -v /absolute/path/to/site:/var/www/html \ php-web:8.2-p 8080:80把宿主机的8080映射到容器的80端口-v /absolute/path/to/site:/var/www/html把宿主机上的代码目录挂载到Apache的网站根目录。挂载代码目录的好处是改代码不用重建镜像宿主机文件一变容器内立刻生效。4.4 用一条SQL验证整个链路容器都起来之后验证一下docker ps应该看到两个容器在运行lamp-mysql和lamp-web同属lamp-net网络。浏览器访问http://localhost:8080看到页面上打印出数据库连接成功和两条用户数据说明整个链路——浏览器到ApacheApache执行PHP代码PHP连MySQLMySQL返回数据——全部打通了。这一步是整个LAMP部署的成败时刻。如果页面报连接失败我通常按这个顺序排查先查两个容器是不是在同一个网络里docker inspect lamp-web | grep NetworkMode再进PHP容器里试网络连通性docker exec -it lamp-web ping lamp-mysql不通就说明网络配置有问题然后看MySQL日志确认用户名密码有没有错docker logs lamp-mysql最后检查用户权限MySQL 8.0默认的caching_sha2_password认证插件如果PHP的mysqli扩展版本较老会认证失败。解决方式是在MySQL初始化时用mysql_native_password创建用户或者在init.sql里显式指定认证插件在这一步你大概率会踩一到两个坑。这很正常。排障就是不断缩小范围的过程先把网络层打通再看应用层层级不要乱。5. 用Docker Compose把整套LAMP固化下来5.1 compose文件怎么组织和docker run的对应关系手动敲了四条docker run之后你会发现一个问题这套部署流程没法轻松复现。换台机器得过一遍同样的手工操作还容易漏参数。这时候Docker Compose登场——用一份YAML文件描述整套应用的所有容器、网络、卷、依赖关系一条命令全部拉起。在项目目录下创建docker-compose.ymlversion: 3.8 services: db: image: mysql:8.0 container_name: lamp-mysql environment: MYSQL_ROOT_PASSWORD: root123 MYSQL_DATABASE: appdb MYSQL_USER: appuser MYSQL_PASSWORD: apppass volumes: - mysql-data:/var/lib/mysql - ./init.sql:/docker-entrypoint-initdb.d/init.sql:ro networks: - lamp-net web: build: . container_name: lamp-web ports: - 8080:80 volumes: - ./site:/var/www/html depends_on: - db networks: - lamp-net volumes: mysql-data: networks: lamp-net:这份文件把之前所有docker run参数翻译成了结构化配置。build: .告诉Compose用当前目录的Dockerfile构建镜像db和web两个service会被放进自动创建的lamp-net网络depends_on: - db声明了启动顺序——先起数据库再起Web。每条配置都能在之前的docker run命令里找到对应项。启动一条命令docker-compose up -dCompose会先构建web镜像再创建网络、数据卷然后依次启动容器。完成之后docker ps看到的结果和手动操作一模一样。这套配置的好处是提交到Git仓库任何同事clone下来一条命令就能得到一模一样的运行环境。这就是环境一致性最直接的体现。5.2 初始化数据的坑数据库卷已存在SQL脚本不会再执行用Compose部署最让我头疼的坑是MySQL的初始化脚本不会第二次执行。想想看你第一次docker-compose up -dMySQL容器创建mysql-data卷是空的容器执行init.sql建表插入数据一切正常。但后来你改动了init.sql比如加了一个字段然后执行docker-compose down docker-compose up -d你会发现新增的SQL根本没有执行。原因在于mysql-data卷里已经有数据了。MySQL镜像只在数据目录为空时才执行docker-entrypoint-initdb.d下的脚本数据目录一旦完成初始化这个机制就永久跳过。日志里会明确告诉你MySQL database directory is not empty, skipping initialization。解决方式分两种开发环境测试删除数据卷强制重新初始化。docker-compose down -v会连卷一起删掉再up -d就重新执行脚本了。但生产环境不要这么干数据全没。结构变更不要把SQL写在初始化脚本里而是等容器起来之后手动执行docker exec -i lamp-mysql mysql -u root -p migration.sql或者在应用启动时做数据库迁移。这个坑的本质是初始化和变更是两个生命周期的操作。初始化只在数据为空时发生一次后续一切变更都走平时运维流程。另外提醒一点depends_on只保证db容器先启动不保证MySQL进程已完成初始化。Web容器可能比MySQL更早开始尝试连接第一次连接失败很正常。所以应用层要有重试机制或者在Compose里用healthcheck让web真正等待db就绪后再启动。这一点在微服务架构里尤其重要。6. 容器重启后日志找不到JVM日志问题与容器残留清理6.1 Java容器异常重启JVM日志被吞掉了社区里关于docker容器部署的java程序异常重启后jvm日志在哪的问题特别多。这个问题的经典场景是Java应用容器运行了一段时间突然挂了Docker根据重启策略自动拉起了新容器。你想去老容器里拿JVM崩溃时生成的hs_err_pid*.log打开发现——没了。为什么会没因为JVM的crash日志默认写到进程的工作目录。在容器里这个工作目录在可写层上。容器被删除后可写层整个被回收crash日志随之消失。你连进容器里找都找不到。哪怕容器还在docker logs也只能看到打到stdout/stderr的日志。JVM的hs_err_pid日志默认是写文件的不进stdout所以docker logs里也看不到崩溃现场。正确做法是在启动命令里显式指定JVM日志路径并把它挂到宿主机java -XX:ErrorFile/logs/hs_err_pid_%p.log -XX:HeapDumpOnOutOfMemoryError -XX:HeapDumpPath/logs/heapdump.hprof -jar app.jar然后在容器启动时挂载日志目录docker run -v /data/logs:/logs ...这样JVM崩溃日志和堆转储文件落到宿主机目录里容器消失也不怕。再配合docker inspect查看容器退出状态比如ExitCode为137说明被OOM Killer杀掉143是收到SIGTERM对定位问题非常有帮助。这里插一个知识docker events命令可以实时看到容器生命周期事件包括创建、销毁、OOM kill等。如果你怀疑容器无缘无故重启先执行docker events再复现问题能把线索抓得更准。我遇到过不少案例排查到最后发现不是容器重启而是应用自己因为OOM被JVM内部杀掉和Docker没有直接关系。6.2 容器停了不等于删了docker ps -a里的残留和清理与日志丢失相反的另一类高频问题是清理残留。很多人发现服务器磁盘空间不够顺手查了一下docker ps明明没有在跑的容器但磁盘却占用很大。这就是因为容器停止后并没有消失。docker ps默认只显示运行中的容器。docker ps -a才会列出所有容器——包括运行中、已停止、已退出的。你每次docker run创建的容器在停止后它会原封不动留在那里可写层里的数据、日志文件、临时文件都占着磁盘。先看占了多少空间docker system df这个命令会列出镜像、容器、数据卷、构建缓存各自占用的磁盘空间非常直观。然后一键清理已停止的容器docker container prune按y确认所有非运行状态的容器会被清掉。类似的还有docker volume prune清理没有任何容器引用的数据卷。这个命令要谨慎——如果某个卷里有重要数据但暂时没有容器使用也会被删掉。我一般删卷之前先docker volume ls确认一遍。还有一个细节默认情况下docker rm删除容器时不会自动删除匿名卷这时用docker rm -v可以连同匿名卷一起删。说白了容器、镜像、数据卷是三块独立的存储资源你得分别管。养成定期执行docker system prune -a的习惯能释放大量空间但它会清掉所有不在运行中的镜像和容器用之前想清楚。说点我自己的实际感受。当年我刚开始学Docker的时候也被部署一套LAMP这种看着很高端的任务唬住过总觉得要懂很多后端知识才能动手。真拆完之后才发现容器的好处恰恰在于它把混乱的底层环境差异收敛成了几个简单概念——镜像、容器、网络、卷。弄懂这四个概念之间的关系比背一百条命令都管用。而排障时遇到的大部分问题也答案都藏在这四个概念的边界上数据写在哪、网络通不通、日志落在哪、资源有没有被清理。把这几个维度理清楚Docker就算真正上手了。这一节的内容到这里我不做总结了。两个案例的完整操作过程都在上面照着敲一遍你收获的远比读一遍多。最后补一句生产环境部署有状态应用一定要把数据卷的备份策略想好这比容器编排本身更考验功底。下节有缘的话聊聊镜像构建的优化思路。

关于本文作者

来自尧图内容编辑团队

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

尧图内容编辑团队

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

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

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

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

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

网站改版的5个关键决策

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

获取专属建站方案

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

立即免费咨询