缘博客
发布日期

Docker(二):数据持久化、Dockerfile 与容器网络

作者
  • 姓名
    社交账号

本篇学习范围

上一章已经能用 docker run 启动 MySQL 和 Nginx,但还有三个实际问题:

  1. 删除容器后,数据库数据和页面文件怎么办?
  2. Java 的 JDK、Jar 和启动命令怎样固化为一个可复用镜像?
  3. 多个容器为什么不能靠写死 IP 通信,应该怎样让服务按名字找到对方?

课件这部分给出的答案分别是:数据卷与本地目录挂载、Dockerfile、自定义网络


容器可写层与持久化边界

镜像由只读层组成,容器启动时会在镜像之上添加自己的可写层。直接在 Nginx 容器的 /usr/share/nginx/html/index.html 中修改页面,修改只留在该容器的可写层里:

  • 停止和再次启动同一个容器,通常还能看到这份修改;
  • 删除容器后,这份可写层和其中的数据都会消失;
  • 新建一个相同镜像的容器,也不会自动带上旧容器的修改。

因此数据库数据、上传文件、静态资源和配置文件需要被放到容器生命周期之外。Docker 常用两种方案:

方案源头由谁管理适合场景
命名数据卷(named volume)Docker daemon 管理路径与生命周期数据库数据等不希望直接依赖宿主机目录结构的持久化内容。
本地目录挂载(bind mount)明确指定宿主机文件或目录静态资源、配置文件、源代码、需要被宿主机直接编辑的文件。

两者都会把内容挂到容器内目录,但外部管理方式完全不同。


命名数据卷:让 Docker 管理宿主机存储位置

课件把数据卷描述为“容器内目录和宿主机目录之间的映射桥梁”。更精确地说,数据卷是 Docker 管理的一块宿主机持久化存储,容器通过挂载点使用它

在典型 Linux Docker Engine 上,名为 html 的卷可能位于:

/var/lib/docker/volumes/html/_data

课件的 Nginx 图示中:

html 卷  →  /usr/share/nginx/html
conf 卷  →  /etc/nginx/conf.d

/var/lib/docker/volumes/... 不是所有环境都应写死的固定路径。Docker Desktop 运行在 Linux VM 中,rootless Docker 的存储目录也不同;应以 docker volume inspect 的结果为准,不要手动依赖或修改 Docker 管理目录。

数据卷管理命令

命令作用示例
docker volume create创建命名数据卷docker volume create html
docker volume ls查看所有数据卷docker volume ls
docker volume inspect查看指定数据卷详情、挂载点和驱动docker volume inspect html
docker volume rm删除指定未被使用的数据卷docker volume rm html
docker volume prune清理未使用的数据卷先阅读提示并确认。

例如:

docker volume create html
docker volume ls
docker volume inspect html
docker volume rm html

docker volume prune 是有数据风险的清理命令。现代 Docker 默认会清理未使用的匿名卷;加入 --all 才会把未使用的命名卷也纳入范围。执行前先列出卷、确认没有数据库或业务数据仍依赖它:

docker volume ls
docker volume prune

docker run 中挂载命名卷

课件格式:

-v 数据卷名:容器内目录

如果数据卷不存在,docker run 会自动创建它。例如将 html 卷挂到 Nginx 默认静态目录:

docker run -d \
  --name nginx-volume \
  -p 8080:80 \
  -v html:/usr/share/nginx/html \
  nginx:1.20.2

html 没有以 /./ 开头,因此 Docker 将它识别为数据卷名称,而不是宿主机路径。容器重建后,只要再次挂载同名 html 卷,数据仍会保留。


本地目录挂载:把指定宿主机路径直接交给容器

课件的格式是:

docker run -d \
  --name 容器名 \
  -p 宿主机端口:容器端口 \
  -v 宿主机目录或文件:容器内目录或文件 \
  镜像名

-v 左侧的写法决定了 Docker 把它当作命名卷还是本地路径:

写法Docker 的理解
-v mysql:/var/lib/mysql名为 mysql 的命名数据卷。
-v ./mysql:/var/lib/mysql当前工作目录下的 mysql 目录。
-v /root/mysql/data:/var/lib/mysql宿主机的绝对目录。

课件的硬性规则:要挂载本地目录或文件,左侧必须以 /./ 开头。若只写一个名称,Docker 会识别为数据卷。

