Docker 基础
written by SJTU-XHW
Reference:Docker 官方文档
本人学识有限,解析难免有错,恳请读者能够批评指正,本人将不胜感激!
前置知识:Linux、Git、虚拟机、略懂操作系统;
Chapter 0. Docker 安装
Mac 安装
1 | brew install --cask docker |
Linux 任何 distribution 自动安装
1 | curl -fsSL get.docker.com -o get-docker.sh # 自动安装脚本 |
Ubuntu 手动安装
卸载旧版本
1
2
3sudo apt-get remove docker \
docker-engine \
docker.io安装 HTTPS 必要软件包 和 CA 证书防止安装包篡改
1
2
3
4
5
6
7
8sudo apt-get update
sudo apt-get install \
apt-transport-https \
ca-certificates \
curl \
gnupg \
lsb-release添加软件源的
GPG
密钥,以确认所下载软件包的合法性1
2
3
4curl -fsSL https://mirrors.aliyun.com/docker-ce/linux/ubuntu/gpg | sudo gpg --dearmor -o /usr/share/keyrings/docker-archive-keyring.gpg
# 如果你在国外,或者正在科学上网,请使用国外密钥:
# curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /usr/share/keyrings/docker-archive-keyring.gpg向 APT 源添加国内软件源(stable 版本)
1
2
3
4
5
6
7
8echo \
"deb [arch=amd64 signed-by=/usr/share/keyrings/docker-archive-keyring.gpg] https://mirrors.aliyun.com/docker-ce/linux/ubuntu \
$(lsb_release -cs) stable" | sudo tee /etc/apt/sources.list.d/docker.list > /dev/null
# 如果你在国外……
# echo \
# "deb [arch=amd64 signed-by=/usr/share/keyrings/docker-archive-keyring.gpg] https://download.docker.com/linux/ubuntu \
# $(lsb_release -cs) stable" | sudo tee /etc/apt/sources.list.d/docker.list > /dev/null更新 APT 缓存并安装
docker-ce
1
2sudo apt-get update
sudo apt-get install docker-ce docker-ce-cli containerd.io docker-compose-plugin启动 Docker 服务并建立对应用户组
1
2
3
4
5sudo systemctl enable docker
sudo systemctl start docker
sudo groupadd docker
sudo usermod -aG docker $USER
Chapter 1. 基本概念
1.1 具体原理、地位和性质
本部分内容涉及内容比较深,学完
Go
和 操作系统再回来填坑……
1.2 Docker 和 传统虚拟化技术的比较
如上图所示,传统虚拟化技术构建出的 Hypervisor
需要虚拟出一套硬件,并在其上运行一个完整的操作系统(Guest OS
),再在该系统上运行所需进程;
而 Docker 容器中的应用进程借助 Docker Engine
直接运行于宿主内核,无需硬件虚拟;
特性 | 容器 | 虚拟机 |
---|---|---|
启动 | 秒级 | 分钟级 |
硬盘使用 | 一般为 MB | 一般为 GB |
性能 | 接近原生 | 弱于 |
系统支持量 | 单机支持上千个容器 | 一般几十个 |
迁移和部署 | 容易 | 困难 |
维护和扩展 | 高质量官方镜像 | 极其困难 |
1.3 镜像
操作系统分为 内核 和 用户空间。对于
Linux
而言,内核启动后,会挂载root
文件系统为其提供用户空间支持;而 Docker 镜像(
Image
),就相当于是一个root
文件系统(实际上就是一个特殊的文件系统);除了提供容器运行时所需的程序、库、资源、配置等文件外,还包含了一些为运行时准备的一些配置参数(如匿名卷、环境变量、用户等);Docker 镜像 不包含 任何动态数据,其内容在构建之后也不会被改变;
Docker 镜像采用 Union FS 技术(操作系统术语),设计为 分层存储 的架构——并非像
*.iso
一样打包成一个文件,而是一组、多层文件系统联合而成;镜像的构建时,会一层层构建;前一层是后一层的基础;
每一层构建完就不再改变,后一层的任何更改只发生在自己这层;
比如,删除前一层文件的操作,实际不是真的删除前一层的文件,而是仅在当前层标记为该文件已删除(最终容器运行时,也不会看到);
优点:分层存储的特征还使得镜像的复用、定制变的更为容易;
镜像的构建方法以后再说;
1.4 容器
Docker 镜像(
Image
)和容器(Container
)的关系,可以看成面向对象程序设计中的 类 和 实例;镜像是静态的定义,容器是镜像运行时的实体;
容器的实质是进程(process),但和运行在
Host OS
上的普通进程不一样,有着独立的命名空间(Linux namespace
),包含独立的root
文件系统、网络配置、进程空间、用户 ID 空间;体现隔离特性,有利于保证宿主系统的安全性;
容器 和 镜像一样,使用分层存储——以镜像为基础层,在其上创建一个当前容器的存储层,称这个为容器运行时访问而准备的存储层为 容器存储层;
容器存储层的生存周期和容器一样,容器消亡时,容器存储层也随之消亡。因此,任何保存于容器存储层的信息都会随容器删除而丢失;
tips 1. 按 Docker 规范,容器不应该向其存储层内写入任何数据,容器存储层要保持无状态化;
注:Instead,应该使用 数据卷(Volume,类似
Linux
中的mount
挂载目录)、或者 绑定宿主目录(这两种方法将在 Chapter 5 介绍),在这些位置的读写会跳过容器存储层,直接对宿主(或网络存储)发生读写,其性能和稳定性更高;相反,当我们运行一个容器的时候(如果不使用卷的话),我们做的任何文件修改都会被记录于容器存储层里,导致多个无关文件被修改,不利于迁移、维护;
tips 2. 数据卷的生存周期独立于容器,容器消亡,数据卷不会消亡,因此在容器被删除或重启后,在数据卷中的数据不会丢失;
1.5 仓库
Docker 社区提供集中的存储、分发镜像的服务,称为
Docker Registry
公开服务,最常用的是Docker Hub
(可以理解成类似Github
、Gitee
一样):提供 公共仓库(public repository) 服务,允许用户免费上传、下载公开的镜像;因此,Registry 和 Repository 是两个完全不同的概念;前者指注册的、提供这些服务的服务器,后者指可以被认为是一个具体的项目或目录;
例如,对于仓库地址
docker.io/ubuntu
来说,docker.io
是注册服务器地址,ubuntu
是仓库名;用户也还可以借助 ① Docker 开发团队的开源 Docker Registry 镜像;或 ② 第三方软件(如 Harbor 和 Sonatype Nexus),来实现本地搭建私有 Docker Registry;
Chapter 2. 使用镜像
2.1 镜像获取
从
Docker Hub
上获取:docker pull [options] [addr:port/]<RepositoryName>[:tag]
[options]
:请运行docker pull --help
查看;[addr:port/]
:默认docker.io:443
(Docker Hub
服务器);<RepositoryName>
:格式为<userName>/<softwareName>
,如果仅有softwareName
,则默认官方镜像(userName = library
);[:tag]
:镜像标签,可以理解成Git
中的tag
,起到标识镜像版本或分类的作用;同一个镜像必须有相同的 ID,可以有不同的tag
;
下载会一层层下载(分层存储),完成后会显示每层 ID 前 12 位和镜像整体的
sha256
摘要;
2.2 列出镜像
列出顶层镜像:
docker image ls
,显示 仓库名、标签、镜像 ID、创建时间和占用空间大小;关于 “占用空间大小”:由于 Docker 使用 Union FS 和多层存储结构,不同层间可以继承、复用,相同的层只需要保存一份即可,因此实际镜像硬盘占用空间很可能要比上面命令显示的小很多;
想要查看镜像、容器、数据卷所占用的真实空间,请运行:
docker system df
;虚悬镜像(dangling image):一种特殊的顶层镜像,这个镜像既没有仓库名,也没有标签,均为
<none>
;- 产生原因:① 同一标签的镜像因为官方的维护,ID 发生变更,这样在
docker pull
同个镜像tag
时,此镜像名被转移到了新下载的镜像身上,而旧的镜像上的这个名称则被取消;② 自己本地构建镜像(docker build
,以后介绍)时,由于新旧镜像同名,旧镜像名称被取消; - 查找虚悬镜像:
docker image ls -f dangling=true
(加-f
参数,--filter
过滤); - 删除虚悬镜像:虚悬镜像已经失去了存在的价值,可以随意删除:
docker image prune
;
- 产生原因:① 同一标签的镜像因为官方的维护,ID 发生变更,这样在
中间层镜像:想要看中间层镜像,需要使用
docker image ls -a
(加-a
参数);此时可能会出现没有标签的镜像,但它们不是虚悬镜像,而是中间层镜像,由于多层存储结构,它们相互依赖,千万不能盲目删除;
按条件筛选列出镜像:
docker image ls -f <dangling=bool/since=repositoryName/label=labelName>
输出格式化:
docker image ls --format "<GoTemplate>"
这里的
GoTemplate
是 Go 语言的模板语法,可以简单使用常见变量:{{.ID}}
、{{.Repository}}
、{{.Tag}}
;
2.3 删除镜像
镜像 ID 删除:
docker image rm <IMAGE_ID>
一般只有脚本使用完整 ID,人工一般都使用短 ID,对长度没有要求,只要能区分就行(一般 长度大于 3 就能区分)
repositoryName
删除:docker image rm <RepositoryName>
;镜像
sha256
摘要删除:docker image <name@sha256:VALUE>
;查询镜像
sha256
摘要:docker image ls --digests
;补充:
Untagged
和Deleted
的区别:请结合Git
自行品味;Git
和这个很相似;查询删除法:
docker image rm $(docker image ls -q <之前的查询条件>)
2.4 镜像定制和构建
注:第一次学习感觉有难度的可以先跳过本节,直接进入下一章 Chapter 3!
镜像的定制实际上就是定制每一层所添加的配置、文件。
正如官方文档所说:如果我们可以把每一层修改、安装、构建、操作的命令都写入一个脚本,用这个脚本来构建、定制镜像,那么之前提及的无法重复的问题、镜像构建透明性的问题、体积的问题就都会解决;
这个脚本(文本文档)就是
Dockerfile
。和Makefile
、CMakeLists
有异曲同工之妙,名字必须是Dockerfile
;
2.4.1 Dockerfile 定制
Dockerfile
包含了一条条的 指令(Instruction),每条指令构建一层,每行一条指令,末尾没有分号;
FROM
指令:指定基础镜像
语法:
FROM <REPO_NAME>
,<REPO_NAME>
为之前提到的存储库名;所谓定制镜像,那一定是以一个镜像为基础,在其上进行定制;
因此一个
Dockerfile
中FROM
是必备的指令,并且必须是第一条指令;Docker Hub
上有很多高质量基础镜像:- 服务类镜像:
nginx
、redis
、mongo
、mysql
、httpd
、php
、tomcat
等; - 语言应用类镜像:
node
、openjdk
、python
、ruby
、golang
等; - 操作系统类镜像(具有对应软件库可更自由地配置):
ubuntu
、debian
、centos
等;
- 服务类镜像:
空白基础镜像:
scratch
(意味着不以任何镜像为基础,接下来所写的指令将作为镜像第一层开始存在);不以任何系统为基础,直接将可执行文件复制进镜像的做法并不罕见:
- Linux 下静态编译的程序(所需的一切库都已经在可执行文件中,无需操作系统的运行时支持);
- 大部分使用 Go 语言开发的应用(Go 特别适合容器微服务架构的原因之一);
RUN
指令:执行命令
语法:
RUN <SH_COMMAND>
或RUN ["executableName", "param1", ...]
;RUN <SH_COMMAND>
参数会自动翻译为RUN ["sh", "-c", "<SH_COMMAND>"]
;
⚠ 注意:每一个
RUN
指令都会新建一层镜像层,因此从重用性角度出发,同一个目的的操作尽量写在一个RUN
语句中,例如:1
2
3
4
5
6
7
8
9
10# 这样的 Dockerfile 编写方式是不恰当的!!!结果就是产生非常臃肿、非常多层的镜像,不仅仅增加了构建部署的时间,也很容易出错。而且很多运行时不需要的东西,都被装进了镜像里,比如编译环境、更新的软件包等等
FROM debian:stretch
RUN apt-get update
RUN apt-get install -y gcc libc6-dev make wget
RUN wget -O redis.tar.gz "http://download.redis.io/releases/redis-5.0.3.tar.gz"
RUN mkdir -p /usr/src/redis
RUN tar -xzf redis.tar.gz -C /usr/src/redis --strip-components=1
RUN make -C /usr/src/redis
RUN make -C /usr/src/redis install1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16# 这段代码只有一个目的,就是编译、安装 redis 可执行文件,所以没有必要建立很多层,这只是一层的事情。
FROM debian:stretch
RUN set -x; buildDeps='gcc libc6-dev make wget' \
&& apt-get update \
&& apt-get install -y $buildDeps \
&& wget -O redis.tar.gz "http://download.redis.io/releases/redis-5.0.3.tar.gz" \
&& mkdir -p /usr/src/redis \
&& tar -xzf redis.tar.gz -C /usr/src/redis --strip-components=1 \
&& make -C /usr/src/redis \
&& make -C /usr/src/redis install \
# 以下清理文件是非常重要的步骤,确保每一层只添加真正需要添加的东西,任何无关的东西都应该清理掉。
&& rm -rf /var/lib/apt/lists/* \ # 清理了 apt 缓存文件
&& rm redis.tar.gz \ # 清理了所有下载、展开的文件
&& rm -r /usr/src/redis \
&& apt-get purge -y --auto-remove $buildDeps # 删除了为了编译构建所需要的软件和库⚠ 更重要的是,每个
RUN
指令由于所处的层数不一样,所以执行这个命令的命令行也不一样,这样就相当于开了新的命令行窗口,有的时候第一种写法甚至是错误的、无法运行的,例如:1
2RUN cd /app
RUN echo "hello" > world.txt你会发现
world.txt
根本不在/app/
下面,因为下一个RUN
的 console 已经不在/app/
下面了;这就是对 Docker 分层存储理解不透的结果。也正是这个原因,请不要把Dockerfile
作为 shell 脚本使用!想要实现上面的操作,可以改成:
RUN cd /app && echo "hello" > world.txt
;⚠ 如果你实在需要分成多个
RUN
步骤(例如为了容器层的可重用性——中间多一层可以有其他用处),但又希望命令行固定在某个目录下进行,请参考下文WORKDIR
指令的使用;
COPY
指令:复制文件
语法:
COPY [--chown=<user:group>] <src1>[, src2, ...], <dist1>[, dist2, ...]
;支持
--chown
参数更改文件所属用户和组;使用
COPY
指令,源文件的各种元数据都会保留。比如读、写、执行权限、文件变更时间等;src
源路径必须在上下文路径中,关于什么是上下文路径,建议立即查看 下文🔗;不仅可以指定多个文件,
src
还支持Go
语言的filepath.Match
通配符规则,可以简单认为:Regex 正则 Go::filepath.Match .* * .? ? [a-zA-Z] [{a-zA-Z}] 如果源路径为文件夹,复制的时候不是直接复制该文件夹,而是将文件夹中的内容复制到目标路径;举个例子:
1
2
3
4
5
6
7
8
9
10
11.
|
|--- Dockerfile
|--- ABC.txt
|--- testDir/
|
|--- DEF.txt
|--- testDir2/
|
|--- FGH.txt
|--- IJK.txt上面的项目目录,如果在 Dockerfile 中有这么一段:
1
2
3...
COPY . /app/
...并且构建镜像以
testDir
为上下文根目录:docker build -f Dockerfile -t XXX ./testDir/
,那么在容器中,testDir
本身不会在里面:1
2
3
4
5
6app/
|--- DEF.txt
|--- testDir2/
|
|--- FGH.txt
|--- IJK.txt
ADD
指令:自动解压并复制文件
- 和
COPY
语法几乎相同,真正需要ADD
的场景是自动解压缩,即<src>
路径是一个*.zip/*.bz/*.xz/*.gz/*.tar
时会自动解压缩;其他时候应该使用语义明确的COPY
;
CMD
指令:设置容器启动命令
用于指定默认的容器主进程的启动命令(容器启动指令
docker run
将在 3.1 介绍),语法和RUN
相同;如果指定了该命令,那么当运行
docker run
后,默认运行容器中的/bin/bash
的行为将被更改为CMD
后的指令;易错点:前台执行 && 后台执行;先问大家伙一个问题,下面的指令有意义吗?
1
2
3FROM nginx
RUN echo "<h1>Hello, nginx!</h1>" > /usr/share/nginx/html/index.html
CMD systemctl start nginx如果你认为没有任何问题的话,恭喜你,你成功地将 传统虚拟机 和 docker 容器搞混了! 请你好好复习 1.2 和 1.4 的内容:容器本身就是一种进程,它不是虚拟机、内部不存在守护进程,因此不允许在“后台”启动应用,必须在前台,否则会直接退出;
引用官方文档的一句话:对于容器而言,其启动程序就是容器应用进程,容器就是为了主进程而存在的,主进程退出,容器就失去了存在的意义,从而退出;
上面的
CMD systemctl start nginx
会被解释成:CMD ["sh", "-c", "service nginx start"]
,这样运行完这条指令后主进程直接结束!想要在容器生存周期内持续运行
nginx
,必须以指定前台的方式运行,如下:1
2
3FROM nginx
RUN echo "<h1>Hello, nginx!</h1>" > /usr/share/nginx/html/index.html
CMD ["nginx", "-g", "daemon off;"]
ENTRYPOINT
指令:设置容器入口点
语法和
RUN
、CMD
都相同,它存在的主要作用有两个:① 更方便地从外部添加运行参数;② 更方便地进行容器运行前准备工作;想理解主要作用,需要知道
ENTRYPOINT
如何工作:当指定ENTRYPOINT
后,CMD
指令的意义发生变化:CMD
默认值变为空,并且CMD
将作为ENTRYPOINT
的参数进行传递;这使得ENTRYPOINT
实现了上述两个优点;分别举一个栗子🌰说明:① 假设有一个镜像是这么设计的:
1
2
3
4
5FROM ubuntu:18.04
RUN apt-get update \
&& apt-get install -y curl \
&& rm -rf /var/lib/apt/lists/*
CMD [ "curl", "-s", "http://myip.ipip.net" ]如果想向里面的
curl
命令添加参数,例如-i
,是做不到的;(之后会介绍
docker run [options] <containerName> [CMD]
启动容器的命令,在”容器名“后面的参数是CMD
,会覆盖Dockerfile
中的CMD
指令)但是如果这么写就不一样了:
1
2
3
4
5FROM ubuntu:18.04
RUN apt-get update \
&& apt-get install -y curl \
&& rm -rf /var/lib/apt/lists/*
ENTRYPOINT [ "curl", "-s", "http://myip.ipip.net" ]这样在
docker run
最后指定CMD
时,这个参数会传递给ENTRYPOINT
作为参数,完美解决这个传参问题;② 再假设我们在使用一个数据库镜像,可能需要以
root
身份完成一些数据库配置、初始化的工作,再根据需要切换用户以保证安全性;比如官方Redis
镜像是这么做的:1
2
3
4
5
6
7
8
9
10
11# File: Dockerfile
FROM alpine:3.4
...
# root 身份建立用户组相关设置
RUN addgroup -S redis && adduser -S -G redis redis
...
ENTRYPOINT ["docker-entrypoint.sh"] # 容器启动后执行该脚本
EXPOSE 6379
CMD [ "redis-server" ] # 因为上文有 ENTRYPOINT,所以这里不是指容器的启动命令,
# 而是指传给 ENTRYPOINT 的默认参数是 "redis-server"1
2
3
4
5
6
7
8
9
10
11
12
...
# allow the container to be started with `--user`
if [ "$1" = 'redis-server' -a "$(id -u)" = '0' ]; then
find . \! -user redis -exec chown redis '{}' +
exec gosu redis "$0" "$@"
fi
exec "$@"
# 这个脚本指的就是根据 CMD 参数的内容来判断,如果是 redis-server 的话,
# 则切换到 redis 用户身份启动服务器,否则依旧使用 root 身份执行
ENV
指令:设置环境变量
语法:
ENV <key> <value>
或ENV <key1>=<value1> [<key2>=<value2> ...]
(语法特殊,只有多个环境变量参数间不用加逗号),环境变量将停留在容器的全生命周期中;它的作用:设置合适的环境变量可以让维护工作变得轻松,例如
node
官方镜像的 Dockerfile:1
2
3
4
5
6
7
8
9
10
11
12
13# ...
ENV NODE_VERSION 7.2.0
RUN curl -SLO "https://nodejs.org/dist/v$NODE_VERSION/node-v$NODE_VERSION-linux-x64.tar.xz" \
&& curl -SLO "https://nodejs.org/dist/v$NODE_VERSION/SHASUMS256.txt.asc" \
&& gpg --batch --decrypt --output SHASUMS256.txt SHASUMS256.txt.asc \
&& grep " node-v$NODE_VERSION-linux-x64.tar.xz\$" SHASUMS256.txt | sha256sum -c - \
&& tar -xJf "node-v$NODE_VERSION-linux-x64.tar.xz" -C /usr/local --strip-components=1 \
&& rm "node-v$NODE_VERSION-linux-x64.tar.xz" SHASUMS256.txt.asc SHASUMS256.txt \
&& ln -s /usr/local/bin/node /usr/local/bin/nodejs
# ...
ARG
指令:设置构建参数
语法与
ENV
相同,不过ARG
只是构建镜像时临时存在的环境变量,并且只在FROM
中生效,想要在其他语句中使用,必须重新指定,举几个栗子🌰:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45# 情况 1 ----------------------------
ARG DOCKER_USERNAME=library
FROM ${DOCKER_USERNAME}/alpine
RUN set -x ; echo ${DOCKER_USERNAME} # 这步是无效输出
# 情况 2 -----------------------------
ARG DOCKER_USERNAME=library
FROM ${DOCKER_USERNAME}/alpine
# 要想在 FROM 之后使用,必须再次指定
ARG DOCKER_USERNAME=library
RUN set -x ; echo ${DOCKER_USERNAME} # 这样才是有效输出
# 情况 3(多阶段构建,后面说) -----------
# 这个变量在每个 FROM 中都生效(使用的场合必须要都是 FROM)
ARG DOCKER_USERNAME=library
FROM ${DOCKER_USERNAME}/alpine
RUN set -x ; echo 1
FROM ${DOCKER_USERNAME}/alpine
RUN set -x ; echo 2
# 情况 4(多阶段构建)-------------------
ARG DOCKER_USERNAME=library
FROM ${DOCKER_USERNAME}/alpine
# 在FROM 之后使用变量,必须在每个阶段分别指定
ARG DOCKER_USERNAME=library
RUN set -x ; echo ${DOCKER_USERNAME} # 有效输出
FROM ${DOCKER_USERNAME}/alpine
# 在FROM 之后使用变量,必须在每个阶段分别指定
ARG DOCKER_USERNAME=library
RUN set -x ; echo ${DOCKER_USERNAME} # 有效输出
VOLUME
指令:定义匿名卷
语法:
VOLUME <inContainerPath>
或VOLUME "<inPath1>", "<inPath2>, ..."
;它的作用:在 之前 提到过,容器运行时应该尽量保持容器存储层不发生写操作,所以数据库类需要保存动态数据的应用,其数据库文件应该保存于卷(volume)中(后面 5.1 会介绍数据卷),而为了防止用户运行容器
docker run
忘记加-v
参数(后面说)来指定目录挂载卷,可以在Dockerfile
中写VOLUME
匿名卷,相当于“没有指定目录名的默认卷”;这样在运行时如果用户不指定挂载,其应用也可以正常运行,不会向容器存储层写入大量数据;
注意:匿名卷正因为匿名、没有指定在
Host OS
上的挂载目录,docker engine 会自动选择诸如:/var/lib/docker/
这样的一些位置存起来,在容器删除后也不会自动删除,得自己找,比较麻烦;所以尽量自己记得挂载数据卷,不要依赖匿名卷;
EXPOSE
指令:声明端口
- 语法:
EXPOSE <port1> [port2 ...]
(和ENV
一样,多个参数没逗号); - ⚠ 注意:是声明,而不是直接暴露,意味着在容器运行时并不会因为这个声明应用就会开启这个端口的服务;那它有什么用呢?
- 它的作用:① 帮助镜像使用者理解这个镜像服务的守护端口,以方便自行配置映射;② 当使用
docker run
的-P(大写)
参数(后面介绍,是将容器端口开放到Host OS
的随机端口)时,默认使用EXPOSE
声明的端口; - 辨析:
EXPOSE
不能和docker run
的-p(小写)
参数(也是后面介绍)作用搞混,后者是真的会进行从宿主到容器的端口映射,而前者只是声明;
WORKDIR
指令:指定工作目录
语法:
WORKDIR <inContainerDir>
(如果容器内该目录不存在,则会自动创建);它的作用:改变以后各层的工作目录位置,确保每一层的命令行默认位置都在该目录下;如果参数是相对路径,那么和之前的工作目录有关,例如:
1
2
3
4
5WORKDIR /a
# ...
WORKDIR b
WORKDIR c
RUN pwd # 此时在容器内命令行中打印的应该是 /a/b/c/
USER
指令:指定当前用户
语法:
USER <userName>[:userGroup]
;它的作用:改变之后层的执行
RUN
,CMD
以及ENTRYPOINT
这类命令的身份(之前说过,docker 容器有一套完整独立的命名空间);和
WORKDIR
的异同比较:- 和
WORKDIR
的作用效果类似,都是改变环境状态并影响以后的层; - 和
WORKDIR
不同的是,如果指定用户不存在,则无法切换——此指令不会自动创建用户;
- 和
⚠ 如果要建立一个用户、用户组(大多数时候用
RUN
),并且在RUN
的中途想要切换用户(不添加中间层的情况,就没法使用USER
),请 一定不要 使用su
和sudo
,因为通常在刚下载的原始镜像中都需要比较麻烦的配置,而且在 TTY 缺失的环境下经常出错;尤其是这种情况:如果容器内的应用需要 “优雅停机”(接收信号量
SIGABRT
),那么一定需要这个应用的进程在容器内的PID
= 1;而运行su/sudo
会先建立sudo
进程,再建立后面的进程,不能保证容器应用的PID
= 1!正确做法之一是下载并使用
gosu
,以redis
镜像中用户设置和切换为例:1
2
3
4
5
6
7
8# 建立 redis 用户和用户组
RUN groupadd -r redis && useradd -r -g redis redis
# 下载 gosu
RUN wget -O /usr/local/bin/gosu "https://github.com/tianon/gosu/releases/download/1.12/gosu-amd64" \
&& chmod +x /usr/local/bin/gosu \
&& gosu nobody true
# 设置 CMD,并使用 gosu 换另外的用户传参(gosu 使用方法:gosu + user + cmd)
CMD [ "exec", "gosu", "redis", "redis-server" ]至于使用
gosu
而不是su/sudo
的深层原因,我看有篇博客写的不错,如果感兴趣这方面的原理可以戳 这里🔗;不过看懂英文的话,最好看gosu
官方解释:github 传送门;另一种正确方法比较方便,是使用
chroot
的--userspec
参数:chroot --userspec=<userName>
,可以完成相似效果;
HEALTHCHECK
指令:健壮性检查
语法:
HEALTHCHECK [OPTIONS] CMD <HOST_COMMAND>
或HEALTHCHECK NONE
;HEALTHCHECK
和CMD
、ENTRYPOINT
一样,只可以出现一次,如果写了多个,只有最后一个生效;[options]
:--interval=<带单位的数值>
、--timeout=<...>
前面两个都默认30s
;--retries=<N>
默认 3 次,超过则认为不健康;CMD <...>
:检查时由宿主机向容器内执行的指令,指令返回值代表本次检查是否成功:0:成功, 1:失败, 2:放弃结果
;NONE
:如果基础镜像有健康检查指令,使用这行可以屏蔽掉其健康检查指令;
它的作用:引用官方文档的话:
在没有
HEALTHCHECK
指令前,Docker engine 只可以通过容器内主进程是否退出来判断容器是否状态异常。很多情况下这没问题,但是如果程序进入死锁状态,或者死循环状态,应用进程并不退出,但是该容器已经无法提供服务了。而自 1.12 之后,Docker 提供了
HEALTHCHECK
指令,通过该指令指定一行命令,用这行命令来判断容器主进程的服务状态是否还正常,从而比较真实地反应容器实际状态。举个例子🌰:对于一个网络服务而言,如果需要检查 WEB 服务是否还有响应,那么可以这么写:
1
2
3
4FROM nginx
RUN apt-get update && apt-get install -y curl && rm -rf /var/lib/apt/lists/*
HEALTHCHECK --interval=5s --timeout=3s \
CMD curl -fs http://localhost/ || exit 1 # 使用 curl 来判断健康检查的日志可以使用
docker inspect --format '{{json .State.Health}}' <containerName>
来查看(json 格式);
ONBUILD
指令:多级构建准备
语法:
ONBUILD <所有其他指令>
;它的效果:它后面跟的是其它指令,而在当前镜像构建时并不会被执行。只有当以当前镜像为基础镜像,去构建下一级镜像的时候才会被执行;
它的作用:提升镜像多级构建的重用性和扩展性;这句非常抽象,需要举例说明;首先说明什么是多级构建——利用已构建好的镜像为基础,构建下一级镜像(事实上所有镜像都是这么构建的,但多级构建更强调这几个镜像都是自己写出来的)
以下的例子涉及
node.js
相关知识,只需了解node.js
使用npm
作为包管理器之一,相关包依赖和启动信息存在package.json
中,可以类比成Python
中的pip
和requirements.txt
;部署项目时需要在根目录下执行npm install
安装依赖,才能运行项目;🌰 假设我们在一个
node.js
项目中,目录情况如下:1
2
3
4
5.
|
|--- Dockerfile
|--- package.json
|--- ... # 其他项目文件因此 Dockerfile 应该这么写:
1
2
3
4
5
6FROM node:slim
WORKDIR /app # 创建并以 /app 为工作目录
COPY ./package.json /app # 将当前上下文目录下的 package.json 文件复制到镜像中的 /app/ 下
RUN [ "npm", "install" ] # 执行 sh -c npm install 安装项目依赖
COPY . /app/ # 将当前上下文目录中(即客户端中的项目根目录)所有文件复制到 /app/ 下
CMD [ "npm", "start" ] # 设置容器默认启动命令为 npm start很好。那如果还有一个项目,项目文件不一样,但依赖的包和它一模一样,如果想要定制为镜像,应该怎么办?你可能会说,简单!把 Dockerfile 直接复制过去不就行了?好,那如果还有 5 个、10个呢?直接复制 Dockerfile 一定不现实,会给版本控制造成极大阻碍(例如,如果你想把这些所有项目的镜像基础
node:slim
换成node:alpine
,那你得一个个改);聪明的人会想,这也好办,直接把和项目无关的部分提出来——这样公共部分只要修改一次就行(这就是多级构建的思想,注意和后面的 多阶段构建区分):
1
2
3
4# 公共镜像的 Dockerfile
FROM node:slim
WORKDIR /app
CMD [ "npm", "start" ]假设这个生成的镜像名叫
my-node
,那么某一个用到这个镜像的项目的 Dockerfile 可以这么写:1
2
3
4
5# 用到公共镜像的某个项目镜像的 Dockerfile
FROM my-node
COPY ./package.json /app
RUN [ "npm", "install" ]
COPY . /app/这么进行 “多级构建” 是没有问题的;但是,如果项目创建容器时,我想给每个项目的启动命令
npm install
都加个相同的参数呢?哦吼,完了,问题又回去了,是不是要一个个改呢……有些同学会说:那把RUN
挪到公共镜像的 Dockerfile 中?不行。因为下面的COPY
和RUN
和项目有关,如果这样构建,某个项目的文件就进入公共镜像中,显然不符合重用的要求;这个时候,应该用
ONBUILD
完成:1
2
3
4
5
6
7
8# 公共镜像的 Dockerfile
FROM node:slim
RUN mkdir /app
WORKDIR /app
ONBUILD COPY ./package.json /app
ONBUILD RUN [ "npm", "install" ]
ONBUILD COPY . /app/
CMD [ "npm", "start" ]这样
ONBUILD
这几步在构建公共镜像时不会运行,而各个项目只需要写:1
FROM my-node
这一句话就行!是不是非常方便!
LABEL
指令:添加镜像元数据 metadata
一般是用来申明镜像的作者、文档地址等:
1
2LABEL org.opencontainers.image.authors="XXX"
LABEL org.opencontainers.image.documentation="https://XXX"
SHELL
指令:指定命令行运行参数
语法:
SHELL ["executable", "params"]
作用:用来指定
RUN
ENTRYPOINT
CMD
指令的 shell,Linux 中默认为["/bin/sh", "-c"]
1
2
3SHELL ["/bin/sh", "-cex"]
# 命令转为 /bin/sh -cex "nginx"
ENTRYPOINT nginx
很重要的网站
Docker
官方镜像Dockerfile
:Dockerfile contents - github;在你不知道官方镜像的设计,或者想学习官方 Dockerfile 的用法时,这是极好的了解途径;
2.4.2 从容器快照定制
见 3.4 容器的导入和导出
2.4.3 构建镜像
语法:
docker build [options] <contextPath>
[options]
中 最常用的是-t <tagName>
,给生成的镜像固定一个标签、-f <DockerfilePath>
指定Dockerfile
;<contextPath>
指上下文路径,想要理解它,就需要理解部分docker build
的工作原理:Docker 采用 C/S 架构设计,在运行时分为 Docker Engine(也就是服务端守护进程
containerd
)和客户端工具(图片最上面的 4 个);Docker 的引擎提供了一组 REST API,被称为 Docker Remote API,而如
docker
命令这样的客户端工具,则是通过这组 API 与 Docker 引擎交互,从而完成各种功能;这种 C/S 分离式的设计,使得操作远程服务器的 docker engine 和本地一样轻松;所以什么是上下文路径(contextPath)?
构建镜像时,构建操作一定是在服务器端进行(C/S 架构),因此服务端会请求将
Dockerfile
所在目录(这不是 contextPath,因为默认不用-f DockerfileName
指定的,引擎默认是上下文目录下的Dockerfile
)下的所有文件打包,并从客户端传递给Docker Engine
;正因如此,① 应该将
Dockerfile
置于一个空目录下,或者项目根目录下;② 如果该目录下没有所需文件,那么应该把所需文件复制一份过来;
③ 如果目录下有些东西确实不希望构建时传给 Docker 引擎,那么可以用
.gitignore
一样的语法写一个.dockerignore
,该文件是用于剔除不需要作为上下文传递给 Docker 引擎的;真正的 contextPath 是:
docker build
指定要压缩给服务器的目录,并且从此以后,Dockerfile
中的所有的相对路径(如COPY
等命令,也强烈建议是相对路径)的 “.
” 都代表docker build
中指定的上下文目录;
举个两个栗子🌰🌰帮助理解上面内容:
假设有一个项目结构是这样的,想要添加到
nginx
镜像中合成新的镜像:1
2
3
4
5
6
7.
|
|--- Dockerfile
|--- .dockerignore
|
|--- index.html
|--- webConfig # nginx 配置文件由于
Dockerfile
恰在项目根目录下,因此可以这么写(这里只是演示什么是上下文目录,不建议这么做,建议使用数据卷或挂载主机目录):1
2
3FROM nginx
COPY ./index.html /usr/share/nginx/html/index.html
COPY ./webConfig /etc/nginx/site-avaliable/webConfig那么指令应该这么写:
docker build -t my_nginx:v1 .
这里:
① 无需指定
Dockerfile
,是因为上下文目录恰好指定的是当前目录,该目录下又恰好有Dockerfile
,因此不用指定;②
Dockerfile
中COPY
语句中的 “源目录(第一参数)” 中的 “.
” 指的是 contextPath,也是在服务器端解压后的 “.
”,而非客户端的“当前目录”;再假设有一个项目结构是这样的,想以
src/
为整个项目打包进容器:1
2
3
4
5
6
7
8.
|
|--- Dockerfile
|--- .dockerignore
|
|--- src/
|--- index.html
|--- webConfig # nginx 配置文件那么如果
Dockerfile
这么写:1
2
3FROM nginx
COPY ./index.html /usr/share/nginx/html/index.html
COPY ./webConfig /etc/nginx/site-avaliable/webConfig则指令应该这么写:
docker build -f ./Dockerfile -t my_nginx:v1 ./src/
这里:
① 指定了上下文路径是
src/
,意味着只会给 docker engine 压缩打包这个目录,仅此而已;② 在这个上下文目录(
src/
)中,没有Dockerfile
,因此需要指定Dockerfile
在客户端的位置;-f
参数指定Dockerfile
,这个路径和 contextPath 无关,真的是客户端的路径;③ 在
Dockerfile
中,COPY
的源路径(第一参数)中的 “.
” 可以理解为的是在服务器端刚解压后的当前路径,如下:1
2
3
4. <--- 它就是上下文路径在 Dockerfile 中表示的含义
|
|--- index.html
|--- webConfig
2.4.4 多阶段构建
多阶段构建和多级构建的对比:后者的目的是为了提升重用性,多写了几个镜像并依赖构建;前者是因为原本的镜像过于庞大,要拆解镜像层次,人为降低镜像的体积;前者一次只会一次构建出一个镜像,后者强调分步构建好几个镜像;
使用方法:就是一个 Dockerfile 文件中写好几个
FROM
,引入as
关键字,使得外部可以通过docker build
的--target
参数指定构建的阶段;并且多阶段之间的镜像可以相互复制文件!举个例子🌰:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24FROM golang:alpine as builder
RUN apk --no-cache add git
WORKDIR /go/src/github.com/go/helloworld/
RUN go get -d -v github.com/go-sql-driver/mysql
COPY app.go .
RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o app .
FROM alpine:latest as prod
RUN apk --no-cache add ca-certificates
WORKDIR /root/
# 这里是使用上一阶段(--from=0) “builder” 镜像的文件
# 0 是“上一个”的简写,还可以 --from=builder
COPY --from=0 /go/src/github.com/go/helloworld/app .
CMD ["./app"]也可以只构建
builder
:docker build --target builder -t XXX:XX .
⚠ 当需要的容器服务过于多的时候,写
Dockerfile
变得很繁琐(例如一个镜像中同时需要 python、nginx、redis 服务等),建议使用docker compose
(见 Chapter 7);
Chapter 3. 操作容器
3.1 创建和启动容器
3.1.1 新建容器并启动
语法:
docker run [options] <IMAGE_NAME or ID> [CMD]
;[options]
:①-i
启动容器内 bash 并绑定于 stdin 允许交互;②
-t
分配伪终端(显示命令提示符,常和-i
联用将容器 I/O 绑定在当前 TTY 上:-it
);如果使用
-i
参数,[CMD]
为必须项,且一般填写/bin/bash
或 sh;③
-d
拒绝和当前终端连接(detach,在启动容器后不与当前 TTY 关联,与-it
互斥,此时 stdout 的内容可以使用docker container logs
查看);④
-p <host_port:container_port>
将容器指定端口映射到主机指定端口;⑤
-P
将容器端口开放到Host OS
的随机端口;⑥
-v <host_dir:container_dir>
将主机指定目录作为数据卷挂载到容器指定目录上,不会覆盖 Dockerfile 中的VOLUME
指令。因为如果有VOLUME
指令,那么在构建镜像时匿名卷已被创建;(详细见 Chapter 5)⑦
--name <containerName>
为容器命名,在容器互联、集群时非常重要;⑧
--network
参数见 6.2 容器互联;[CMD]
:会覆盖 Dockerfile 中的CMD
指令;
运行该命令后 docker engine 在干什么:
- 检查本地是否存在指定的镜像,不存在就从 Docker Registory 下载;
- 利用镜像创建并启动一个容器;
- 分配一个文件系统,并在只读的镜像层外面挂载一层可读写层;
- 从宿主主机配置的网桥接口中桥接一个虚拟接口到容器中去;
- 从地址池配置一个 ip 地址给容器;
- 执行用户指定的应用程序;
- 执行完毕后容器被终止;
3.1.2 启动已终止的容器
就一句:docker container start <ID>
(如果容器没有exit,应该用 restart
);
3.2 终止容器
也就一句:docker container stop <ID>
;
此外,查看所有正在运行的容器:docker container ls
;
⚠ 处于终止状态的容器需要在后面加 -a
参数:docker container ls -a
;
3.3 进入容器
本质上就是在容器中创建一个 bash 程序,并把 I/O 绑定到当前 TTY 上。总共两种方法:
docker attach <ID>
,不建议使用,因为执行exit
退出这个 bash 后,会给整个容器发送 SIGABRT 信号,导致终止;docker exec -it <ID> bash
:使用exit
退出 bash 不影响原来容器;显然docker exec
不止可以进入容器,还能干其他事(把-it
去掉,最后一个参数换成要运行的命令等等,详细内容请参阅官方文档,或者运行docker exec --help
);
3.4 导入和导出
将容器保存为快照(这会丢弃容器的元数据):
docker export <ID> > compressFile.tar
;将容器快照展开为镜像(这是镜像的另一种创建方式:从容器创建):
cat compressFile.tar | docker import - <REPO_NAME>
复习一下,
REPO_NAME
=userName/IMAGE_NAME:tag
;除了管道流方法以外,还可以从指定地址展开镜像:
docker import http://example.com/exampleimage.tgz example/imagerepo
;
3.5 删除容器
删除处于终止状态的容器:
docker container rm <ID>
;- 如果需要删除任何状态的容器(包括正在运行的),则需添加
-f
参数,这会给正在运行的容器先发送SIGKILL
信号; - 如果实在需要在停止容器的同时删除挂载的数据卷:添加
-v
参数;
- 如果需要删除任何状态的容器(包括正在运行的),则需添加
清除所有处于终止状态的容器:
docker container prune
复习一下:清除所有虚悬镜像:
docker image prune
Chapter 4. 访问仓库
4.1 Docker Hub
登录/登出 docker 账户:
docker login/logout
;查找相关镜像:
docker search [--filter=starts=N] <...>
(里面示例筛选器是筛选 N 星以上的镜像);推送镜像到自己账户的公共仓库中(需登录):
docker push <userName/IMGAE_NAME:tag>
;提示:目前自动构建(Automated build)仅支持付费用户,没钱没用过,不讨论;
4.2 私有仓库管理
docker-registry
相关命令,平时没啥人用,不作介绍;
Chapter 5. 数据管理
5.1 数据卷
在之前已经无数次接触到数据卷的概念,现在详细说明数据卷的概念和使用;
概念和特征:是一个可供一个或多个容器使用的特殊目录,可以理解为 Linux 下对目录或文件进行
mount
;数据卷可以在容器之间共享和重用;
对数据卷的修改会立马生效;但对数据卷的更新,不会影响镜像(挂载的特征);
数据卷默认会一直存在,即使容器被删除(没有自动回收机制,独立于容器,需要手动删除)
数据无价,谨慎删除;
匿名卷也是一个数据卷;
管理数据卷:docker 支持在没有启动容器时,手动对数据卷进行管理;
查看所有数据卷:
docker volume ls
;查看特定数据卷详细信息:
docker volume inspect <volumeName>
;创建数据卷:
docker volume create <volumeName>
;和匿名卷一样,默认挂载于
/var/lib/docker/containers/
中;启动一个挂载指定数据卷的容器:3.1.1 介绍过,
-v
参数,等价于:--mount source=<volumeName>,target=<containerPath>
;手动删除数据卷:
docker volume rm <volumeName>
;清理没有被当前容器使用的数据卷:
docker volume prune
;
5.2 挂载主机目录
和数据卷比较:挂载主机目录和挂载数据卷极其类似,不过前者是自定义位置,后者默认和匿名卷都放在一起;
使用:
docker run
的-v
参数,如果 Host OS 中没有指定目录,会自动创建;或者:
docker run
的--mount type=bind,source=<dir>,target=<cDir>
,如果本地目录不存在,则报错;也可以仅赋予只读权限:
--mount type=bind,source=<dir>,target=<cDir>,readonly
;甚至可以只挂载一个主机文件;
Chapter 6. 网络配置
6.1 外部访问
绝大部分内容已在 3.1.1 启动容器时介绍,这里仅介绍一些高级用法;
-p
参数可以多次使用来绑定多个端口:docker run -d -p 80:80 -p 443:443 nginx:alpine
-P
参数的 “随机” 是针对 Host OS 的端口随机分配,一般会按照 Dockerfile 中的EXPOSE
分配指定的容器内部端口;等价于在
docker run
中加入参数:-p 127.0.0.1::<EXPOSE_port>
6.2 容器互联
以前会使用docker run
的 --link
参数,进行点对点的连接;但随着结点数的增大,这么做不容于配置;正确做法是自行配置容器间的网络;
新建容器网络:
docker network create -d bridge <netName>
-d
参数指定网络类型,参数值有bridge
(网桥) 和overlay
,前者用的多,后者需要配合swarm mode
配置集群,暂时不介绍;运行容器同时将容器连接到网络:
docker run
的--network <netName>
注意:如果要容器连接网络,强烈建议自己为容器命名:
--name
参数,因为容器间交互识别就靠容器名——在一个容器中可以访问同网络的另一个容器:ping <containerName>
;
其实更方便的做法是:使用 Docker compose
(Chapter 7);
6.3 DNS 配置
这个操作在特殊场合有用(因为一般默认容器中的 DNS 设置就够用了),比如某些同学想用 Grasscutter
建立 Ys 私服的时候,包装成 Docker 镜像时就需要将一系列网络代理到本机上——如果使用 DNS 劫持的方法就需要在容器内配置 DNS;
最方便的方法是构建镜像时自己写一个 HOST
文件在放到/etc/hosts
;
当然,也可以通过 docker run
的 -h HOSTNAME
写入 /etc/hosts
和 /etc/hostname
,还可以通过 docker run
的 --dns=IP_ADDR
写入 DNS 服务器地址(在 /etc/resolv.conf
);
还可以通过修改主机 /etc/docker/daemon.json
来同时修改所有容器的 DNS 服务器:
1 | { |
Chapter 7. Docker Compose
7.1 简介
地位:由
Python
编写的 Docker 官方的开源项目;定位:定义和运行多个 Docker 容器的应用(Defining and running multi-container Docker applications);
作用:允许用户通过一个单独的
docker-compose.yml
模板文件(YAML 格式)来定义一组相关联的应用容器为一个项目(project),很方便地实现容器运行设置、互联;两个重要概念:服务和项目
- 服务 (
service
):一个应用的容器,实际上可以包括若干运行相同镜像的容器实例; - 项目 (
project
):由一组关联的应用容器组成的一个完整业务单元,在docker-compose.yml
文件中定义;
Compose
的默认管理对象是项目,通过子命令对项目中的一组容器进行便捷地生命周期管理;- 服务 (
和 docker 普通用法的比较【重要】:docker compose 不在于构建镜像,而在于直接一条龙拉取已有镜像/按
Dockerfile
构建自定义镜像(前面章节的内容),并按配置运行一组容器;举例认识🌰:假设有一个项目,使用
Python
建立一个记录页面访问次数的 Web 服务,使用Flask
框架、redis
服务器;(本部分无需看懂,只需要感受
docker compose
使用便捷就行)如果使用 docker 原来的方法,编写
Dockerfile
来构建容器,那么需要作很多事:从一个 Python 镜像中开始构建、安装 flask、安装 redis、复制项目文件、删除安装的 apt 缓存和安装包等文件,运行docker run
还要加各种参数,例如挂载、端口、网络互联……但如果使用
docker compose
就不一样了:编写模板文件:
1
2
3
4
5
6
7
8
9
10
11# File: docker-compose.yml
version: '3'
services: # 容器内的所有服务
web: # web 服务
build: . # 镜像构建方法是当前页面的自定义镜像:./Dockerfile
ports: # 运行该 web 服务的容器时,进行端口映射,相当于 docker run 的 -p
- "5000:5000"
redis: # redis 服务
image: "redis:alpine" # 镜像构建方法是直接从 redis:alpine 拉取编写 web 服务的镜像构建 Dockerfile(python + flask + 项目文件):
1
2
3
4
5FROM python:3.6-alpine
ADD . /code
WORKDIR /code
RUN pip install redis flask
CMD ["python", "app.py"]最后直接运行:
docker compose up -d
,结束!
7.2 安装
需要已经安装 Docker 及其服务;
1 | DOCKER_CONFIG=/usr/local/lib/docker/cli-plugins |
7.3 compose 命令
对于 Compose 来说,大部分命令的对象既可以是项目本身,也可以指定为项目中的服务或者容器。如果没有特别的说明,命令对象将是项目,这意味着项目中所有的服务都会受到命令影响;
语法:docker compose [options] [compose-command]
;
[options] 参数
-f template
指定 docker compose 模板,默认docker-compose.yml
;-p Name
指定项目名称;--verbose
运行时调试信息更详细;
[compose-command] 命令
构建&运行命令
pull
:拉取模板中的基础镜像;build [options]
:按模板构建项目中的所有镜像并组合为容器;--force-rm
:删除构建过程中产生的临时镜像;--no-cache
: 不使用缓存构建;
start [serviceName]
:启动已存在的指定服务容器;restart [-t TIMEOUT]
:重启项目中的服务;-t TIMEOUT
:重启前停止容器;
run [options] <serviceName> [CMD]
:手动按docker run
一样的参数启动指定容器,用的少,因为不如在docker-compose
里设置并且up
启动;up [options]
:强大的命令,可以一次性实现上述所有命令(构建镜像、(重新)创建服务、启动服务,并关联服务相关容器);-d
(detach):后台运行;- 其他选项和上面的前 4 条命令一样;
停止&管理命令
down
:停止up
所启动的容器,同时移除网络;stop [serviceName]
:仅停止指定服务容器,不删除任何东西,可以通过start
再启动;kill [-s SIGNAL] [serviceName]
:通过-s
传递终止信号结束服务容器 ;pause/unpause [serviceName]
:暂停指定服务容器 / 恢复被暂停的服务容器;rm [options] [serviceName]
:删除所有处于停止状态的服务容器,options 参数和 docker 一样;exec [options]
:进入指定容器,参数和 docker 一样;
维护命令
images
:列出模板中所含的所有镜像;ps
:列出项目中当前所有容器;top
:查看各服务容器里运行的进程(这样就不用在容器里装procps
包了);logs [serviceName]
:查看某个服务容器的 stdout 输出,对调试有用;version
:版本信息;help
:帮助;
7.4 docker-compose 模板
和 7.1 里说的一样,docker-compose.yml
同时完成指定构建什么镜像(自己不会设计镜像)和 docker run
的工作,所有可以将下面的选项和前几章的指令一一对照;
下面直接在文件中说明:
1 | version: "3" # 指定一个版本 |
还有两个重要选项 和 一个重要的行为需要单独拎出来说:
① volumes
挂载选项:对应 Dockerfile 的 VOLUME
指令,用法如下:
1 | version: "3" |
② networks
网络选项:对应 docker run 的 --network
参数,用法如下:
1 | version: "3" |
⚠ 重要行为:docker-compose.yml
读取环境变量
如果在模板文件中使用 ${XXX}
的变量,模板会先搜索系统环境变量,再搜索之前设置的 env_files/environment
选项;
docker 入门基础篇完【EOF】
预计将会跟进 docker 底层实现分析、Kubernetes
集群 等内容;