Docker训练营:专业阶段
容器的存储
Docker 存储机制的核心目的,就是为了解决这个问题,让数据能够独立于容器的生命周期而持久化 (Persistent)。
Docker 提供了三种主要的存储机制:
- 默认存储方式
- 卷 (Volumes) - 官方最推荐的方式
- 绑定挂载 (Bind Mounts)
让我们通过一个 Nginx Web 服务器的例子来理解这三种方式的区别。我们将在每种方式下执行相同的操作:创建一个 HTML 文件,然后测试数据的持久性。
1. 默认存储方式:
默认存储下当一个容器被删除后(docker rm),它在运行期间产生的所有数据都会丢失。
# 运行一个 nginx 容器 |
2. 卷(volume)存储方式:
当一个容器被删除后(docker rm),它在运行期间产生的所有数据都会丢失。这对于应用本身来说是没问题的,但对于应用产生的重要数据(比如数据库文件、用户上传的图片、日志等)来说,这显然是不可接受的。
Docker 存储机制的核心目的,就是为了解决这个问题,让数据能够独立于容器的生命周期而持久化 (Persistent)。因此有了使用卷存储的方式,volume存储卷由 Docker 创建和管理,我们可以使用该docker volume create命令显式的创建卷,或者在容器创建时创建卷。
你可以把 卷 想象成一个由 Docker 自己管理的“专属U盘”。
工作原理:卷是一个由 Docker 创建和管理的特殊目录,它存在于宿主机的文件系统中(通常在 /var/lib/docker/volumes/ 目录下,但你不应该直接操作这个目录)。你可以把这个卷添加到任何一个容器中指定的路径上。
核心优势:
独立于容器:卷的生命周期与容器无关。即使删除了所有使用它的容器,卷和里面的数据依然存在。
易于管理:可以使用 docker volume 命令进行创建、查看、删除等操作,非常方便。
高性能:在 Linux 系统上,卷的读写性能非常高。
易于备份和迁移:可以轻松地备份、恢复或将卷迁移到其他 Docker 主机。
实践演示
# 创建一个 Docker volume |
3. 绑定挂载(Bind Mounts):
如果说“卷”是 Docker 管理的文件,那 绑定挂载 就是把你宿主机上的一个已有目录直接映射到容器里。
工作原理:将宿主机上的一个文件或目录,直接挂载到容器内的指定路径。你在宿主机上修改文件,容器内会立刻看到变化,反之亦然。
实践演示:在这个场景中,我们将主机上的目录直接挂载到容器中:
# 创建本地目录 |
卷和绑定挂载的区别?
简单来说,它们的核心区别在于数据由谁来管理:
- 卷 (Volume):由 Docker 在其专有存储区域(例如 Linux 上的
/var/lib/docker/volumes/
)创建和管理。你只需要关心卷的名字,而不需要关心它在主机上的具体位置。这是 Docker 官方推荐的方式。 - 绑定挂载 (Bind Mount):由用户指定主机上的一个文件或目录,直接映射到容器中。你完全控制数据在主机上的位置。
为了更清晰地理解,我们从多个维度进行详细对比:
详细对比:卷 vs. 绑定挂载
特性 | 卷 (Volumes) | 绑定挂载 (Bind Mounts) |
---|---|---|
管理者 | Docker | 用户 / 主机操作系统 |
主机位置 | Docker 管理的特定目录,对用户透明 | 用户指定的任意文件或目录路径 |
推荐场景 | 生产环境。数据库、用户上传文件、需要持久化的应用状态等。 | 开发环境。挂载源代码、共享配置文件等。 |
可移植性 | 高。不依赖主机特定的文件结构,方便迁移和备份。 | 低。强依赖于主机上存在的特定路径,不易迁移。 |
性能 | 在 Linux 上通常性能更好,Docker 可以进行内部优化。 | 在 macOS 和 Windows 上可能因文件系统同步而有 I/O 延迟。 |
安全性 | 更安全。数据与主机文件系统隔离,降低了容器影响主机的风险。 | 风险较高。容器默认有权修改主机文件系统,可能误删或修改重要文件。 |
初始化 | 如果将一个空的卷挂载到容器中非空的目录,Docker 会自动将容器目录中的内容复制到卷中。 | 直接用主机目录覆盖容器目录。如果主机目录是空的,容器目录也会变空。 |
管理命令 | 可以通过 Docker CLI (docker volume create/ls/rm ) 进行管理。 |
无法通过 Docker CLI 直接管理,需要用操作系统的命令 (mkdir , rm )。 |
总结:何时使用?
遵循一个简单的原则:
当你需要持久化应用数据,尤其是在生产环境中时,请优先使用卷 (Volume)。
- 例如:数据库文件 (
/var/lib/mysql
)、WordPress 的上传内容 (/var/www/html/wp-content
)、应用程序日志等。
- 例如:数据库文件 (
当你在开发环境中,需要将本地代码实时同步到容器中进行调试时,请使用绑定挂载 (Bind Mount)。
- 例如:将你的 Node.js/Python/Go 项目的源代码目录挂载到容器中。
当你需要将主机的特定配置文件共享给容器时,也可以使用绑定挂载。
- 例如:将自定义的
nginx.conf
文件挂载到 Nginx 容器中。
- 例如:将自定义的
Docker网络管理:
默认情况下,Docker 已经为你处理了大部分网络设置,所以单个容器可以轻松访问互联网。但是,当你开始运行由多个容器组成的应用(例如,一个 Web 应用容器 +一个数据库容器)时,理解 Docker 网络就变得至关重要。
Docker 通过“网络驱动 (Network Drivers)”来实现不同的网络模式。下面是主要的几种网络模式:
Bridge (桥接网络) - 最常用,默认模式
Host (主机网络)
None (无网络)
Overlay (覆盖网络) - 我们将在学习集群编排时再详细讨论它。
Docker 网络命令详解
docker网络管理的常用命令如下:
# 列出所有网络 |
Bridge网络:
你可以把 Bridge 网络 想象成一个虚拟的路由器,Docker 在你的电脑里创建了这个路由器,所有连接到这个网络的容器,都像是连接到了同一个局域网(LAN)中。
默认行为: 当 Docker 服务启动时,它会创建一个名为 bridge 的默认桥接网络。如果你在 docker run 时不指定任何网络,容器就会被连接到这个默认网络上。Docker 会从一个私有 IP 地址池(例如 172.17.0.x)中为每个容器分配一个 IP。
缺电: 在默认的 bridge 网络中,容器之间虽然可以通信,但只能通过它们难以预测的内部 IP 地址进行通信。不支持通过容器名进行通信。这对于构建应用来说非常不便。
因此为了为了克服默认网络的缺陷, 我们应该始终为我们的容器使用自定义的桥接网络。
使用自定义的桥接网络之后我们就不用考虑容器的ip可能会变化了,可以直接使用它们的容器名作为主机名进行通信。并且只有连接到同一个自定义网络上的容器可以通信,提供了更好的网络间的容器隔离。
实践示例:
# 先创建一个Bridge网络 |
Host网络:
Host 网络 是一种非常直接的模式,它跟Brideg网络相比放弃了容器的网络隔离性。
Host工作原理:使用 host 网络的容器,将不会拥有自己独立的网络空间,而是直接共享宿主机的网络。容器内监听的任何端口,都会直接暴露在宿主机的 IP 地址上。
实践:
# 使用 host 网络运行 Nginx |
不需要使用 -p 参数进行端口映射。我们可以直接在浏览器中访问 http://localhost:80(但或者如果 80 端口被占用,Nginx 会启动失败)
None网络:
连接到 none 网络的容器,就像是被拔掉了网线。它拥有自己的网络空间,但没有任何网络接口(除了一个本地回环 lo 接口)。它完全与外界隔离。
适用于那些不需要任何网络连接,只负责执行计算任务或文件处理的批处理作业。
Docker Compose
1. 从单容器到容器编排
在前面的课程中,我们学习了如何使用 Docker 容器来运行单个服务。
通过 docker run
命令,我们可以快速启动一个数据库、一个 Web 服务器或者一个缓存服务。
这种方式在开发简单应用时非常有效。然而,随着应用架构的演进,微服务的理念逐渐流行,一个应用可能由多个相互依赖的服务组成。
如果继续使用单容器管理方式,我们需要手动管理容器间的网络连接、存储卷映射、环境变量配置等,这不仅增加了运维的复杂度,还容易因手动操作而出错。
这就是为什么我们需要一个更高层次的工具来管理多容器应用。
Docker Compose 应运而生,它通过一个声明式的 YAML 配置文件,帮助我们定义和管理多容器应用。
通过 Docker Compose,我们可以用一个命令就完成整个应用的部署,而不需要手动管理每个容器。
2. Docker Compose 核心概念
Docker Compose 是一个用于定义和运行多容器 Docker 应用程序的工具。使用 Compose,你可以通过一个 YAML 文件来配置应用程序的所有服务,然后使用一个命令来创建和启动所有服务。
2.1 主要概念
- 服务 (Services): 容器的定义,包括使用哪个镜像、端口映射、环境变量等
- 网络 (Networks): 定义容器之间如何通信
- 卷 (Volumes): 定义数据的持久化存储
- 依赖关系 (Dependencies): 定义服务之间的启动顺序
- 环境变量 (Environment Variables): 管理不同环境的配置
2.2 核心命令
docker compose up
: 创建和启动所有服务docker compose down
: 停止和删除所有服务docker compose ps
: 查看服务状态docker compose logs
: 查看服务日志
YAML文件案例分析:
YAML文件代码:
# 指定 Docker Compose 文件格式的版本。'3.8' 是一个稳定且功能丰富的版本。 |
问1:当挂载卷的时候怎么样才能知道容器内的正确路径?
通过这么多例子练习下来我发现一个问题就是不知道挂载卷的时候的路径是怎么获得的,因此我上网搜索后得到了两种解决办法:
- 第一种:读官方文档 (最推荐、最标准)
对于所有官方或流行的第三方镜像(如 nginx, postgres, redis, mysql, wordpress 等),它们的作者都会在 Docker Hub 的镜像主页上明确写出推荐挂载的路径。这是最权威、最直接的方法。 - 第二种:进入容器内部进行探索 (最灵活、最常用)
如果文档和 Dockerfile 都找不到线索,或者你想确认一下,最直接的方法就是启动一个临时容器,然后亲自“走进去”看一看。
这是排查问题时最有用的技巧。
问2:端口映射port作用是什么:
默认情况下容器和宿主机的网络是隔离的,假设一个例子:我们在宿主机上启动了一个nginx服务,在浏览器访问localhost:80,在宿主机上是可以正常访问的。但是如果使用docker启动nginx,在宿主机上访问localhost:80,那么就会访问失败。
因此我们需要使用port来映射宿主机和容器内的端口,比如在run镜像时使用-p 80:80,这样就使得宿主机访问80端口时,会跳转到容器内的80端口,这样我们在宿主机上访问localhost:80,就可以访问到容器内的nginx服务。
专业阶段作业练习:
exercise1:
作业要求:
# 作业要求: |
首先我们先编写好一个yml框架:
services: |
之后按步骤依次解决,首先看第一个要求持久化MySQL数据,因此我们需要用到volume,我们需要yml中在先创建一个volume并添加进mysql服务中:
volumes: |
然后我们查看mysql的官方文档,可以找到持久化数据的路径是/var/lib/mysql,因此我们可以在docker-compose.yml中添加如下代码:
mysql: |
接着,我们来解决第三个要求:通过环境变量设置 MySQL root 密码。这同样是官方镜像支持的标准做法,可以提高安全性。我们使用 environment 关键字来定义环境变量。
mysql: |
现在,我们的服务还缺少一些基本的定义,比如使用哪个镜像、重启策略、端口映射等。我们来把它们补全。
image: 指定使用官方的 mysql:8.0 镜像。
restart: 设置为 unless-stopped,这是一个很好的习惯,意味着除非我们手动停止,否则容器总会在退出后自动重启。
ports: 将主机的 3306 端口映射到容器的 3306 端口,方便我们从本地连接数据库。
将这些补充完整后,我们就得到了最终的 docker-compose.yml 文件。
version: '3.8' |
成果验证:
最后,也是最关键的一步,我们需要验证我们的配置是否正确,特别是数据持久化是否生效。
第一步:启动服务
在项目根目录下打开终端,运行命令:
docker compose up -d |
服务将会在后台启动。
第二步:初次连接并验证初始化
我们需要进入容器内部的 MySQL 客户端来执行 SQL 命令。
# 使用我们在 yml 中定义的容器名进入容器 |
成功登录后,我们检查 testdb
是否被 init.sql
成功创建:
-- 在 mysql 命令行中执行 |
你应该可以在列出的数据库中看到 testdb
,这证明 init.sql
的挂载和自动执行是成功的。
第三步:创建测试数据
为了验证数据持久化,我们需要在数据库里创建一些数据。
-- 切换到 testdb 数据库 |
此时你应该能看到我们刚刚插入的“张三”这条记录。
第四步:模拟“灾难”,删除容器
现在我们来模拟容器因故被删除的场景。运行 down
命令会停止并删除容器。
docker compose down |
注意:这个命令不会删除我们定义的命名卷 mysql-data
,这正是我们期望的!
第五步:重新启动服务
再次启动服务,Docker Compose 会创建一个全新的 mysql_db
容器,但会重新挂载已经存在的 mysql-data
数据卷。
docker compose up -d |
第六步:最终验证
最后,我们再次连接到这个全新的容器,检查我们之前创建的数据是否还在。
docker compose exec mysql_db mysql -u root -p |
登录后,直接查询数据:
-- 切换数据库 |
如果此时你仍然能看到“张三”那条记录,那就完美地证明了数据持久化是成功的! 我们的数据被安全地保存在了 mysql-data
卷中,并没有因为容器的删除而丢失。
exercise2: 使用 Docker Compose 部署 Python Web 应用与 Redis
1. 作业要求
使用 Python Flask 作为 Web 框架,Redis 作为内存数据库,来实现一个简单的计数器应用。
作业要求:
# 作业目标: |
2. 应用开发与容器化
第一步:编写 Python 应用代码
与之前的作业不同,这次需要先准备好 Python 应用本身以及两个核心文件:requirements.txt
用于声明依赖,app.py
是应用主程序。
不过这两个文件在项目中已经提前为我们写好了,在本次作业中我们只需要编写Dockerfile文件,学习掌握docker相关的知识就可以了。
第二步:为 Python 应用编写 Dockerfile
接下来,根据作业要求,为这个 Flask 应用编写了 Dockerfile
。
# 选择一个官方的、精简的 Python 镜像作为基础 |
这里有一个非常重要的学习点:CMD
命令中的 --host=0.0.0.0
。Flask 默认只在 localhost
(127.0.0.1) 上监听,这意味着它只接受来自容器内部的连接。为了让它可以接收来自 Docker 网络中其他容器(如 Nginx 或我们直接从主机映射的端口)的连接,必须将其绑定到 0.0.0.0
,代表监听所有网络接口。
3. 使用 Docker Compose 进行编排
完成了应用的容器化之后,我开始编写 docker-compose.yml
文件来定义和连接 web
和 redis
两个服务。
最终的 docker-compose.yml
文件:
version: '3.8' |
在这个配置中,重点是容器间的网络:
- 自定义网络
mynet
:将web
和redis
两个服务都连接到这个网络,实现了它们之间的隔离和通信。web
服务可以通过服务名redis
直接访问到 Redis 容器。 - Redis 数据持久化:与 MySQL 类似,我为 Redis 也创建了一个命名卷
redis_data
,并将其挂载到 Redis 容器内的数据存储目录 (/data
),以确保计数值在容器重启后不会丢失。
4. 验证学习成果
所有文件准备就绪后,我开始了最终的测试。
第一步:启动服务
docker compose up -d --build |
第二步:测试计数功能
我打开终端,多次使用 curl
命令访问 /ping
接口。
curl http://localhost:5000/ping |
计数值正常累加,证明 web
服务和 redis
服务之间的通信是成功的。
第三步:验证数据持久化
为了确认 Redis 的数据也能持久化,我模拟了一次服务下线和重启。
# 停止并移除容器 |
服务重启后,我再次访问 /ping
接口:
curl http://localhost:5000/ping |
计数器从上次停止的值 2
继续累加到了 3
,这完美地证明了 Redis 的数据通过 redis_data
卷被成功地持久化了。
exercise3: 使用 Docker Compose 编排多服务 Web 应用 (Nginx + Go + MySQL)
1. 作业背景与要求
在完成了单个服务的容器化部署后,下一个是挑战一个更接近真实世界场景的作业:将一个 Web 应用栈(反向代理、后端应用、数据库)通过 Docker Compose 进行编排。
作业要求:
# 作业要求: |
2. 方案设计与文件结构
根据要求,设计了一个经典的三层架构:
- Nginx (nginx): 作为前端的反向代理和流量入口,负责接收所有外部请求,并将其转发给后端的 Go 应用。
- 应用服务 (app): 使用 Golang 编写的后端服务,负责处理业务逻辑(计数器功能),并与数据库交互。
- 数据库 (mysql): 负责存储计数器的数据,并实现持久化。
为了组织好这个项目,我创建了如下的目录结构:
/exercise2 |
3. 分步实现
第一步:后端应用 app
的容器化
因为要求需要我为它编写了 Dockerfile
,这里我学习并使用了一个重要的优化技巧——多阶段构建(multi-stage build)。
- 构建阶段 (builder): 我先使用一个包含完整 Go 开发环境的
golang:1.21-alpine
镜像来编译我的代码,生成一个静态的二进制可执行文件。 - 最终阶段 (final): 然后,我使用一个极度精简的
alpine:latest
基础镜像,只将上一个阶段编译好的二进制文件复制进来。
这样做的好处是,最终的应用镜像非常小,不包含任何编译工具和源代码,更加安全和轻量。
app/Dockerfile
文件内容:
# ---- Stage 1: Build ---- |
第二步:配置 Nginx 反向代理
Nginx 的职责是作为“前台接待”,将来自外部的请求(例如 http://localhost:8080/count
)转发给内部的 app
服务。我在 nginx/nginx.conf
文件中定义了这个规则。
nginx/nginx.conf
文件内容:
events { |
第三步:编写核心编排文件 docker-compose.yml
这是将所有服务串联起来的“总指挥”。我定义了 nginx
, app
, mysql
三个服务,并将它们连接到同一个自定义网络 app-network
。
在这个过程中,我遇到了很多问题,并通过不断修正,最终得到了一个可以成功build的版本。
最终的 docker-compose.yml
文件:
# docker-compose.yml (格式修正版) |
4. 调试与排错:
在实践中,一次性写对所有配置是很难的。我遇到了一系列典型的问题,解决它们的过程让我学到了更多。
问题一:Nginx 启动失败 (
Exited (1)
)- 现象:
nginx
容器启动后立即退出。 - 排查: 通过
docker compose logs nginx
查看日志,发现报错信息是[emerg] "server" directive is not allowed here
。 - 原因: 问AI回答说:我最初的
nginx.conf
文件缺少了顶层的events {}
和http {}
块,不符合 Nginx 的主配置文件格式。 - 收获: 学会了通过日志定位 Nginx 配置语法错误。
- 现象:
问题二:MySQL 重启循环 (
Restarting (1)
)- 现象:
mysql
容器不停地重启。 - 排查:
docker compose logs mysql
日志显示Database is uninitialized and password option is not specified
。 - 原因:
docker-compose.yml
文件中mysql
服务的environment
部分使用了不标准的列表格式 (- KEY=VALUE
),导致环境变量没有被正确传递。 - 收获: 掌握了 Docker Compose 中定义环境变量的标准字典格式 (
KEY: VALUE
),理解了 YAML 语法的重要性。
- 现象:
问题三:502 Bad Gateway (最终的难题)
- 现象: 所有服务都显示
running
,但访问localhost:8080
时 Nginx 返回 502 错误。 - 排查: 这是一个复杂的过程。
- 我首先检查了
app
服务的日志,发现它声称在3000
端口上运行正常。 - 然后我检查了
nginx
日志,发现有Connection refused
错误。 - 最终,我使用了
docker compose exec nginx curl http://app:3000/count
这个强大的诊断命令,直接从 Nginx 容器内部测试app
服务,发现它是健康的!
- 我首先检查了
- 原因: 这排除了所有可能,最终发现还是
docker-compose.yml
中app
服务的environment
格式依然是错误的,导致它无法连接数据库,Web 服务处于“假死”状态。 - 收获: 学会了如何系统性地调试多服务应用。当外部访问失败时,要深入到容器网络内部进行测试,以精确定位故障点。
- 现象: 所有服务都显示
5. 最终验证
在应用了所有修正和 healthcheck
之后,我执行了最终的启动和测试流程。
清理并启动
docker compose down -v
docker compose up -d --build等待服务就绪
通过docker compose ps
可以观察到服务从starting
到healthy
的过程。测试计数功能
curl http://localhost:8080/count