02菜鸟:玩转现成镜像

4045 字
20 分钟
02菜鸟:玩转现成镜像

第 2 关 玩转现成镜像#

本关的产出:三个同时跑着的容器(nginx、mysql、redis),每一个都被改过默认行为;以及一套「拿到任意一个陌生镜像,自己找出它有哪些旋钮」的方法。

2.1 镜像不是黑盒,它身上有一排默认值#

第 1 关把 nginx 当靶子练了一遍命令,那会儿它只是「随便一个能在浏览器打开的镜像」,命令练完、容器删掉,没人关心它自己是怎么配的。这一关换个问法:同一个镜像,能不能不重新构建,就让它表现出不同的行为?

能。因为一个镜像打包的不只是程序本身,还有一整套默认值:默认监听哪个端口、默认读哪个路径下的配置、默认把数据写在哪儿、默认以哪个身份运行、默认启动时跑哪个程序。这些是镜像的作者在构建时就写下的——但写下的是默认值,不是不可改的值。

docker run 时给出的参数,就是这一次运行的实际值。参数盖住默认值,而镜像本身一个字节都不动。

能盖的四类旋钮,其中两类第 1 关已经用过了:

  1. 端口:-p 宿主:容器(第 1 关用过)。
  2. 启动命令:写在镜像名后面的那串东西。
  3. 环境变量:-e 名字=值。
  4. 挂载:-v 宿主:容器,用宿主机的目录或文件顶掉容器里的。

这一关把这四类全用一遍,最后回头解决一个问题:这些旋钮的名字,是怎么知道的。

2.2 环境变量:官方镜像最常留的那排旋钮#

先把这个词说清楚。环境变量就是一组「名字 = 值」,跟着进程一起启动。程序在启动的那一刻读它们一遍:读到了就照上面写的做,没读到就用自己的默认值。这不是 Docker 发明的东西,是 Linux 上早就有的机制;Docker 只是把它变成了一个命令行参数。

「官方镜像」指的是 Docker 官方维护并发布的那批常见软件镜像,nginx、mysql、redis 都是。mysql 官方镜像就是一个典型:它启动时必须知道 root 密码是什么,而它又没有合理的默认值可给,于是干脆不给,逼你显式写出来。

docker run -d --name mysql-demo -e MYSQL_ROOT_PASSWORD=hecloud mysql

拆开看:

  1. --name mysql-demo:给这个容器起个名字。
  2. -e MYSQL_ROOT_PASSWORD=hecloud:把这个名字和值送进容器的环境变量。镜像里的启动脚本读到它,就知道 root 密码该设成什么。
  3. 结尾的 mysql:镜像名。本地没有它会先去拉。

「起个名字」是什么意思:容器一被创建就有一个名字,不写 --name 时 Docker 会随手拼两个单词给它(dreamy_einstein 之类),docker ps 的 NAMES 一列就是它。所以 --name 不是重命名,而是指定。

名字就是容器身上的一个字段,这两个动作是同一个字段的两个时机:docker run --name 在容器创建时把它写定,docker rename <容器> <新名字> 在创建之后改写它——后者才是重命名。左边填 ID 或旧名字都行,运行中、已停止都能改,改完不用重启。

名字和容器 ID 是一回事——都是这个容器的标识,凡是命令里要写 <容器> 的地方两样都能填。区别只在:ID 是 Docker 生成的、唯一但记不住;名字是人给的、短、好念,也唯一。第 1 关一直用 ID,是为了先看清「容器真有 ID 这回事」;要同时跑好几个容器时,名字就明显更好使。

名字还有第二份工作:等第 5 关给容器接上自定义网络,它就是别的容器能叫到它的那个名字。到那时「起个有意义的名字」就不只是好念的问题了。