使用 -v 时的两个坑

  1. 当 bind mount 的宿主机源路径不存在时,-v 可能自动创建一个目录。若本来想挂载一个配置文件,却把文件名拼错,最终可能得到“宿主机多了目录、容器启动异常”的隐蔽错误。
  2. 把目录挂到容器已有内容的目录上,会遮住该目录原有文件。就像把 U 盘挂到 Linux 的某个目录后,原目录内容暂时不可见一样。

先创建源目录和文件、再挂载会更清楚:

mkdir -p /root/tlias-nginx/html
mkdir -p /root/tlias-nginx/conf
touch /root/tlias-nginx/conf/nginx.conf

生产脚本中,--mount 的字段更明确,而且默认会在源路径不存在时直接报错:

docker run --mount type=bind,src=/root/tlias-nginx/html,dst=/usr/share/nginx/html nginx:1.20.2

课程仍以 -v 为核心,理解其判别规则最重要。对只读配置和静态资源,可考虑附加 :ro,避免容器反向修改宿主机内容:

-v /root/tlias-nginx/conf/nginx.conf:/etc/nginx/nginx.conf:ro

在启用 SELinux 的 Linux 上,挂载权限还可能需要 :z:Z 标签选项。不要在不了解影响时对系统目录随意加标签。


MySQL:同时挂载数据、初始化脚本和配置

课件要求把三类 MySQL 内容放到宿主机:

宿主机路径容器内路径用途
/root/mysql/data/var/lib/mysqlMySQL 数据文件。
/root/mysql/init/docker-entrypoint-initdb.d初始化 SQL 脚本目录。
/root/mysql/conf/etc/mysql/conf.dMySQL 配置片段目录。

完整示例:

mkdir -p /root/mysql/data
mkdir -p /root/mysql/init
mkdir -p /root/mysql/conf

docker run -d \
  --name mysql \
  -p 3307:3306 \
  -e TZ=Asia/Shanghai \
  -e MYSQL_ROOT_PASSWORD='请替换为强随机密码' \
  -v /root/mysql/data:/var/lib/mysql \
  -v /root/mysql/init:/docker-entrypoint-initdb.d \
  -v /root/mysql/conf:/etc/mysql/conf.d \
  mysql:8

官方 MySQL 镜像只会在数据目录首次初始化且为空时执行 /docker-entrypoint-initdb.d 中的脚本。已有数据目录再改 MYSQL_ROOT_PASSWORD 或新增初始化脚本,并不会重置旧数据库。清空数据前先做可恢复的备份。


自定义镜像:把运行环境、应用和启动方式写成 Dockerfile

镜像的三层概念

课件将镜像结构拆成三部分:

BaseImage(基础镜像)
Layer(每条安装、复制、配置指令形成的层)
Entrypoint(容器启动入口)
  • BaseImage:提供操作系统库、运行环境和基础文件,例如课件中的 centos:7
  • LayerRUNCOPY 等每一步构建操作会生成新的镜像层。后续构建可以复用未变化的层缓存,因此把变化频繁的应用文件放在后面通常更利于缓存。
  • Entrypoint:容器启动时运行的入口命令。一个容器通常围绕一个主要前台进程运行。

Dockerfile 是什么

Dockerfile 是一个纯文本文件,由固定指令描述镜像构建过程。Docker 在 build 时按顺序执行这些指令。

指令课件含义示例
FROM指定基础镜像FROM centos:7
ENV设置后续构建和运行时可用的环境变量ENV key=value
COPY从构建上下文复制文件进入镜像COPY app.jar /app/app.jar
RUN在构建镜像时执行 Shell 命令RUN mkdir -p /app
EXPOSE说明容器运行时预期监听的端口EXPOSE 8080
ENTRYPOINT指定容器运行入口ENTRYPOINT ["java", "-jar", "app.jar"]
WORKDIR设置后续指令的工作目录WORKDIR /app

EXPOSE 8080 只是镜像元数据和使用说明,不会真的开放宿主机端口。运行容器时仍要用 -p 宿主机端口:容器端口 发布端口。

课件的 Java 镜像 Dockerfile

下面完整保留课件的 JDK 17 + Jar 打包示例:

FROM centos:7
COPY jdk17.tar.gz /usr/local/
RUN tar -xzf /usr/local/jdk17.tar.gz -C /usr/local/ && rm /usr/local/jdk17.tar.gz
ENV JAVA_HOME=/usr/local/jdk-17.0.10
ENV PATH=$JAVA_HOME/bin:$PATH
RUN mkdir -p /app
WORKDIR /app
COPY app.jar app.jar
EXPOSE 8080
ENTRYPOINT ["java", "-jar", "/app/app.jar"]

