容器的存储

Docker 存储机制的核心目的,就是为了解决这个问题,让数据能够独立于容器的生命周期而持久化 (Persistent)

Docker 提供了三种主要的存储机制:

  1. 默认存储方式
  2. 卷 (Volumes) - 官方最推荐的方式
  3. 绑定挂载 (Bind Mounts)

让我们通过一个 Nginx Web 服务器的例子来理解这三种方式的区别。我们将在每种方式下执行相同的操作:创建一个 HTML 文件,然后测试数据的持久性。

1. 默认存储方式:

默认存储下当一个容器被删除后(docker rm),它在运行期间产生的所有数据都会丢失。

# 运行一个 nginx 容器
docker run -d --name web-default -p 8000:80 nginx

# 在容器中创建一个测试页面
docker exec -it web-default sh -c 'echo "<h1>Hello from Default Storage</h1>" > /usr/share/nginx/html/index.html'

# 访问页面验证内容
curl http://localhost:8000

# 删除容器
docker rm -f web-default

# 用同样的配置重新运行容器
docker run -d --name web-default -p 8000:80 nginx

# 再次访问页面,会看到默认的 Nginx 欢迎页面,之前的内容已经丢失
curl http://localhost:8000

2. 卷(volume)存储方式:

当一个容器被删除后(docker rm),它在运行期间产生的所有数据都会丢失。这对于应用本身来说是没问题的,但对于应用产生的重要数据(比如数据库文件、用户上传的图片、日志等)来说,这显然是不可接受的。
Docker 存储机制的核心目的,就是为了解决这个问题,让数据能够独立于容器的生命周期而持久化 (Persistent)。因此有了使用卷存储的方式,volume存储卷由 Docker 创建和管理,我们可以使用该docker volume create命令显式的创建卷,或者在容器创建时创建卷。
你可以把 卷 想象成一个由 Docker 自己管理的“专属U盘”。

工作原理:卷是一个由 Docker 创建和管理的特殊目录,它存在于宿主机的文件系统中(通常在 /var/lib/docker/volumes/ 目录下,但你不应该直接操作这个目录)。你可以把这个卷添加到任何一个容器中指定的路径上。

核心优势:

  • 独立于容器:卷的生命周期与容器无关。即使删除了所有使用它的容器,卷和里面的数据依然存在。

  • 易于管理:可以使用 docker volume 命令进行创建、查看、删除等操作,非常方便。

  • 高性能:在 Linux 系统上,卷的读写性能非常高。

  • 易于备份和迁移:可以轻松地备份、恢复或将卷迁移到其他 Docker 主机。
    实践演示

# 创建一个 Docker volume
docker volume create nginx_data

# 运行 Nginx 容器并挂载卷
docker run -d --name web-volume -p 8081:80 -v nginx_data:/usr/share/nginx/html nginx

# 在容器中创建一个测试页面
docker exec -it web-volume sh -c 'echo "<h1>Hello from Volume Storage</h1>" > /usr/share/nginx/html/index.html'

# 访问页面验证内容
curl http://localhost:8081

# 删除容器
docker rm -f web-volume

# 用同样的配置重新运行容器
docker run -d --name web-volume-2 -p 8081:80 \
-v nginx_data:/usr/share/nginx/html nginx

# 再次访问页面,内容仍然存在
curl http://localhost:8081

# 查看卷的详细信息
docker volume inspect nginx_data

3. 绑定挂载(Bind Mounts):

如果说“卷”是 Docker 管理的文件,那 绑定挂载 就是把你宿主机上的一个已有目录直接映射到容器里。
工作原理:将宿主机上的一个文件或目录,直接挂载到容器内的指定路径。你在宿主机上修改文件,容器内会立刻看到变化,反之亦然。
实践演示:在这个场景中,我们将主机上的目录直接挂载到容器中:

# 创建本地目录
mkdir nginx-content
echo "<h1>Hello from Bind Mount Storage</h1>" > nginx-content/index.html

# 运行 Nginx 容器并挂载本地目录
docker run -d --name web-bind \
-p 8082:80 \
-v $(pwd)/nginx-content:/usr/share/nginx/html nginx

# 访问页面验证内容
curl http://localhost:8082

# 在主机上修改文件
echo "<h1>Updated content from host</h1>" > nginx-content/index.html

# 无需重启容器,直接访问更新后的内容
curl http://localhost:8082

# 删除容器
docker rm -f web-bind