不给 MYSQL_ROOT_PASSWORD 会怎样?容器起来后立刻退出,docker logs 里写得明明白白:它拒绝启动,理由是没有告诉它 root 密码。这是官方镜像一个很常见的约定:没有合理默认值、又必须知道的东西,就用这种方式逼你说出来,而不是悄悄用一套「root + 空密码」把问题埋进深处。等到线上才发现,代价就大了。

怎么知道密码真的生效了:等它起来之后进去问一句——docker exec -it mysql-demo mysql -uroot -phecloud -e "show databases;"。能列出数据库,就说明 -e 送进去的那个密码确实被程序读到了。

注意:要给的变量不止一个时,-e 名字=值 写几遍就是几个。

要观察:docker ps 里这个容器的 PORTS 一列是空的。mysql 默认在容器里监听 3306,但没写 -p,所以那个端口只存在于容器自己的网络里,宿主机上碰不到。

易踩的坑:

  • 密码写在命令里,会留在终端的历史记录里,也会明文记在容器的配置里。这一关先接受这个做法,正经的密码管理留到第 13 关。
  • 环境变量只在容器第一次创建时读一次,之后就成了容器配置的一部分。Docker 没有提供「改一个已存在容器的环境变量」这个操作——名字有 docker rename,CPU、内存这类运行参数另外有命令,唯独环境变量没有。想变只能删掉重建,docker start 读的也是当初那一份。(删掉重建为什么不吃亏,2.3 会说。)
  • 名字在所有容器里唯一,包括已经停掉的。一个停着不动的 mysql-demo 也照样占着这个名字,再起一个同名的会直接报 Conflict. The container name "/mysql-demo" is already in use,得先 docker rm mysql-demo。
  • -e 和名字常被放在一起记,但两者的可变性正好相反:-e 定了就改不了,名字随时能改(docker rename)。唯一的共同点是都长在容器身上——容器一删,两者一起消失。
  • mysql 第一次启动要先初始化数据目录,得等十几秒才真正能连。这期间上面那条查询会报连接失败,但那不是密码错了。docker logs mysql-demo 能看到它在做什么,最后打印出 ready for connections 才算好了。

2.3 挂载:把宿主机的目录或文件接到容器里#

第 1 关留过一句话:容器删掉之后,写进它自己文件系统里的改动一并消失。这句话得先把「文件系统」拆开才站得住——它在这里指的是两个不同层级的东西:

  1. 跑 Docker 的那台 Linux 虚拟机,有一份自己的文件系统。它就是一块普通的磁盘,镜像和容器都住在这块磁盘上,是它的住户。
  2. 容器里的进程看到的那个 /,不是一个磁盘,是拼出来的:下面那层来自镜像,只读;上面那层是容器自己写的。

所以容器里的 /etc/nginx/nginx.conf 并不像普通机器上那样「是磁盘上的一个文件」,它是一次查询的结果——去镜像那层找到了,就给你。改它,改动落到容器自己那层;容器一删,那层没了,读到的又变回镜像里原来那份。

容器没有自己的磁盘。 它不是一台小机器,它看到的那个根目录由镜像撑着,只有它自己改过的那一点点是独占的。数据就是这样丢的。第 3 关讲镜像怎么分层时,会把这套拼装讲全。

mysql 把整个数据库都存在容器自己的文件系统里。按上面那段,删掉容器就等于删库——这显然不行。要让数据活在容器之外,就得让容器别把数据写在自己身上,而是让宿主机上的一个目录,出现在它写数据的那个位置上。挂载做的事,就是让那个位置不再来自镜像、也不再来自容器自己那层,而是直接指向宿主机上一个真实存在的目录。

docker run -d --name mysql-demo \
-e MYSQL_ROOT_PASSWORD=hecloud \
-v ~/docker/mysql-data:/var/lib/mysql \
mysql
  • -v 宿主路径:容器路径:顺序和 -p 一样,是「宿主在前,容器在后」,同样是容易记反的地方。
  • /var/lib/mysql 是 mysql 官方镜像约定好的数据目录。
  • ~/docker/mysql-data 是宿主机上的路径。~ 指家目录,所以这条命令的意思是:让家目录下 docker/mysql-data 这个目录,成为容器里的 /var/lib/mysql。写进后者的东西,实际落到前者身上。