逐段理解:

  1. FROM centos:7 提供基础系统;
  2. COPY 把构建上下文中的 JDK 压缩包复制到镜像;
  3. RUN 解压 JDK 并删掉压缩包,避免把无用安装包留在同一层;
  4. ENV 设置 JAVA_HOMEPATH
  5. WORKDIR /app 后,COPY app.jar app.jar 把应用放到 /app/app.jar
  6. ENTRYPOINT 使用 exec 形式启动 Java,Java 进程会成为容器的主进程。

课件使用的 centos:7 是历史教学基础镜像。CentOS Linux 7 已结束维护,新项目应选择仍在维护的基础镜像,并固定已验证的版本或摘要。构建时还应用 .dockerignore 排除日志、构建产物和密钥,避免它们意外进入 build context。

docker build 与 build context

课件构建命令:

docker build -t myImage:1.0 .

语法含义:

docker build -t 镜像名:标签 Dockerfile所在的构建上下文

末尾 . 表示当前目录作为 build context。Docker 默认会从这个目录寻找 Dockerfile,COPY 的源文件也必须位于这个上下文中。

课程示例的 myImage:1.0 含有大写字母,实际镜像仓库名应使用小写,例如:

docker build -t my-image:1.0 .

Dockerfile 不在默认位置时可用 -f 指定:

docker build -f docker/Dockerfile -t my-image:1.0 .

注意第二个 . 仍是构建上下文,不等同于 Dockerfile 文件本身的位置。


容器网络:默认 bridge 与自定义网络

默认 bridge 网络

课件图示中的 Docker Host 通过 docker0 虚拟网桥连接多个容器:

docker0:172.17.0.1/16
   ├── vethX → 容器 mysql 的 eth0:172.17.0.2
   ├── vethY → 容器 java 的 eth0:172.17.0.3
   └── vethZ → 其他容器的 eth0:172.17.0.4

这些地址是常见示意,不保证每台 Docker 主机都使用 172.17.0.0/16,也不应被业务配置写死。容器重建后 IP 可能变化。

课件强调:加入自定义网络的容器可以通过容器名互相访问。实际项目应让后端使用数据库服务名或容器名,而不是依赖临时容器 IP。

网络操作命令

命令作用
docker network create创建自定义网络。
docker network ls查看网络列表。
docker network inspect查看网络详情、子网和已连接容器。
docker network connect把指定容器连接到网络。
docker network disconnect让容器离开网络。
docker network rm删除指定没有被使用的网络。
docker network prune清理未使用网络。

示例:

docker network create itheima
docker network ls
docker network inspect itheima
docker network connect itheima mysql
docker network disconnect itheima mysql
docker network rm itheima

docker network prune 也属于清理操作,会删除未使用网络;执行前查看提示并确认没有环境依赖它。

在同一自定义网络中按名称访问

创建网络后,在启动时指定 --network

docker network create itheima

docker run -d \
  --name mysql \
  --network itheima \
  -e MYSQL_ROOT_PASSWORD='请替换为强随机密码' \
  mysql:8

docker run -d \
  --name tlias-server \
  --network itheima \
  -p 8080:8080 \
  tlias-server:1.0

此时后端配置数据库地址时可使用 mysql 作为主机名。数据库端口不必为了容器间通信而发布到宿主机,只有宿主机或外部客户端需要访问时才添加 -p


本篇命令清单

# 命名数据卷
docker volume create 数据卷名
docker volume ls
docker volume inspect 数据卷名
docker volume rm 数据卷名
docker volume prune

# 挂载
docker run -v 数据卷名:容器内目录 镜像名
docker run -v /宿主机目录:容器内目录 镜像名
docker run -v ./相对目录:容器内目录 镜像名

# 构建镜像
docker build -t my-image:1.0 .
docker build -f docker/Dockerfile -t my-image:1.0 .

# 网络
docker network create 网络名
docker network ls
docker network inspect 网络名
docker network connect 网络名 容器名
docker network disconnect 网络名 容器名
docker network rm 网络名
docker network prune

数据卷和 bind mount 的当前行为可对照 Docker 存储文档;Dockerfile 指令的完整语义见 Dockerfile reference。下一篇会把这些基础组合为后端、前端和 Compose 的多容器项目部署。