# 用同样的配置重新运行容器
docker run -d --name web-bind-2 -p 8082:80 \
-v $(pwd)/nginx-content:/usr/share/nginx/html nginx

# 再次访问页面,内容仍然存在
curl http://localhost:8082

卷和绑定挂载的区别?

简单来说,它们的核心区别在于数据由谁来管理

  • 卷 (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网络管理的常用命令如下:

# 列出所有网络
docker network ls

# 检查网络详情
docker network inspect <network-name>

# 创建自定义网络
docker network create [options] <network-name>

# 将容器连接到网络
docker network connect <network-name> <container-name>

# 断开容器与网络的连接
docker network disconnect <network-name> <container-name>

# 删除网络
docker network rm <network-name>

# 删除所有未使用的网络
docker network prune

Bridge网络:

你可以把 Bridge 网络 想象成一个虚拟的路由器,Docker 在你的电脑里创建了这个路由器,所有连接到这个网络的容器,都像是连接到了同一个局域网(LAN)中。

  • 默认行为: 当 Docker 服务启动时,它会创建一个名为 bridge 的默认桥接网络。如果你在 docker run 时不指定任何网络,容器就会被连接到这个默认网络上。Docker 会从一个私有 IP 地址池(例如 172.17.0.x)中为每个容器分配一个 IP。

  • 缺电: 在默认的 bridge 网络中,容器之间虽然可以通信,但只能通过它们难以预测的内部 IP 地址进行通信。不支持通过容器名进行通信。这对于构建应用来说非常不便。

因此为了为了克服默认网络的缺陷, 我们应该始终为我们的容器使用自定义的桥接网络。
使用自定义的桥接网络之后我们就不用考虑容器的ip可能会变化了,可以直接使用它们的容器名作为主机名进行通信。并且只有连接到同一个自定义网络上的容器可以通信,提供了更好的网络间的容器隔离。
实践示例:

# 先创建一个Bridge网络

docker network create my-app-network

# 我们启动一个容器并将其连接到我们的新网络
docker run -d \
--name my-postgres-db \
--network my-app-network \
-e POSTGRES_PASSWORD=mysecretpassword \
postgres:13

# 现在,假设我们有一个应用需要连接数据库。我们可以在启动它时,直接使用 my-postgres-db 这个名字作为数据库的主机地址!
# 为了演示,我们使用一个带有网络工具的临时容器 (nicolaka/netshoot) 来 ping 我们的数据库。
docker run --rm -it \
--network my-app-network \
nicolaka/netshoot ping my-postgres-db

Host网络:

Host 网络 是一种非常直接的模式,它跟Brideg网络相比放弃了容器的网络隔离性。
Host工作原理:使用 host 网络的容器,将不会拥有自己独立的网络空间,而是直接共享宿主机的网络。容器内监听的任何端口,都会直接暴露在宿主机的 IP 地址上。
实践:

# 使用 host 网络运行 Nginx
docker run -d --name nginx-on-host --network 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' 是一个稳定且功能丰富的版本。
version: '3.8'

# 'services' 是核心部分,定义了组成我们应用的所有容器。
services:

# ----------------------------------------------------------------
# 服务 1: Nginx (反向代理 - The Conductor)
# 它是整个应用的唯一入口,负责将用户的请求转发到正确的前端或后端服务。
# ----------------------------------------------------------------
nginx:
# 从当前目录下的 'nginx' 子目录中的 Dockerfile 构建此服务的镜像。
# 这个 Dockerfile 通常包含一个 nginx.conf 配置文件,用于设置反向代理规则。
build: ./nginx
# 'ports' 将容器的端口暴露给宿主机,让外部世界可以访问。
# 格式: "宿主机端口:容器端口"
# 这里我们将宿主机的 8080 端口映射到 Nginx 容器的 80 端口。
# 因此,我们可以通过 http://localhost:8080 访问我们的应用。
ports:
- "8080:80"
# 'depends_on' 定义了服务的启动依赖顺序。
# 这确保了 Nginx 会在 'frontend' 和 'backend' 服务启动之后再启动,
# 否则 Nginx 启动时可能会因为找不到上游服务而报错。
depends_on:
- frontend
- backend

# ----------------------------------------------------------------
# 服务 2: Frontend (前端应用, e.g., React, Vue, Angular)
# 负责用户界面的展示。
# ----------------------------------------------------------------
frontend:
# 从 './frontend' 子目录的 Dockerfile 构建前端镜像。
build: ./frontend
# 'expose' 只在 Docker 内部网络中暴露端口,宿主机无法直接访问。
# 这是一种安全的实践,因为我们只希望 Nginx 能够访问前端服务,
# 而不希望用户直接访问它。
expose:
- "3000"
# 设置环境变量。这里用于告诉 React 应用 API 请求应该发往哪个路径。
# Nginx 会配置将 /api 的请求转发到后端。
environment:
- REACT_APP_API_URL=/api
# 前端通常依赖后端提供数据,所以设置依赖关系。
depends_on:
- backend
# 'volumes' 用于数据持久化或代码同步。
volumes:
# 1. 绑定挂载: 将宿主机的 './frontend' 目录映射到容器的 '/app' 目录。
# 这使得我们在本地修改前端代码时,容器内能立刻看到变化,非常适合开发。
- ./frontend:/app
# 2. 匿名卷 (非常重要的技巧!):
# 这个设置可以防止宿主机的 node_modules 目录覆盖容器内部的 node_modules。
# 在 Dockerfile 中,我们通常会运行 npm install 来生成适配 Linux 容器的依赖。
# 如果没有这一行,上面那行绑定挂载会把宿主机(可能是 Windows/macOS)的 node_modules
# 也映射进去,导致依赖库因操作系统不兼容而崩溃。
- /app/node_modules

# ----------------------------------------------------------------
# 服务 3: Backend (后端应用, e.g., Node.js, Python, Go)
# 负责处理业务逻辑和与数据库交互。
# ----------------------------------------------------------------
backend:
# 从 './backend' 子目录的 Dockerfile 构建后端镜像。
build: ./backend
# 同样使用 'expose',因为只有 Nginx 需要访问它。
expose:
- "3001"
# 设置环境变量,这里是数据库的连接字符串。
# 注意: 我们直接使用服务名 'mongodb' 作为主机名,这是 Docker Compose 提供的服务发现功能。
environment:
- MONGODB_URI=mongodb://mongodb:27017/todos
# 后端依赖数据库,所以先启动数据库。
depends_on:
- mongodb
# 同样使用了上面解释过的卷挂载技巧,用于开发时的代码热更新。
volumes:
- ./backend:/app
- /app/node_modules

# ----------------------------------------------------------------
# 服务 4: MongoDB (数据库)
# 负责存储应用的所有持久化数据。
# ----------------------------------------------------------------
mongodb:
# 直接使用 Docker Hub 上的官方 Mongo 镜像,版本为 6。
image: mongo:6
# 暴露 27017 端口给内部网络,让 'backend' 服务可以连接。
expose:
- "27017"
# 使用命名卷 'mongodb_data' 来持久化数据库文件。
# 这样一来,即使 `docker compose down`,数据库文件也不会丢失。
volumes:
- mongodb_data:/data/db

# 在文件的顶层声明所有在服务中使用的命名卷。
# 这让 Docker Compose 知道需要创建和管理这个卷。
volumes:
mongodb_data:

问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服务。
image

专业阶段作业练习:

exercise1:

作业要求:

# 作业要求:
# 1. 使用 Docker Volume 持久化 MySQL 数据,保障数据不会因容器重启而丢失。
# 2. 通过 init.sql 初始化数据库,创建 testdb。
# 3. 通过环境变量设置 MySQL root 密码。
# 4. 提供测试脚本验证数据持久化。
# 5. 使用 docker compose 部署。
#
# 说明:
# - mysql-data 是 named volume,用于持久化 /var/lib/mysql 目录。
# - ./init.sql 会在容器首次启动时自动执行,初始化数据库。
# - MYSQL_ROOT_PASSWORD 设置 root 用户密码。
# - 3306:3306 将 MySQL 服务暴露到主机。

首先我们先编写好一个yml框架:

services:
mysql:
...
volumes:
...

之后按步骤依次解决,首先看第一个要求持久化MySQL数据,因此我们需要用到volume,我们需要yml中在先创建一个volume并添加进mysql服务中:

volumes:
mysql-data:

然后我们查看mysql的官方文档,可以找到持久化数据的路径是/var/lib/mysql,因此我们可以在docker-compose.yml中添加如下代码:

mysql:
volumes :
- mysql-data:/var/lib/mysql
- ./init.sql:/docker-entrypoint-initdb.d/init.sql

接着,我们来解决第三个要求:通过环境变量设置 MySQL root 密码。这同样是官方镜像支持的标准做法,可以提高安全性。我们使用 environment 关键字来定义环境变量。

mysql:
environment:
- MYSQL_ROOT_PASSWORD=020416

现在,我们的服务还缺少一些基本的定义,比如使用哪个镜像、重启策略、端口映射等。我们来把它们补全。

  • image: 指定使用官方的 mysql:8.0 镜像。

  • restart: 设置为 unless-stopped,这是一个很好的习惯,意味着除非我们手动停止,否则容器总会在退出后自动重启。

  • ports: 将主机的 3306 端口映射到容器的 3306 端口,方便我们从本地连接数据库。

将这些补充完整后,我们就得到了最终的 docker-compose.yml 文件。

version: '3.8'

services:
mysql:
image: mysql:8.0
container_name: mysql_db
restart: unless-stopped
environment:
MYSQL_ROOT_PASSWORD: '123456'
ports:
- "3306:3306"
volumes:
- mysql-data:/var/lib/mysql
- ./init.sql:/docker-entrypoint-initdb.d/init.sql

volumes:
mysql-data:

成果验证:
最后,也是最关键的一步,我们需要验证我们的配置是否正确,特别是数据持久化是否生效。

第一步:启动服务

在项目根目录下打开终端,运行命令:

docker compose up -d

服务将会在后台启动。

第二步:初次连接并验证初始化

我们需要进入容器内部的 MySQL 客户端来执行 SQL 命令。

# 使用我们在 yml 中定义的容器名进入容器
# -p 后面会提示输入密码,我们输入 '123456'
docker compose exec mysql_db mysql -u root -p

成功登录后,我们检查 testdb 是否被 init.sql 成功创建:

-- 在 mysql 命令行中执行
SHOW DATABASES;

你应该可以在列出的数据库中看到 testdb,这证明 init.sql 的挂载和自动执行是成功的。

第三步:创建测试数据

为了验证数据持久化,我们需要在数据库里创建一些数据。

-- 切换到 testdb 数据库
USE testdb;

-- 创建一个简单的 users 表
CREATE TABLE users (id INT AUTO_INCREMENT PRIMARY KEY, name VARCHAR(50));

-- 插入一条数据
INSERT INTO users (name) VALUES ('张三');

-- 查询验证数据是否存在
SELECT * FROM users;

此时你应该能看到我们刚刚插入的“张三”这条记录。

第四步:模拟“灾难”,删除容器

现在我们来模拟容器因故被删除的场景。运行 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

登录后,直接查询数据:

-- 切换数据库
USE testdb;

-- 查询数据
SELECT * FROM users;

如果此时你仍然能看到“张三”那条记录,那就完美地证明了数据持久化是成功的! 我们的数据被安全地保存在了 mysql-data 卷中,并没有因为容器的删除而丢失。

exercise2: 使用 Docker Compose 部署 Python Web 应用与 Redis

1. 作业要求

使用 Python Flask 作为 Web 框架,Redis 作为内存数据库,来实现一个简单的计数器应用。

作业要求:

# 作业目标:
# 构建一个 Python Web 应用镜像,支持通过 Redis 计数。
# 同时使用 docker-compose 管理 Web 应用与 Redis 两个服务。

# 关键步骤说明:
# - 使用自定义 bridge 网络,实现容器间通信。
# - Web 应用通过 /ping 路径访问,并使用 Redis 计数。
# - 可通过 Dockerfile 构建 web 服务镜像。
# - Web 服务暴露在 5000 端口。
# - Redis 数据需持久化。

2. 应用开发与容器化

第一步:编写 Python 应用代码

与之前的作业不同,这次需要先准备好 Python 应用本身以及两个核心文件:requirements.txt 用于声明依赖,app.py 是应用主程序。
不过这两个文件在项目中已经提前为我们写好了,在本次作业中我们只需要编写Dockerfile文件,学习掌握docker相关的知识就可以了。

第二步:为 Python 应用编写 Dockerfile

接下来,根据作业要求,为这个 Flask 应用编写了 Dockerfile

# 选择一个官方的、精简的 Python 镜像作为基础
FROM python:3.10-slim

# 在容器内创建一个工作目录
WORKDIR /app

# 复制依赖文件并安装。先复制 requirements.txt 并安装,
# 这样可以利用 Docker 的层缓存,如果依赖不变,后续构建时就无需重复安装,速度更快。
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

# 将项目下的所有文件(主要是 app.py)复制到工作目录
COPY . .

# 暴露 Flask 默认的 5000 端口(文档性质)
EXPOSE 5000

# 设置容器启动时要执行的命令
CMD ["flask", "run", "--host=0.0.0.0"]

这里有一个非常重要的学习点:CMD 命令中的 --host=0.0.0.0。Flask 默认只在 localhost (127.0.0.1) 上监听,这意味着它只接受来自容器内部的连接。为了让它可以接收来自 Docker 网络中其他容器(如 Nginx 或我们直接从主机映射的端口)的连接,必须将其绑定到 0.0.0.0,代表监听所有网络接口。

3. 使用 Docker Compose 进行编排

完成了应用的容器化之后,我开始编写 docker-compose.yml 文件来定义和连接 webredis 两个服务。

最终的 docker-compose.yml 文件:

version: '3.8'

services:
web:
# 使用当前目录下的 Dockerfile 进行构建
build: .
ports:
# 将主机的 5000 端口映射到容器的 5000 端口
- "5000:5000"
networks:
- mynet
depends_on:
- redis

redis:
# 使用官方的、轻量的 redis:alpine 镜像
image: "redis:alpine"
networks:
- mynet
# 将命名卷挂载到 Redis 的数据目录 /data,实现持久化
volumes:
- redis_data:/data

# 创建一个名为 mynet 的自定义桥接网络
networks:
mynet:
driver: bridge

# 在顶层声明命名卷
volumes:
redis_data:

在这个配置中,重点是容器间的网络:

  • 自定义网络 mynet:将 webredis 两个服务都连接到这个网络,实现了它们之间的隔离和通信。web 服务可以通过服务名 redis 直接访问到 Redis 容器。
  • Redis 数据持久化:与 MySQL 类似,我为 Redis 也创建了一个命名卷 redis_data,并将其挂载到 Redis 容器内的数据存储目录 (/data),以确保计数值在容器重启后不会丢失。

4. 验证学习成果

所有文件准备就绪后,我开始了最终的测试。

第一步:启动服务

docker compose up -d --build

第二步:测试计数功能
我打开终端,多次使用 curl 命令访问 /ping 接口。

curl http://localhost:5000/ping
# 输出: This page has been pinged 1 times.

curl http://localhost:5000/ping
# 输出: This page has been pinged 2 times.

计数值正常累加,证明 web 服务和 redis 服务之间的通信是成功的。

第三步:验证数据持久化
为了确认 Redis 的数据也能持久化,我模拟了一次服务下线和重启。

# 停止并移除容器
docker compose down

# 再次启动服务
docker compose up -d

服务重启后,我再次访问 /ping 接口:

curl http://localhost:5000/ping
# 输出: This page has been pinged 3 times.

计数器从上次停止的值 2 继续累加到了 3,这完美地证明了 Redis 的数据通过 redis_data 卷被成功地持久化了。


exercise3: 使用 Docker Compose 编排多服务 Web 应用 (Nginx + Go + MySQL)

1. 作业背景与要求

在完成了单个服务的容器化部署后,下一个是挑战一个更接近真实世界场景的作业:将一个 Web 应用栈(反向代理、后端应用、数据库)通过 Docker Compose 进行编排。

作业要求:

# 作业要求:
# 1. 使用 Docker Compose 编排 Golang Web 服务、MySQL 和 Nginx 三个服务。
# 2. Golang Web 服务需实现 /count 路径,计数存储在 MySQL。
# 3. Nginx 反向代理端口到 Web 服务。
# 4. 所有服务通过自定义网络通信,Web 仅对 Nginx 暴露。
# 5. 提供测试脚本验证计数功能。
#
# 说明:
# - app 服务通过 Dockerfile 构建,依赖 MySQL。
# - nginx 服务挂载自定义配置文件。
# - 统一使用 app-network 网络。
# - MYSQL_DSN 环境变量用于配置数据库连接。
# - mysql-data 是 named volume,用于持久化 MySQL 数据。

2. 方案设计与文件结构

根据要求,设计了一个经典的三层架构:

  1. Nginx (nginx): 作为前端的反向代理和流量入口,负责接收所有外部请求,并将其转发给后端的 Go 应用。
  2. 应用服务 (app): 使用 Golang 编写的后端服务,负责处理业务逻辑(计数器功能),并与数据库交互。
  3. 数据库 (mysql): 负责存储计数器的数据,并实现持久化。

为了组织好这个项目,我创建了如下的目录结构:

/exercise2
├── docker-compose.yml # 核心编排文件
|
├── app/ # 存放 Go 应用相关文件
│ ├── main.go # Go 源代码
│ ├── Dockerfile # 用于构建 Go 应用镜像
│ └── go.mod # Go 模块依赖文件

└── nginx.conf # 存放 Nginx 配置

3. 分步实现

第一步:后端应用 app 的容器化

因为要求需要我为它编写了 Dockerfile,这里我学习并使用了一个重要的优化技巧——多阶段构建(multi-stage build)

  • 构建阶段 (builder): 我先使用一个包含完整 Go 开发环境的 golang:1.21-alpine 镜像来编译我的代码,生成一个静态的二进制可执行文件。
  • 最终阶段 (final): 然后,我使用一个极度精简的 alpine:latest 基础镜像,只将上一个阶段编译好的二进制文件复制进来。

这样做的好处是,最终的应用镜像非常小,不包含任何编译工具和源代码,更加安全和轻量。

app/Dockerfile 文件内容:

# ---- Stage 1: Build ----
FROM golang:1.21-alpine AS builder
WORKDIR /src
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 go build -ldflags "-s -w" -o /app/main .

# ---- Stage 2: Final ----
FROM alpine:latest
COPY --from=builder /app/main /app/main
# 声明应用监听的端口(文档性质)
EXPOSE 3000
CMD ["/app/main"]

第二步:配置 Nginx 反向代理

Nginx 的职责是作为“前台接待”,将来自外部的请求(例如 http://localhost:8080/count)转发给内部的 app 服务。我在 nginx/nginx.conf 文件中定义了这个规则。

nginx/nginx.conf 文件内容:

events {
worker_connections 1024;
}
http {
server {
listen 80; # Nginx 在容器内部监听 80 端口
location / {
# 使用变量和 resolver 的方式,可以避免 Nginx 启动时因 app 未就绪而导致的 DNS 解析失败问题
set $upstream_app http://app:3000;
# 将请求转发给名为'app'的服务的'3000'端口
proxy_pass $upstream_app;

# 设置一些必要的代理头信息
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}
}
}

第三步:编写核心编排文件 docker-compose.yml

这是将所有服务串联起来的“总指挥”。我定义了 nginx, app, mysql 三个服务,并将它们连接到同一个自定义网络 app-network

在这个过程中,我遇到了很多问题,并通过不断修正,最终得到了一个可以成功build的版本。

最终的 docker-compose.yml 文件:

# docker-compose.yml (格式修正版)

version: '3.8'

services:
# Nginx 服务,作为应用的入口
# MySQL 数据库服务
mysql:
image: mysql:8.0
container_name: go-mysql-db
restart: always
volumes:
- mysql-data:/var/lib/mysql
networks:
- app-network
environment:
MYSQL_ROOT_PASSWORD: '020416'
MYSQL_DATABASE: 'appdb'

# Golang 应用服务
app:
build: ./app
container_name: go-app
expose:
- "3000"
networks:
- app-network
depends_on:
- mysql
environment:
MYSQL_DSN: 'root:020416@tcp(mysql:3306)/appdb?parseTime=true'

nginx:
image: nginx:1.25-alpine
container_name: go-proxy
ports:
- "8080:80"
volumes:
# 注意:根据你的文件结构,这里使用 ./nginx.conf
- ./nginx.conf:/etc/nginx/nginx.conf:ro
networks:
- app-network
depends_on:
- app


networks:
app-network:
driver: bridge

volumes:
mysql-data: {}

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 错误。
    • 排查: 这是一个复杂的过程。
      1. 我首先检查了 app 服务的日志,发现它声称在 3000 端口上运行正常。
      2. 然后我检查了 nginx 日志,发现有 Connection refused 错误。
      3. 最终,我使用了 docker compose exec nginx curl http://app:3000/count 这个强大的诊断命令,直接从 Nginx 容器内部测试 app 服务,发现它是健康的!
    • 原因: 这排除了所有可能,最终发现还是 docker-compose.ymlapp 服务的 environment 格式依然是错误的,导致它无法连接数据库,Web 服务处于“假死”状态。
    • 收获: 学会了如何系统性地调试多服务应用。当外部访问失败时,要深入到容器网络内部进行测试,以精确定位故障点。

5. 最终验证

在应用了所有修正和 healthcheck 之后,我执行了最终的启动和测试流程。

  1. 清理并启动

    docker compose down -v
    docker compose up -d --build
  2. 等待服务就绪
    通过 docker compose ps 可以观察到服务从 startinghealthy 的过程。

  3. 测试计数功能

    curl http://localhost:8080/count