02菜鸟:玩转现成镜像

第 2 关 玩转现成镜像
本关的产出:三个同时跑着的容器(nginx、mysql、redis),每一个都被改过默认行为;以及一套「拿到任意一个陌生镜像,自己找出它有哪些旋钮」的方法。
2.1 镜像不是黑盒,它身上有一排默认值
第 1 关把 nginx 当靶子练了一遍命令,那会儿它只是「随便一个能在浏览器打开的镜像」,命令练完、容器删掉,没人关心它自己是怎么配的。这一关换个问法:同一个镜像,能不能不重新构建,就让它表现出不同的行为?
能。因为一个镜像打包的不只是程序本身,还有一整套默认值:默认监听哪个端口、默认读哪个路径下的配置、默认把数据写在哪儿、默认以哪个身份运行、默认启动时跑哪个程序。这些是镜像的作者在构建时就写下的——但写下的是默认值,不是不可改的值。
docker run 时给出的参数,就是这一次运行的实际值。参数盖住默认值,而镜像本身一个字节都不动。
能盖的四类旋钮,其中两类第 1 关已经用过了:
- 端口:
-p 宿主:容器(第 1 关用过)。 - 启动命令:写在镜像名后面的那串东西。
- 环境变量:
-e 名字=值。 - 挂载:
-v 宿主:容器,用宿主机的目录或文件顶掉容器里的。
这一关把这四类全用一遍,最后回头解决一个问题:这些旋钮的名字,是怎么知道的。
2.2 环境变量:官方镜像最常留的那排旋钮
先把这个词说清楚。环境变量就是一组「名字 = 值」,跟着进程一起启动。程序在启动的那一刻读它们一遍:读到了就照上面写的做,没读到就用自己的默认值。这不是 Docker 发明的东西,是 Linux 上早就有的机制;Docker 只是把它变成了一个命令行参数。
「官方镜像」指的是 Docker 官方维护并发布的那批常见软件镜像,nginx、mysql、redis 都是。mysql 官方镜像就是一个典型:它启动时必须知道 root 密码是什么,而它又没有合理的默认值可给,于是干脆不给,逼你显式写出来。
docker run -d --name mysql-demo -e MYSQL_ROOT_PASSWORD=hecloud mysql拆开看:
--name mysql-demo:给这个容器起个名字。-e MYSQL_ROOT_PASSWORD=hecloud:把这个名字和值送进容器的环境变量。镜像里的启动脚本读到它,就知道 root 密码该设成什么。- 结尾的
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 关留过一句话:容器删掉之后,写进它自己文件系统里的改动一并消失。这句话得先把「文件系统」拆开才站得住——它在这里指的是两个不同层级的东西:
- 跑 Docker 的那台 Linux 虚拟机,有一份自己的文件系统。它就是一块普通的磁盘,镜像和容器都住在这块磁盘上,是它的住户。
- 容器里的进程看到的那个
/,不是一个磁盘,是拼出来的:下面那层来自镜像,只读;上面那层是容器自己写的。
所以容器里的 /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 关。
再回头看,官方镜像的默认约定其实相当一致,一般摸清三条就够用了:
- 没有合理默认值、又必须知道的,就拒绝启动,用这种方式逼你显式给(mysql 的 root 密码就是)。
- 数据目录固定在某个路径,文档里会写。这个路径是「运行期才写」的,所以可以挂载,也应该挂载。
- 端口默认只在容器内监听,想在宿主机上访问,必须自己写
-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 关的事。
本关命令速查
运行容器时的各类参数,见 命令速查表 · 运行时的参数。
支持与分享
如果这篇文章对你有帮助,欢迎分享给更多人或打赏支持!