这个能力不只对数据目录有用。镜像里的任何一个文件,都能被宿主机上的文件顶掉——包括它的配置文件。比如把 nginx 的主配置换成自己写的:

docker run -d --name nginx-demo -p 8080:80 \
-v ~/docker/nginx.conf:/etc/nginx/nginx.conf:ro \
nginx
  • 挂的可以是一个目录,也可以是一个文件。
  • 末尾的 :ro 表示容器里只能读、不能改。挂配置文件时习惯加上,免得容器里的程序把宿主机上这份文件顺手改了。

验证一下真的留住了:docker rm -f mysql-demo 把容器删掉,再用上面那条命令重新起一个,挂的是同一个目录——之前建过的库还在。这就是「数据活在容器之外」的意思。

这一步顺手印证了一件更大的事:容器是可以随手删掉重造的,只要它身上有状态的东西都挂到了外面。改环境变量、改端口、改启动命令,走的都是同一个套路——把容器删掉,把 docker run 那条命令改一改,重来一遍。所以那条命令本身是值得留着的,容器反倒不值钱。

换句话说:有状态的东西挂出去,没状态的东西写在创建命令里。这是这一关真正想让人带走的一句话,也是为什么第 6 关的 Compose 干的事,就是把这条命令从命令行里搬进一个文件。

易踩的坑:

  • -v 的第一段,必须以 / 或 ./ 开头,才会被当成宿主机的路径;不然它会被当成一个卷的名字(那是另一套东西,第 4 关讲),不是目录。这条规则能解释一个高频错误:~ 只在 shell 里展开,所以一旦写成 -v "~/docker/mysql-data:/var/lib/mysql"——引号把 ~ 挡住了,它原样传给 Docker,开头不是 /,于是被当成卷名,报 includes invalid characters for a local volume name ... If you intended to pass a host directory, use absolute path。报错里那句提示就是答案。
  • 宿主路径不存在时,Docker 不会报错,而是替你造一个空目录。如果本该挂的是一个文件,那个位置就变成了一个空目录,容器里的程序找不到文件直接报错,报错内容还跟「挂载」两个字没关系。
  • 挂载会整个盖住目标位置。宿主目录是空的,容器里那个目录就变成空的——哪怕镜像里原本装了东西。所以挂数据目录之前,先确认镜像在那个位置是安全的.
  • macOS 上宿主机和容器之间隔着一层,读写挂载目录比在 Linux 上慢一截。开发够用,跑数据库压测就不合适了。

为什么需要 -v 这件事、它和「卷」有什么区别、数据怎么备份迁移,是第 4 关的全部内容。这一关只把它当成「把外面的东西接进来」这个动作用。

2.4 覆盖启动命令:镜像说它默认这么启动,你可以改启动模式#

镜像里除了默认值,还写死了一件事:容器一启动就跑哪个程序。nginx 镜像跑 nginx,mysql 镜像跑 mysqld,redis 镜像跑 redis-server。想换掉它,就在镜像名后面接着写:

docker run --rm redis redis-server --appendonly yes
  • 镜像名后面的东西(redis-server)就是启动命令,它会把镜像里写死的那个整个换掉。
  • --rm 表示容器一退出就自动删掉。跑「看一眼就走」的临时容器时常用,省得回头再 docker rm。
  • 这条命令换掉的就是 redis-server,变动只不过是给它加了个参数—appendonly yes。

要换成一个完全不同的程序也行。这招真正的用处是:钻进一个陌生镜像里,看看它到底装了什么。手头有个没用过的镜像,不确定里面是什么样子,就开一个容器给它一个终端,而不是先费劲跑起来、再想办法钻进去:

docker run -it --rm nginx sh

进去之后 ls、cat、which 都跟普通 Linux 一样,看完 exit 出来,容器自己就没了。

要观察:容器生下来就开始跑这条命令,命令一结束容器就退出。所以像上面那样开一个 shell 的容器,退出那一刻它就从 docker ps 里消失了;docker ps -a 也看不到——--rm 已经把它删了。

易踩的坑:

  • 有些镜像在启动真正的主程序之前,还会先跑一段自己的准备工作(初始化数据目录、生成配置文件)。那段准备工作不会被替换掉,被换掉的只有最后那个程序。所以「镜像名后面写什么,就跑什么」这句话是简写版,第 3 关讲镜像的启动配置时会补全。
  • 换掉启动命令之后,容器的主进程就成了你写的这个。它一退出,容器就停——这正是上面那个 shell 容器的行为。

2.5 这些旋钮的名字,从哪儿知道#

四类旋钮都会用了,但还剩最关键的一步:拿到一个从没用过的官方镜像,怎么知道它认哪些环境变量、数据该往哪个目录挂? 总不能靠猜。

第一手资料是官方镜像的文档页。 Docker Hub 上每个官方镜像都有一页说明,里面列着它支持的环境变量、约定好的数据目录和配置目录、默认暴露的端口、默认以哪个身份运行。优先看它。

docker inspect <镜像> 是另一条路:它把镜像的完整配置整个打出来,比文档页更全,但输出很长、里面没有任何解释——它是留给排查问题用的。这里先知道有这么个东西就够,怎么读它放到第 12 关。

再回头看,官方镜像的默认约定其实相当一致,一般摸清三条就够用了:

  1. 没有合理默认值、又必须知道的,就拒绝启动,用这种方式逼你显式给(mysql 的 root 密码就是)。
  2. 数据目录固定在某个路径,文档里会写。这个路径是「运行期才写」的,所以可以挂载,也应该挂载。
  3. 端口默认只在容器内监听,想在宿主机上访问,必须自己写 -p。

2.6 三连:把三个一起跑起来#

前面每样都单练过一遍,现在把 nginx、mysql、redis 三个同时跑起来,各改各的行为。

docker run -d --name web -p 8080:80 nginx
docker run -d --name db -p 3306:3306 \
-e MYSQL_ROOT_PASSWORD=hecloud \
-v ~/docker/mysql-data:/var/lib/mysql \
mysql
docker run -d --name cache -p 6379:6379 \
-v ~/docker/redis-data:/data \
redis

三个镜像改的地方各不相同:web 只做了端口映射;db 既给了必填的环境变量,又把数据目录挂了出来;cache 只挂了数据目录,因为它没有必填变量,默认就是不带密码启动的。

跑起来之后有一点会立刻感觉到:三个容器各跑各的,互相看不见。web 里的 nginx 没有任何办法找到 db,db 也找不到 cache——它们各自是一个独立的小世界。怎么让它们互相说话,是第 5 关的事。

本关命令速查#

运行容器时的各类参数,见 命令速查表 · 运行时的参数。

支持与分享

如果这篇文章对你有帮助,欢迎分享给更多人或打赏支持!

打赏
02菜鸟:玩转现成镜像
https://hecloud.top/posts/tech/docker/02玩转现成镜像/
作者
贺小云
发布于
2026-09-19
许可协议
CC BY-NC-SA 4.0

评论区

Profile Image of the Author
贺小云
热爱技术与折腾的博客 Dancing with uncertainty. 与不确定性共舞
公告
出于成本考虑,贺云已全面转型Serverless架构
分类
标签
碎碎念
站点统计
文章
91
分类
5
标签
44
总字数
103,192
运行时长
0 天
最后活动
0 天前
站点信息
构建平台
Cloudflare Pages
博客版本
Firefly v6.16.5
文章许可
CC BY-NC-SA 4.0