目录
- 目录
- 1. Docker 的技术实现
- 2. Docker 的理念
- 3. Docker 的核心组成
- 4. 安装 Docker
- 5. 镜像与容器
- 6. 镜像仓库
- 7. 运行和管理容器
- 8. 为容器配置网络
- 9. 管理和存储数据
- 10. 保存和共享镜像
- 11. 通过 Dockerfile 创建镜像
- 12. Dockerfile 使用技巧
- 13. 使用 Docker Compose 管理容器
1. Docker 的技术实现
Docker 的实现,主要归结于三大技术:命名空间 (Namespaces)、控制组 (Control Groups) 和联合文件系统 (Union File System)。
Namespace
命名空间是 Linux 内核在2.4
版本之后逐渐引入的一项用于进程运行隔离的模块。
和很多编程语言中命名空间的概念类似,Linux Kernel 中的 Namespace 能够将计算机资源进行切割划分,形成各自独立的空间。
就实现而言,命名空间可以分为很多具体的子系统,如User Namespace
、Net Namespace
、PID Namespace
、Mount Namespace
等等。
利用PID Namespace
,Docker 就实现了容器中运行进程相互隔离这一目标。
CGroups
资源控制组(常缩写为CGroups
)是 Linux 内核在2.6
版本后逐渐引入的一项对计算机资源进行控制的模块。
顾名思义,CGroups 的作用就是控制计算机资源。它与 Namespace 的对比如下:
Namespace
:以隔离进程、网络、文件系统等虚拟资源为目的CGroups
:主要做的是硬件资源的隔离
虚拟化除了制造出虚拟的环境以隔离统一物理平台运行的不同程序之外,另一大作用就是控制硬件资源的分配。CGroups
的使用正是为了这样的目的。
CGroups 除了隔离硬件资源,还有控制资源分配这个关键性作用。通过 CGroups,我们可以指定任意一个隔离环境对任意资源的占用值或占用率,在很多分布式场景下会很有帮助
Union File System
联合文件系统(Union File System)是一种能够同时挂载不同实际文件或文件夹到同一目录,形成一种联合文件结构的文件系统。Docker 创新性的将其引入到容器实现中,用它解决虚拟环境对文件系统占用过量、实现虚拟环境快速启停等问题。
在 Docker 中,提供了一种对 UnionFS 的改进实现,也就是 AUFS(Advanced Union File System)。
AUFS 将文件的更新挂载到旧的文件上,而不去修改那些不更新的内容(类似差量更新的概念)。这样一来,Docker 就大幅减少了虚拟文件系统对物理存储空间的占用。
2. Docker 的理念
先来看一张 Docker 官方提供的容器结构设计架构图:
与其他虚拟化实现甚至其他容器引擎不同的是,Docker 推崇一种轻量级容器结构,即一个应用一个容器。
Docker 的轻量级容器实现和虚拟机的相关参数对比如下:
属性 | Docker | 虚拟机 |
---|---|---|
启动速度 | 秒级 | 分钟级 |
硬盘使用 | MB 级 | GB 级 |
性能 | 接近原生 | 较低 |
普通机器支撑量 | 数百个 | 几个 |
3. Docker 的核心组成
之前提到了 Docker 实现容器引擎的一些技术,但都是相对底层的原理实现。在 Docker 将它们进行封装后,我们并不会直接去操作它们。在 Docker 中,还另外提供了一些软件层面的概念,这才是我们操作 Docker 所针对的对象
在 Docker 的体系中,有四大基本组件(Object):
- 镜像(Image)
- 容器(Container)
- 网络(Network)
- 数据卷(Volume)
镜像
镜像(Image)也是其他虚拟化技术中常常使用的一个概念。所谓镜像,可以理解为一个只读的文件包,其中包含了虚拟环境运行最原始文件系统的内容。
Docker 的镜像与虚拟机中的镜像还是存在一定区别的。首先,Docker 利用 AUFS 作为底层文件系统实现。通过这种方式,Docker 实现了一种增量式的镜像结构:
每次对镜像内容的修改,Docker 都会将这些修改写入一个新镜像层。因此,Docker 镜像实质上是无法修改的,因为所有对镜像的修改只会产生新的镜像,而不是更新原有的镜像。
容器
在容器技术中,容器(Container)就是用来隔离虚拟环境的基础设施。而在 Docker 里,它也被引申为隔离出来的虚拟环境。
可以将镜像理解为编程中的类,那么容器就是类的一个实例。镜像内存放的是不可变化的东西,而当以它们为基础的容器启动后,容器内也就成为了一个“活”的空间
网络
Docker 实现了强大的网络功能,我们不但能够轻松的对每个容器的网络进行配置,还可以在容器间建立虚拟网络,将多个容器包裹其中,同时与其他网络环境隔离。
另外,Docker 还可以在容器中构建独立的域名解析环境,这使得我们可以在不修改代码和配置的前提下直接迁移容器,而 Docker 会为我们完成新环境的网络适配。
对于这个功能,甚至可以在不同的物理服务器之间实现,让处在两台物理机上的两个 Docker 容器,加入到同一个虚拟网络中,形成完全屏蔽硬件的效果
数据卷
得益于 Docker 底层 UnionFS 技术的支持,我们除了能够从宿主机操作系统中挂载目录之外,还可以建立独立的目录以持久化存放数据,或者在容器之间共享数据。
在 Docker 中,通过这几种方式进行数据共享或持久化的文件或目录,我们都称之为数据卷(Volume)。
Docker Engine
目前这款实现容器化的工具是由 Docker 官方进行维护的,Docker 官方将其命名为 Docker Engine,同时定义其为工业级的容器引擎(Industry-standard Container Engine)。在 Docker Engine 中,实现了 Docker 技术最核心的部分——容器引擎。
docker daemon
深究 Docker Engine,会发现它其实是由多个独立软件所组成的软件包。在这些程序中,最核心的就是 docker daemon 和 docker CLI。
Docker 所提供的容器管理、应用编排、镜像分发等功能,都集中在了 docker daemon 中。而我们之前所提到的镜像模块、容器模块、数据卷模块和网络模块也都实现在其中。
在操作系统中,docker daemon 通常以服务的形式运行以便静默的提供这些功能,所以我们通常称之为 Docker 服务
docker CLI
在 docker daemon 管理容器等相关资源的同时,它也向外暴露了一套 RESTful API,我们能够通过这套接口对 docker daemon 中运行的容器和其他资源进行管理。
为了方便我们通过控制台对 docker daemon 进行管理,Docker Engine 直接附带了 docker CLI 这个控制台程序。
容易看出,docker daemon 和 docker CLI 组成了一个标准的 C/S 结构应用程序。而衔接这两者的,正是 docker daemon 所提供的 RESTful API
4. 安装 Docker
docker version
> docker version
Client:
Version: 18.06.1-ce
API version: 1.38
Go version: go1.10.3
Git commit: e68fc7a
Built: Tue Aug 21 17:24:56 2018
OS/Arch: linux/amd64
Experimental: false
Server:
Engine:
Version: 18.06.1-ce
API version: 1.38 (minimum version 1.12)
Go version: go1.10.3
Git commit: e68fc7a
Built: Tue Aug 21 17:23:21 2018
OS/Arch: linux/amd64
Experimental: false
docker info
> docker info
Containers: 32
Running: 16
Paused: 0
Stopped: 16
Images: 33
Server Version: 18.06.1-ce
Storage Driver: overlay2
Backing Filesystem: extfs
Supports d_type: true
Native Overlay Diff: true
Logging Driver: json-file
Cgroup Driver: cgroupfs
Plugins:
Volume: local
Network: bridge host macvlan null overlay
Log: awslogs fluentd gcplogs gelf journald json-file logentries splunk syslog
Swarm: inactive
Runtimes: runc
Default Runtime: runc
Init Binary: docker-init
containerd version: 468a545b9edcd5932818eb9de8e72413e616e86e
runc version: 69663f0bd4b60df09991c08812a60108003fa340
init version: fec3683
Security Options:
apparmor
seccomp
Profile: default
Kernel Version: 4.15.0-38-generic
Operating System: Ubuntu 18.04.1 LTS
OSType: linux
Architecture: x86_64
CPUs: 8
Total Memory: 31.21GiB
Name: abelsu7-ubuntu
ID: RT3B:UYYD:MO4K:IMYS:3TG6:ZKGT:PUUK:DZBO:4FF5:KUA5:2OH7:YTDL
Docker Root Dir: /var/lib/docker
Debug Mode (client): false
Debug Mode (server): false
Registry: https://index.docker.io/v1/
Labels:
Experimental: false
Insecure Registries:
127.0.0.0/8
Live Restore Enabled: false
配置国内镜像源
修改/etc/docker/daemon.json
(若文件不存在则直接新建)这个 Docker 服务的配置文件:
{
"registry-mirrors": [
"https://registry.docker-cn.com"
]
}
之后重启 Docker 使配置生效:
> sudo systemctl restart docker
可通过docker info
来查阅当前注册的镜像源列表:
> docker info
## ......
Registry Mirrors:
https://registry.docker-cn.com/
## ......
5. 镜像与容器
Docker 镜像
可以将 Docker 镜像理解为包含应用程序及其相关依赖的一个基础文件系统,在 Docker 容器启动的过程中,它会以只读的方式被用于创建容器的运行环境。
深入镜像实现
与其他虚拟机的镜像管理不同,Docker 将镜像管理纳入到了自身设计中,也就是说,所有的 Docker 镜像都是按照 Docker 所设定的逻辑打包的,也是受到 Docker Engine 所控制的。
对于每一个记录文件系统修改的镜像层来说,Docker 都会根据它们的信息生成一个 Hash 码,这是一个长度为 64 位的字符串,足以保证全球唯一性
由于镜像层都拥有唯一的编码,我们就能够区分不同的镜像层并保证它们的内容与编码是一致的,从而允许我们在镜像之间共享镜像层:
查看镜像
使用docker images
命令查看当前连接的 docker daemon 中存放和管理了哪些镜像:
> docker images
REPOSITORY TAG IMAGE ID CREATED SIZE
redis alpine a5cff96d7b8f 5 weeks ago 50.8MB
k8s.gcr.io/kube-controller-manager v1.13.3 0482f6400933 5 weeks ago 146MB
k8s.gcr.io/kube-proxy v1.13.3 98db19758ad4 5 weeks ago 80.3MB
k8s.gcr.io/kube-apiserver v1.13.3 fe242e556a99 5 weeks ago 181MB
k8s.gcr.io/kube-scheduler v1.13.3 3a6f709e97a0 5 weeks ago 79.6MB
quay.io/coreos/flannel v0.11.0-amd64 ff281650a721 6 weeks ago 52.6MB
ubuntu 16.04 b0ef3016420a 2 months ago 117MB
influxdb latest 623f651910b3 3 months ago 238MB
memcached latest 8230c836a4b3 3 months ago 62.2MB
mongo 3.2 fb885d89ea5c 3 months ago 300MB
mist/mailmock latest 95c29bda552f 3 months ago 299MB
mist/docker-socat latest f00ed0eed13f 3 months ago 7.8MB
mistce/logstash v3-3-1 0f90a36d12c8 4 months ago 730MB
mistce/api v3-3-1 4a21b676352f 4 months ago 705MB
mistce/nginx v3-3-1 4f55dd9b39e0 4 months ago 109MB
mistce/gocky v3-3-1 ee93caf66f70 4 months ago 440MB
mistce/elasticsearch-manage v3-3-1 10a48b9ea0e1 4 months ago 65.8MB
mistce/ui v3-3-1 b8fdbe0ccb23 4 months ago 626MB
ubuntu-with-vi-dockerfile latest 74ba87f80b96 4 months ago 169MB
ubuntu-with-vi latest 9d2fac08719d 4 months ago 169MB
k8s.gcr.io/coredns 1.2.6 f59dcacceff4 4 months ago 40MB
ubuntu latest ea4c82dcd15a 4 months ago 85.8MB
centos latest 75835a67d134 5 months ago 200MB
k8s.gcr.io/etcd 3.2.24 3cab8e1b9802 5 months ago 220MB
hello-world latest 4ab4c602aa5e 6 months ago 1.84kB
elasticsearch 5.6.10 73e6fdf8bd4f 7 months ago 486MB
mistce/landing v3-3-1 b0e433749aa9 7 months ago 532MB
kibana 5.6.10 bc661616b61c 8 months ago 389MB
hello-world <none> 2cb0d9787c4d 8 months ago 1.85kB
traefik v1.5 fde722950ccf 12 months ago 49.7MB
mist/swagger-ui latest 0b5230f1b6c4 12 months ago 24.8MB
k8s.gcr.io/pause 3.1 da86e6ba6ca1 14 months ago 742kB
rabbitmq 3.6.6-management c74093aa9895 2 years ago 179MB
镜像命名
在docker images
命令打印出来的内容中,我们还能看到两个与镜像命名有关的数据:REPOSITORY
和TAG
,这两者共同组成了 Docker 镜像的命名规则:
准确来说,Docker 镜像的命名可以分成三个部分:username
、repository
和tag
:
username
:主要用于识别上传镜像的不同用户,与 Github 中的用户空间类似repository
:主要用于识别镜像的内容,形成对镜像的表意描述tag
:主要用于标记镜像的版本,方便区分镜像内容的不同细节
有的镜像没有
username
这个部分,表示这个镜像是由 Docker 官方所维护和提供的,就不再单独标记用户了
另外,Docker 中还有一个约定,当我们在操作中没有具体给出镜像的tag
时,Docker 会采用latest
作为缺省tag
。
容器的生命周期
下面是一张容器运行的状态流转图:
上图展示了几种常见的对 Docker 容器的操作命令,以及执行它们之后容器运行状态的变化。重点关注容器以下几个核心状态:
Created
:容器已创建,但尚未运行Running
:容器运行中Paused
:容器暂停运行Stopped
:容器停止运行(注意与Create
的区别)Deleted
:容器被删除
主进程
在 Docker 的设计中,容器的生命周期其实与容器中PID
为1
的进程有着密切的关系。容器的启动,本质上可以理解为这个进程的启动,而容器的停止也意味着这个进程的停止,反之亦然。
当我们启动容器时,Docker 会按照镜像中的定义,启动对应的程序,并将这个程序的主进程作为容器的主进程(也就是
PID
为1
的进程)。而当我们控制容器停止时,Docker 会向主进程发送结束信号,通知程序退出
写时复制机制
Docker 的写时复制(Copy on Write)与编程中的相类似,也就是在通过镜像运行容器时,并不是马上就把镜像里的所有内容拷贝到容器所运行的沙盒文件系统中,而是利用 UnionFS 将镜像以只读的方式挂载到沙盒文件系统中。只有在容器中发生对文件的修改时,修改才会体现到沙盒环境上。
换言之,容器在创建和启动的过程中,不需要进行任何的文件系统复制操作,也不需要为容器单独开辟大量的硬盘空间,Docker 容器的启动速度也由此得到了保障
Docker 官网关于容器与镜像关系的说明
A container is launched by running an image. An image is an executable package that includes everything needed to run an application—the code, a runtime, libraries, environment variables, and configuration files.
A container is a runtime instance of an image—what the image becomes in memory when executed (that is, an image with state, or a user process). You can see a list of your running containers with the command, docker ps, just as you would in Linux.
6. 镜像仓库
如果说我们把镜像的结构用 Git 项目的结构做类比,那么镜像仓库就可以看作是 Gitlab、Github 等代码托管平台,只不过 Docker 的镜像仓库托管的不是代码项目,而是镜像
借助镜像仓库这个中转站,Docker 实现了镜像的分发功能。我们可以将开发环境上所使用的镜像推送至镜像仓库,并在测试或生产环境上拉取它们,而这个过程仅需要几个命令,甚至可以自动化的完成。
拉取镜像
可以使用docker pull
命令拉取镜像:
> docker pull ubuntu
Using default tag: latest
latest: Pulling from library/ubuntu
124c757242f8: Downloading [===============================================> ] 30.19MB/31.76MB
9d866f8bde2a: Download complete
fa3f2f277e67: Download complete
398d32b153e8: Download complete
afde35469481: Download complete
当没有显式指定镜像的标签时,Docker 将默认使用latest
。当然,也可以使用完整的镜像名来拉取镜像:
> docker pull openresty/openresty:1.13.6.2-alpine
1.13.6.2-alpine: Pulling from openresty/openresty
ff3a5c916c92: Pull complete
ede0a2a1012b: Pull complete
0e0a11843023: Pull complete
246b2c6f4992: Pull complete
Digest: sha256:23ff32a1e7d5a10824ab44b24a0daf86c2df1426defe8b162d8376079a548bf2
Status: Downloaded newer image for openresty/openresty:1.13.6.2-alpine
Docker Hub
Docker Hub 是 Docker 官方建立的中央镜像仓库,同时也是 Docker Engine 的默认镜像仓库。
搜索镜像
使用docker search
命令搜索 Docker Hub 中的镜像:
> docker search ubuntu
NAME DESCRIPTION STARS OFFICIAL AUTOMATED
ubuntu Ubuntu is a Debian-based Linux operating sys… 9312 [OK]
dorowu/ubuntu-desktop-lxde-vnc Docker image to provide HTML5 VNC interface … 281 [OK]
rastasheep/ubuntu-sshd Dockerized SSH service, built on top of offi… 208 [OK]
consol/ubuntu-xfce-vnc Ubuntu container with "headless" VNC session… 161 [OK]
ubuntu-upstart Upstart is an event-based replacement for th… 96 [OK]
ansible/ubuntu14.04-ansible Ubuntu 14.04 LTS with ansible 96 [OK]
neurodebian NeuroDebian provides neuroscience research s… 56 [OK]
1and1internet/ubuntu-16-nginx-php-phpmyadmin-mysql-5 ubuntu-16-nginx-php-phpmyadmin-mysql-5 49 [OK]
ubuntu-debootstrap debootstrap --variant=minbase --components=m… 40 [OK]
nuagebec/ubuntu Simple always updated Ubuntu docker images w… 23 [OK]
tutum/ubuntu Simple Ubuntu docker images with SSH access 19
i386/ubuntu Ubuntu is a Debian-based Linux operating sys… 17
1and1internet/ubuntu-16-apache-php-7.0 ubuntu-16-apache-php-7.0 13 [OK]
ppc64le/ubuntu Ubuntu is a Debian-based Linux operating sys… 12
eclipse/ubuntu_jdk8 Ubuntu, JDK8, Maven 3, git, curl, nmap, mc, … 8 [OK]
codenvy/ubuntu_jdk8 Ubuntu, JDK8, Maven 3, git, curl, nmap, mc, … 5 [OK]
darksheer/ubuntu Base Ubuntu Image -- Updated hourly 5 [OK]
pivotaldata/ubuntu A quick freshening-up of the base Ubuntu doc… 2
smartentry/ubuntu ubuntu with smartentry 1 [OK]
1and1internet/ubuntu-16-sshd ubuntu-16-sshd 1 [OK]
paasmule/bosh-tools-ubuntu Ubuntu based bosh-cli 1 [OK]
pivotaldata/ubuntu-gpdb-dev Ubuntu images for GPDB development 0
1and1internet/ubuntu-16-healthcheck ubuntu-16-healthcheck 0 [OK]
ossobv/ubuntu Custom ubuntu image from scratch (based on o… 0
1and1internet/ubuntu-16-rspec ubuntu-16-rspec 0 [OK]
管理镜像
要想获得镜像更详细的信息,可以使用docker inspect
命令:
> docker inspect mongo:3.2
[
{
"Id": "sha256:fb885d89ea5c35ac02acf79a398b793555cbb3216900f03f4b5f7dc31e595e31",
"RepoTags": [
"mongo:3.2"
],
"RepoDigests": [
"mongo@sha256:9e09fe9e747fb0ee1e64b572818e7397eb9a73e36a2b08bcc7846e9acf0a587f"
],
"Parent": "",
"Comment": "",
"Created": "2018-11-16T00:55:06.547559408Z",
"Container": "16a23b0d45ef66220aec0a2e542ff527da9da07889d4d862087630912d9ad86f",
"ContainerConfig": {
"Hostname": "16a23b0d45ef",
"Domainname": "",
"User": "",
"AttachStdin": false,
"AttachStdout": false,
"AttachStderr": false,
"ExposedPorts": {
"27017/tcp": {}
},
"Tty": false,
"OpenStdin": false,
"StdinOnce": false,
"Env": [
"PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin",
"GOSU_VERSION=1.10",
"JSYAML_VERSION=3.10.0",
"GPG_KEYS=DFFA3DCF326E302C4787673A01C4E7FAAAB2461C \t42F3E95A2C4F08279C4960ADD68FA50FEA312927",
"MONGO_PACKAGE=mongodb-org",
"MONGO_REPO=repo.mongodb.org",
"MONGO_MAJOR=3.2",
"MONGO_VERSION=3.2.21"
],
"Cmd": [
"/bin/sh",
"-c",
"#(nop) ",
"CMD [\"mongod\"]"
],
"ArgsEscaped": true,
"Image": "sha256:d7430950b72ba7ecb5986396f9a3404b5b0d88c2ba39eb7f2d4b51b002db00ea",
"Volumes": {
"/data/configdb": {},
"/data/db": {}
},
"WorkingDir": "",
"Entrypoint": [
"docker-entrypoint.sh"
],
"OnBuild": [],
"Labels": {}
},
"DockerVersion": "17.06.2-ce",
"Author": "",
"Config": {
"Hostname": "",
"Domainname": "",
"User": "",
"AttachStdin": false,
"AttachStdout": false,
"AttachStderr": false,
"ExposedPorts": {
"27017/tcp": {}
},
"Tty": false,
"OpenStdin": false,
"StdinOnce": false,
"Env": [
"PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin",
"GOSU_VERSION=1.10",
"JSYAML_VERSION=3.10.0",
"GPG_KEYS=DFFA3DCF326E302C4787673A01C4E7FAAAB2461C \t42F3E95A2C4F08279C4960ADD68FA50FEA312927",
"MONGO_PACKAGE=mongodb-org",
"MONGO_REPO=repo.mongodb.org",
"MONGO_MAJOR=3.2",
"MONGO_VERSION=3.2.21"
],
"Cmd": [
"mongod"
],
"ArgsEscaped": true,
"Image": "sha256:d7430950b72ba7ecb5986396f9a3404b5b0d88c2ba39eb7f2d4b51b002db00ea",
"Volumes": {
"/data/configdb": {},
"/data/db": {}
},
"WorkingDir": "",
"Entrypoint": [
"docker-entrypoint.sh"
],
"OnBuild": [],
"Labels": null
},
"Architecture": "amd64",
"Os": "linux",
"Size": 300019217,
"VirtualSize": 300019217,
"GraphDriver": {
"Data": {
"LowerDir": "/var/lib/docker/overlay2/de4bf7c9580fda62420fd6a4e529783aeb161b44457ec6636bfaa97e94084ab0/diff:/var/lib/docker/overlay2/31a2f54b5cf142ae50d5ff530fd9159cd61129a47ab76b6b32656b6db42b765b/diff:/var/lib/docker/overlay2/f495f57ba2b9e665444151dc913bd1b8952a2e3d416d546b6722e44a038900c0/diff:/var/lib/docker/overlay2/51696b913195f45c1ce36c76240a0cf9836b593a16b0853238a5515bd9178322/diff:/var/lib/docker/overlay2/bcb73a5809c820e1eeb3c7cf4acc04c89b9e4d17be7c5ce9e3962580d14f2446/diff:/var/lib/docker/overlay2/d84695101463e67d0a4c901285a557cb0f4fc84a56840ce6433a225b799e2fc4/diff:/var/lib/docker/overlay2/d86783053f0a3e71f89c7b05328b2021a75bcf833911f7dc5fdad50e166a3d39/diff:/var/lib/docker/overlay2/a7a9a982dc727d527a3af4d04a19e359062c2d74bdd8fb497a057ca09ffcf290/diff:/var/lib/docker/overlay2/cbd9ce2cce4a6e2ec032e6cf25281016715a57f11ede109097c796383d13aac2/diff:/var/lib/docker/overlay2/339f33b0ff9703a7e50cce8459b89f5fb932ccc9460c8489a0f5d2cf65114033/diff",
"MergedDir": "/var/lib/docker/overlay2/6ba7ad9bf4a5e344d1edd12b87fe42d9abd828d480385a2637a47947c9a4af7f/merged",
"UpperDir": "/var/lib/docker/overlay2/6ba7ad9bf4a5e344d1edd12b87fe42d9abd828d480385a2637a47947c9a4af7f/diff",
"WorkDir": "/var/lib/docker/overlay2/6ba7ad9bf4a5e344d1edd12b87fe42d9abd828d480385a2637a47947c9a4af7f/work"
},
"Name": "overlay2"
},
"RootFS": {
"Type": "layers",
"Layers": [
"sha256:337a2e6463ae008c12681f29c50edde52ea5be2cc2f46d09b8254fd835b1f5a9",
"sha256:9d3049f87bb2ba7ac0469cad7ee11f871ff4fc735cc4dfdc1be6a1fe877861a5",
"sha256:75c2031620755be658ab335a6abb72376804e533e91fecb52315682b21aeeeca",
"sha256:ed81bb40beffca626f698965d5ec236b394ad8db229bb6dbfeb7be7a61b32768",
"sha256:38ccb1166c8a15aedf5a9d7f12b81436b9812175c3ce6c50fac39246a3ffc935",
"sha256:1f5a9fb2648f17bd23ab13f9e70f8631d233f33f73329302144da1aa2e4a5b0f",
"sha256:fcd5eec06559827da59d45500626b2dbf5673d03bba7aea9c9b9b786e8a10b54",
"sha256:2bcf250f248858339faf2dc746c44197c9eecc34999d485c60d636c7fcbc4d20",
"sha256:f6a5611931ed6ed6db65ad6a87abd7774f267c68c6f6d84cae65e0760c8a47b0",
"sha256:b436f480c034edfc426e1fcadbebaf50c72c0ce92c66924b6cf6ba344e455560",
"sha256:7eaf69109a2207f735d6423fe61a05200e3431ba9cdeafd6a27fa3c067c9f0ae"
]
},
"Metadata": {
"LastTagTime": "0001-01-01T00:00:00Z"
}
}
]
删除镜像
可以使用docker rmi
命令删除镜像,参数是镜像名或 ID,可以同时删除多个镜像。需要注意的是,需要先通过docker rm
删除依赖该镜像的容器之后,该镜像才可以被删除:
> docker rmi redis:3.2 redis:4.0
Untagged: redis:3.2
Untagged: redis@sha256:745bdd82bad441a666ee4c23adb7a4c8fac4b564a1c7ac4454aa81e91057d977
Deleted: sha256:2fef532eadb328740479f93b4a1b7595d412b9105ca8face42d3245485c39ddc
## ......
Untagged: redis:4.0
Untagged: redis@sha256:b77926b30ca2f126431e4c2055efcf2891ebd4b4c4a86a53cf85ec3d4c98a4c9
Deleted: sha256:e1a73233e3beffea70442fc2cfae2c2bab0f657c3eebb3bdec1e84b6cc778b75
## ......
7. 运行和管理容器
容器的创建和启动
先来回顾一下容器的状态转换图:
可以看到,Docker 容器的生命周期共分为以下五种状态:
Created
:容器已经被创建,容器所需的相关资源已经准备就绪,但容器中的程序还未处于运行状态Running
:容器正在运行,其中的应用程序也正在运行Paused
:容器已经暂停,其中的所有程序都处于暂停状态Stopped
:容器处于停止状态,占用的资源和沙盒环境依然存在,只是容器中的应用程序均已停止运行Deleted
:容器已删除,相关占用的资源及存储在 Docker 中的管理信息也都被释放和移除
创建容器
> docker create --name=nginx nginx:1.12
34f277e22be252b51d204acbb32ce21181df86520de0c337a835de6932ca06c3
启动容器
> docker start nginx
Docker 还允许我们通过docker run
这个命令将docker create
和docker start
这两步操作合成为一步:
> docker run --name nginx -d nginx:1.12
通过-d
或--detach
选项告诉 Docker 在启动后将程序与控制台分离,使其在后台运行。
管理容器
使用docker ps
查看正在运行中的 Docker 容器:
> docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
89f2b769498a nginx:1.12 "nginx -g 'daemon of…" About an hour ago Up About an hour 80/tcp nginx
添加-a
或--al
选项查看所有状态下的容器:
> docker ps -a
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
425a0d3cd18b redis:3.2 "docker-entrypoint.s…" 2 minutes ago Created redis
89f2b769498a nginx:1.12 "nginx -g 'daemon of…" About an hour ago Up About an hour 80/tcp nginx
停止和删除容器
使用docker stop
命令停止正在运行中的容器:
> docker stop nginx
容器停止后,其维持的文件系统沙盒环境还是存在的,内部被修改的内容也都会保留,我们可以通过docker start
命令再次启动这个容器。
当需要完全删除容器时,可以使用docker rm
命令:
> docker rm nginx
正在运行中的容器默认情况下是不能被删除的,可以增加
-f
或--force
选项来强制停止并删除容器,不过这样做并不妥当
进入容器
我们知道,容器是一个隔离运行环境的东西,它里面除了镜像所规定的主进程之外,其他进程也是能够运行的。Docker 为我们提供了docker exec
命令来让容器运行我们所给出的命令:
> docker exec nginx more /etc/hostname
::::::::::::::
/etc/hostname
::::::::::::::
83821ea220ed
通过下列命令可以在容器中另外启动一个bash
终端,并利用-it
参数启用一个伪终端,方便我们与容器中的bash
进行交互:
> docker exec -it nginx bash
root@83821ea220ed:/>
-i
(--interactive
)表示保持我们的输入流,只有使用它才能保证控制台程序能够正确识别我们的命令-t
(--tty
)表示启用一个伪终端,形成我们与bash
的交互。如果没有它,我们就无法看到bash
内部的执行结果
连接到容器主程序
Docker 为我们提供了一个docker attach
命令,用于将当前的输入输出流连接到指定的容器上:
> docker attach nginx
可以理解为:将容器中的主程序转为了前台运行
由于我们的输入输出流连接到了容器的主程序上,我们的输入输出操作也就直接针对了这个程序,而我们发送的 Linux 信号也会转移到这个程序上。例如我们可以通过Ctrl^C
来向程序发送停止信号,这样一来容器也会停止运行。
8. 为容器配置网络
容器网络
容器网络实质上也是由 Docker 为应用程序所创造的虚拟环境的一部分,它能让应用从宿主机操作系统的网络环境中独立出来,形成容器自有的网络设备、IP 协议栈、端口套接字、IP 路由表、防火墙等等与网络相关的模块。
在 Docker 网络中,有三个比较核心的概念,沙盒(Sandbox)、网络(Network)、端点(Endpoint):
- 沙盒:提供了容器的虚拟网络栈,隔离了容器网络与宿主机网络,形成了完全独立的容器网络环境
- 网络:可以理解为 Docker 内部的虚拟子网,网络内的参与者相互可见并能够进行通讯。Docker 的这种虚拟网络也是与宿主机网络存在隔离关系的,主要是为了形成容器间的安全通讯环境
- 端点:是位于容器或网络隔离墙之上的洞,其主要目的是形成一个可以控制的、突破封闭网络环境的出入口。当容器的端点与网络的端点形成配对后,就如同在这两者之间架起了桥梁,便能够进行数据传输了
这三者一起构成了 Docker 网络的核心模型,即容器网络模型(Container Network Model)
浅析 Docker 的网络实现
容器网络模型为容器引擎提供了一套标准的网络对接范式,而在 Docker 中,实现这套范式的是 Docker 所封装的libnetwork
模块。
目前 Docker 官方提供了五种网络驱动:Bridge
、Host
、Overlay
、MacLan
、None
。
其中,Bridge
网络是 Docker 容器的默认网络驱动,而Overlay
网络则是借助 Docker Swarm 来搭建的跨 Docker Daemon 网络,我们可以通过它搭建跨物理主机的虚拟网络,进而让不同物理机中运行的容器感知不到多个物理机的存在。
容器互联
要让一个容器连接到另外一个容器,我们可以在容器通过docker create
或docker run
创建时通过--link
选项进行配置:
> docker run -d --name mysql -e MYSQL_RANDOM_ROOT_PASSWORD=yes mysql
> docker run -d --name webapp --link mysql webapp:latest
要想在 Web 应用中连接到 MySQL 数据库,只需要将容器的网络命名填入到连接地址中。例如下面的代码,连接地址中的mysql
就类似我们常见的域名解析,Docker 会将其指向 MySQL 容器的 IP 地址:
String url = "jdbc:mysql://mysql:3306/webapp"
暴露端口
虽然容器间的网络打通了,但并不意味着我们可以任意访问被连接容器中的任何服务。Docker 为容器网络增加了一套安全机制,只有容器自身允许的端口,才能被其他容器所访问。
在docker ps
的结果中可以看到容器暴露给其他容器访问的端口:
> docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
95507bc88082 mysql:5.7 "docker-entrypoint.s…" 17 seconds ago Up 16 seconds 3306/tcp, 33060/tcp mysql
暴露端口可以通过 Docker 镜像定义,也可以在容器创建时通过--expose
选项进行定义:
> docker run -d --name mysql -e MYSQL_RANDOM_ROOT_PASSWORD=yes --expose 13306 --expose 23306 mysql:5.7
可以看到13306
和23306
这两个端口已经成功的打开:
> docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
3c4e645f21d7 mysql:5.7 "docker-entrypoint.s…" 4 seconds ago Up 3 seconds 3306/tcp, 13306/tcp, 23306/tcp, 33060/tcp mysql
通过别名连接
Docker 还支持连接时使用别名来摆脱容器名的限制:
> docker run -d -name webapp --link mysql:database webapp:latest
这里使用了--link <name>:<alias>
的形式连接到 MySQL 容器,并设置它的别名为database
。这样当我们要在 Web 应用中使用 MySQL 连接时,就可以用database
替代连接地址:
String url = "jdbc:mysql://database:3306/webapp";
管理网络
容器能够互相连接的前提是两者同处于一个网络中,这里的网络可以理解为 Docker 所虚拟的子网,而容器网络沙盒可以看作是虚拟的主机。只有当多个主机在同一个子网时,才能互相看到并进行网络数据交换。
当我们启动 Docker 服务时,他会为我们创建一个默认的bridge
网络。而我们创建的容器在不专门指定网络的情况下,都会连接到这个网络上。
通过docker inspect
命令查看容器,可在Network
部分看到容器网络的相关信息:
> docker inspect mysql
[
{
## ......
"NetworkSettings": {
## ......
"Networks": {
"bridge": {
"IPAMConfig": null,
"Links": null,
"Aliases": null,
"NetworkID": "bc14eb1da66b67c7d155d6c78cb5389d4ffa6c719c8be3280628b7b54617441b",
"EndpointID": "1e201db6858341d326be4510971b2f81f0f85ebd09b9b168e1df61bab18a6f22",
"Gateway": "172.17.0.1",
"IPAddress": "172.17.0.2",
"IPPrefixLen": 16,
"IPv6Gateway": "",
"GlobalIPv6Address": "",
"GlobalIPv6PrefixLen": 0,
"MacAddress": "02:42:ac:11:00:02",
"DriverOpts": null
}
}
## ......
}
## ......
}
]
创建网络
使用docker network create
命令创建网络,通过-d
选项指定驱动类型,默认为bridge
:
> docker network create -d bridge individual
通过docker network ls
或者docker network list
查看 Docker 中已经存在的网络:
> docker network ls
NETWORK ID NAME DRIVER SCOPE
bc14eb1da66b bridge bridge local
35c3ef1cc27d individual bridge local
在之后创建容器时,可以通过--network
来指定容器所要加入的网络:
> docker run -d --name mysql -e MYSQL_RANDOM_ROOT_PASSWORD=yes --network individual mysql:5.7
通过docker inspect mysql
观察一下此时的容器网络:
> docker inspect mysql
[
{
## ......
"NetworkSettings": {
## ......
"Networks": {
"individual": {
"IPAMConfig": null,
"Links": null,
"Aliases": [
"2ad678e6d110"
],
"NetworkID": "35c3ef1cc27d24e15a2b22bdd606dc28e58f0593ead6a57da34a8ed989b1b15d",
"EndpointID": "41a2345b913a45c3c5aae258776fcd1be03b812403e249f96b161e50d66595ab",
"Gateway": "172.18.0.1",
"IPAddress": "172.18.0.2",
"IPPrefixLen": 16,
"IPv6Gateway": "",
"GlobalIPv6Address": "",
"GlobalIPv6PrefixLen": 0,
"MacAddress": "02:42:ac:12:00:02",
"DriverOpts": null
}
}
## ......
}
## ......
}
]
可以看到容器所加入的网络已经变成为individual
。
当两个容器处于不同的网络时,之间是不能互相连接引用的
端口映射
Docker 提供了端口映射的功能来允许我们从容器外部通过网络访问容器中的应用:
要映射端口,我们可以在创建容器时使用-p
或--publish
选项**,格式为-p <ip>:<host-port>:<container-port>
:
> docker run -d --name nginx -p 80:80 -p 443:443 nginx:1.12
之后就可以在容器列表里看到端口映射的配置:
> docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
bc79fc5d42a6 nginx:1.12 "nginx -g 'daemon of…" 4 seconds ago Up 2 seconds 0.0.0.0:80->80/tcp, 0.0.0.0:443->443/tcp nginx
9. 管理和存储数据
Docker 中的沙盒文件系统虽然说有很多优势,但也存在弊端:
- 沙盒文件系统是随容器生命周期所创建和移除的,数据无法直接被持久化存储
- 由于容器隔离,我们很难从容器外部直接获得或操作容器内部文件中的数据
为了解决这些问题,UnionFS 支持挂载不同类型的文件系统到统一的目录结构中。
挂载方式
基于底层存储实现,Docker 提供了三种适用于不同场景的文件系统挂载方式:Bind Mount、Volume 和 Tmpfs Mount。
Bind Mount
:能够直接将宿主机操作系统中的目录和文件挂载到容器内的文件系统中,需要同时指定容器内、外的路径。在容器内外对文件读写,都是相互可见的Volume
:也是从宿主机操作系统中挂载目录到容器,只不过这个挂载的目录由 Docker 进行管理,我们只需要指定容器内的目录即可Tmpfs Mount
:支持挂载系统内存的中的一部分到容器的文件系统里,不过存储并不是持久的,其中的内容会随着容器的停止而消失
挂载文件到容器
在创建容器时通过传递-v
或--volume
选项来指定挂载对应的目录或文件,格式为-v <host-path>:<container-path>
:
> docker run -d --name nginx -v /webapp/html:/usr/share/nginx/html nginx:1.12
容器启动后,就可以看到挂载的目录或文件已经出现在容器中:
> docker exec nginx ls /usr/share/nginx/html
index.html
可以通过docker inspect
查看容器数据挂载的相关信息:
> docker inspect nginx
[
{
## ......
"Mounts": [
{
"Type": "bind",
"Source": "/webapp/html",
"Destination": "/usr/share/nginx/html",
"Mode": "",
"RW": true,
"Propagation": "rprivate"
}
],
## ......
}
]
可以看到有一个RW
字段,表示挂载目录或文件具有读写性(Read and Write)。
Docker 还支持以只读的方式挂载,这样目录或文件只能被容器中的程序读取,而无法修改。只需要在挂载选项后添加:ro
:
> docker run -d --name nginx -v /webapp/html:/usr/share/nginx/html:ro nginx:1.12
挂载临时文件目录
Tmpfs Mount
是一种特殊的挂载方式,它主要利用内存来存储数据,因此其特征就是高读写速度、临时性挂载。
在创建容器时,通过--tmpfs
传递挂载到容器内的目录即可,不需要指定内存的具体位置:
> docker run -d --name webapp --tmpfs /webapp/cache webapp:latest
也可以通过docker inspect
命令进行查看:
> docker inspect webapp
[
{
## ......
"Tmpfs": {
"/webapp/cache": ""
},
## ......
}
]
Tmpfs Mount
有以下几种常见的使用场景:
- 应用不需要进行持久化保存的敏感数据,可以借助内存的非持久性和程序隔离性来保障安全
- 读写速度要求较高、数据变化量大,但不需要持久化保存的数据,可以借助内存的高读写速度减少操作的时间
使用数据卷
数据卷(Volume)本质上仍然是宿主机操作系统上的一个目录,只不过它存放在 Docker 内部,接受 Docker 的管理。
在使用Volume
进行挂载时,我们不需要知道数据具体存储在了宿主机操作系统的何处,只需要给定容器中的哪个目录会被挂载即可:
> docker run -d --name webapp -v /webapp/storage webapp:latest
数据卷挂载到容器后,可以通过docker inspect
命令查看容器中数据卷的挂载信息:
> docker inspect webapp
[
{
## ......
"Mounts": [
{
"Type": "volume",
"Name": "2bbd2719b81fbe030e6f446243386d763ef25879ec82bb60c9be7ef7f3a25336",
"Source": "/var/lib/docker/volumes/2bbd2719b81fbe030e6f446243386d763ef25879ec82bb60c9be7ef7f3a25336/_data",
"Destination": "/webapp/storage",
"Driver": "local",
"Mode": "",
"RW": true,
"Propagation": ""
}
],
## ......
}
]
为了方便识别数据卷,可以通过-v <name>:<container-path>
的形式来命名数据卷:
> docker run -d --name webapp -v appdata:/webapp/storage webapp:latest
共用数据卷
由于数据卷的命名在 Docker 中是唯一的,因此可以很方便的让多个容器挂载同一个数据卷:
> docker run -d --name webapp -v html:/webapp/html webapp:latest
> docker run -d --name nginx -v html:/usr/share/nginx/html:ro nginx:1.12
使用
-v
选项来挂载数据卷时,如果数据卷不存在,Docker 就会自动创建和分配宿主机操作系统的目录。如果同名数据卷已经存在,则会直接引用
删除数据卷
可以直接通过docker volume rm
命令来删除指定的数据卷:
> docker volume rm appdata
在删除数据卷之前,我们必须保证数据卷没有被任何容器所使用,否则 Docker 会报错
在docker rm
删除容器的命令中,还可以添加-v
选项来删除容器关联的数据卷:
docker rm -v webapp
如果没有随容器删除这些数据卷,Docker 在创建新的容器时也不会启用它们。这时可以通过docker volume prune
命令删除那些没有被容器引用的数据卷:
> docker volume prune
Deleted Volumes:
af6459286b5ce42bb5f205d0d323ac11ce8b8d9df4c65909ddc2feea7c3d1d53
0783665df434533f6b53afe3d9decfa791929570913c7aff10f302c17ed1a389
65b822e27d0be93d149304afb1515f8111344da9ea18adc3b3a34bddd2b243c7
## ......
数据卷容器
所谓数据卷容器(Volume Container),就是一个没有具体指定应用,甚至不需要运行的容器。我们使用它的目的,是为了定义一个或多个数据卷并持有它们的引用:
可通过以下命令创建一个数据卷容器:
> docker create --name appdata -v /webapp/storage ubuntu
数据卷容器可以看作是容器间的文件系统桥梁,可以像加入网络一样引用数据卷容器,添加--volumes-from
参数即可:
> docker run -d -name webapp --volumes-from appdata webapp:latest
备份和迁移数据卷
利用数据卷容器,可以很方便的对数据卷中的数据进行迁移。
数据备份、迁移、恢复的过程可以理解为对数据进行打包,移动到其他位置,在需要的地方解压的过程
首先建立一个用来存放打包文件的目录/backup
。要备份数据,我们还需要建立一个临时容器,将用于备份的目录和要备份的数据卷都挂载上去:
> docker run --rm --volumes-from appdata -v /backup:/backup ubuntu tar cvf /backup/backup.tar /webapp/storage
--rm
选项用来让容器在停止后自动删除
备份后,就可以在/backup
目录下找到数据卷的备份文件backup.tar
了。
要恢复数据卷中的数据,也可以借助临时容器来完成:
> docker run --rm --volumes-from appdata -v /backup:/backup ubuntu tar xvf /backup/backup.tar -C /webapp/storage --strip
通过 mount 选项挂载
Docker 还为我们提供了一个支持相对丰富的挂载方式,也就是通过--mount
选项来配置挂载:
> docker run -d --name webapp webapp:latest --mount 'type=volume,src=appdata,dst=/webapp/storage,volume-driver=local,volume-opt=type=nfs,volume-opt=device=<nfs-server>:<nfs-path>' webapp:latest
10. 保存和共享镜像
提交容器更改
Docker 将容器内沙盒文件系统记录成镜像层的时候,会先暂停容器的运行,保证容器内的文件系统处于一个相对稳定的状态,以确保数据的一致性。
> docker commit -m "Configured" webapp
sha256:0bc42f7ff218029c6c4199ab5c75ab83aeaaed3b5c731f715a3e807dda61d19e
> docker images
REPOSITORY TAG IMAGE ID CREATED SIZE
<none> <none> 0bc42f7ff218 3 seconds ago 372MB
## ......
为镜像命名
使用docker tag
能够为未命名的镜像指定镜像名:
> docker tag 0bc42f7ff218 webapp:1.0
也可以为已有的镜像创建一个新的命名:
> docker tag webapp:1.0 webapp:2.0
当我们对未命名的镜像进行命名后,Docker 就不会在镜像列表里继续显示这个镜像,取而代之的是我们新的命名。而如果我们对已有镜像使用docker tag
时,旧的镜像依然会存在于镜像列表中:
> docker images
REPOSITORY TAG IMAGE ID CREATED SIZE
webapp 1.0 0bc42f7ff218 29 minutes ago 372MB
webapp latest 0bc42f7ff218 29 minutes ago 372MB
## ......
还可以直接在提交镜像更改时指定新的镜像名:
> docker commit -m "Upgrade" webapp webapp:2.0
镜像的迁移
可以使用管道:
> docker save webapp:1.0 > webapp-1.0.tar
或者可以使用docker save
命令,并添加-o
选项,用来指定输出文件:
> docker save -o ./webapp-1.0.tar webapp:1.0
导入镜像
可以使用管道:
> docker load < webapp-1.0.tar
或者添加-i
选项指定输入文件:
> docker load -i webapp-1.0.tar
批量迁移
通过docker save
和docker load
命令还可以批量迁移镜像,只要在docker save
中传入多个镜像名作为参数,就可以将这些镜像都打成一个包,方便我们一次性迁移多个镜像:
> docker save -o ./images.tar webapp:1.0 nginx:1.12 mysql:5.7
导入和导出容器
使用docker export
命令可以直接导出容器,可以简单的将其理解为docker commit
和docker save
命令的结合体:
> docker export -o ./webapp.tar webapp
相对的,使用docker export
导出的容器包,我们可以使用docker import
导入。使用docker import
并非直接将容器导入,而是将容器运行时的内容以镜像的形式导入,所以导入的结果还是一个镜像,而不是容器:
> docker import ./webapp.tar webapp:1.0
11. 通过 Dockerfile 创建镜像
关于 Dockerfile
Dockerfile 是 Docker 中用于定义镜像自动化构建流程的配置文件,在 Dockerfile 中,包含了构建镜像过程中需要执行的命令和其他操作。
Dockerfile 的内容很简单,主要以两种形式呈现:一种是注释行,另一种是指令行。
编写 Dockerfile
首先来看一个完整的 Dockerfile 例子,这是用于构建 Docker 官方所提供的Redis
镜像的 Dockerfile 文件:
FROM debian:stretch-slim
# add our user and group first to make sure their IDs get assigned consistently, regardless of whatever dependencies get added
RUN groupadd -r redis && useradd -r -g redis redis
# grab gosu for easy step-down from root
# https://github.com/tianon/gosu/releases
ENV GOSU_VERSION 1.10
RUN set -ex; \
\
fetchDeps=" \
ca-certificates \
dirmngr \
gnupg \
wget \
"; \
apt-get update; \
apt-get install -y --no-install-recommends $fetchDeps; \
rm -rf /var/lib/apt/lists/*; \
\
dpkgArch="$(dpkg --print-architecture | awk -F- '{ print $NF }')"; \
wget -O /usr/local/bin/gosu "https://github.com/tianon/gosu/releases/download/$GOSU_VERSION/gosu-$dpkgArch"; \
wget -O /usr/local/bin/gosu.asc "https://github.com/tianon/gosu/releases/download/$GOSU_VERSION/gosu-$dpkgArch.asc"; \
export GNUPGHOME="$(mktemp -d)"; \
gpg --keyserver ha.pool.sks-keyservers.net --recv-keys B42F6819007F00F88E364FD4036A9C25BF357DD4; \
gpg --batch --verify /usr/local/bin/gosu.asc /usr/local/bin/gosu; \
gpgconf --kill all; \
rm -r "$GNUPGHOME" /usr/local/bin/gosu.asc; \
chmod +x /usr/local/bin/gosu; \
gosu nobody true; \
\
apt-get purge -y --auto-remove $fetchDeps
ENV REDIS_VERSION 3.2.12
ENV REDIS_DOWNLOAD_URL http://download.redis.io/releases/redis-3.2.12.tar.gz
ENV REDIS_DOWNLOAD_SHA 98c4254ae1be4e452aa7884245471501c9aa657993e0318d88f048093e7f88fd
# for redis-sentinel see: http://redis.io/topics/sentinel
RUN set -ex; \
\
buildDeps=' \
wget \
\
gcc \
libc6-dev \
make \
'; \
apt-get update; \
apt-get install -y $buildDeps --no-install-recommends; \
rm -rf /var/lib/apt/lists/*; \
\
wget -O redis.tar.gz "$REDIS_DOWNLOAD_URL"; \
echo "$REDIS_DOWNLOAD_SHA *redis.tar.gz" | sha256sum -c -; \
mkdir -p /usr/src/redis; \
tar -xzf redis.tar.gz -C /usr/src/redis --strip-components=1; \
rm redis.tar.gz; \
\
# disable Redis protected mode [1] as it is unnecessary in context of Docker
# (ports are not automatically exposed when running inside Docker, but rather explicitly by specifying -p / -P)
# [1]: https://github.com/antirez/redis/commit/edd4d555df57dc84265fdfb4ef59a4678832f6da
grep -q '^#define CONFIG_DEFAULT_PROTECTED_MODE 1$' /usr/src/redis/src/server.h; \
sed -ri 's!^(#define CONFIG_DEFAULT_PROTECTED_MODE) 1$!\1 0!' /usr/src/redis/src/server.h; \
grep -q '^#define CONFIG_DEFAULT_PROTECTED_MODE 0$' /usr/src/redis/src/server.h; \
# for future reference, we modify this directly in the source instead of just supplying a default configuration flag because apparently "if you specify any argument to redis-server, [it assumes] you are going to specify everything"
# see also https://github.com/docker-library/redis/issues/4#issuecomment-50780840
# (more exactly, this makes sure the default behavior of "save on SIGTERM" stays functional by default)
\
make -C /usr/src/redis -j "$(nproc)"; \
make -C /usr/src/redis install; \
\
rm -r /usr/src/redis; \
\
apt-get purge -y --auto-remove $buildDeps
RUN mkdir /data && chown redis:redis /data
VOLUME /data
WORKDIR /data
COPY docker-entrypoint.sh /usr/local/bin/
ENTRYPOINT ["docker-entrypoint.sh"]
EXPOSE 6379
CMD ["redis-server"]
Dockerfile 的结构
总体上来看,可以将 Dockerfile 理解为一个由上往下执行指令的脚本文件。可以将 Dockerfile 的指令简单的分为五大类:
- 基础指令:用于定义新镜像的基础和性质
- 控制指令:是指导镜像构建的核心部分
- 引入指令:用于将外部文件直接引入到构建镜像内部
- 执行指令:能够为基于镜像所创建的容器,指定在启动时需要执行的脚本或命令
- 配置指令:对镜像以及基于镜像所创建的容器,可以通过配置指令对其网络、用户等内容进行配置
Dockerfile 常见指令
1. FROM
在镜像构建的过程中,可以通过FROM
指令指定一个基础镜像。Docker 会先获取到这个基础镜像,再在这个镜像的基础上进行构建操作
FROM <image> [AS <name>]
FROM <image>[:<tag>] [AS <name>]
FROM <image>[@<digest>] [AS <name>]
2. RUN
在RUN
指令之后,我们直接拼接上需要执行的命令。在构建时,Docker 就会执行这些命令,并将它们对文件系统的修改记录下来,形成镜像的变化:
RUN <command>
RUN ["executable", "param1", "param2"]
RUN
指令支持以\
换行,如果单行的长度过大,建议对内容进行切割,方便阅读
3. ENTRYPOINT 和 CMD
基于镜像启动的容器,在容器启动时会根据镜像所定义的一条命令来启动容器中进程号为1
的进程。而这个命令的定义,就是通过 Dockerfile 中的ENTRYPOINT
和CMD
实现的。
ENTRYPOINT ["executable", "param1", "param2"]
ENTRYPOINT command param1 param2
CMD ["executable","param1","param2"]
CMD ["param1","param2"]
CMD command param1 param2
ENTRYPOINT
和CMD
指令用法近似,都是给出需要执行的指令,并且它们都可以为空- 当
ENTRYPOINT
和CMD
同时给出时,CMD
中的内容会作为ENTRYPOINT
定义命令的参数,最终执行容器启动的还是ENTRYPOINT
所给出的命令
4. EXPOSE
通过EXPOSE
指令可以为镜像指定要暴露的端口:
EXPOSE <port> [<port>/<protocol>...]
当我们通过EXPOSE
指令配置了镜像的端口暴露定义,那么基于这个镜像所创建的容器,在被其他容器通过--link
选项连接时,就能够直接允许来自其他容器对这些端口的访问。
5. VOLUME
在一些程序里,我们需要持久化一些数据。可以通过VOLUME
指令来定义基于此镜像的容器所自动建立的数据卷,这样就无需单独使用-v
选项进行配置:
VOLUME ["/data"]
6. COPY 和 ADD
在制作新镜像时,我们可能需要将一些软件配置、程序代码、执行脚本等直接导入到镜像内的文件系统里。使用COPY
或ADD
指令能够帮助我们直接从宿主机的文件系统里拷贝内容到镜像里的文件系统中:
COPY [--chown=<user>:<group>] <src>... <dest>
ADD [--chown=<user>:<group>] <src>... <dest>
COPY [--chown=<user>:<group>] ["<src>",... "<dest>"]
ADD [--chown=<user>:<group>] ["<src>",... "<dest>"]
COPY
与ADD
指令的定义完全一样,主要区别在于ADD
能够支持使用网络端的URL
地址作为src
源,并且在源文件被识别为压缩包时,自动进行解压,而COPY
则没有这两个能力。
构建镜像
在编写好Dockerfile
之后,我们就可以使用docker build
命令构建我们所定义的镜像:
> docker build ./webapp
docker build
可以接收一个参数,这个参数为一个目录路径(本地路径或 URL 路径)。Docker 会将这个目录作为构建的环境目录,默认情况下,也会从这个目录下寻找名为Dockerfile
的文件。
如果我们的Dockerfile
文件路径不在这个目录下,则可以通过-f
选项单独给出Dockerfile
文件的路径:
> docker build -t webapp:latest -f ./webapp/a.Dockerfile ./webapp
最好在构建镜像时添加-t
选项,用来指定新生成的镜像的名称:
> docker build -t webapp:latest ./webapp
12. Dockerfile 使用技巧
构建中使用变量
在 Dockerfile 里,可以使用ARG
指令建立一个参数变量。我们可以在构建时通过构建指令传入这个参数变量,并且在 Dockerfile 里使用它:
FROM debian:stretch-slim
## ......
ARG TOMCAT_MAJOR
ARG TOMCAT_VERSION
## ......
RUN wget -O tomcat.tar.gz "https://www.apache.org/dyn/closer.cgi?action=download&filename=tomcat/tomcat-$TOMCAT_MAJOR/v$TOMCAT_VERSION/bin/apache-tomcat-$TOMCAT_VERSION.tar.gz"
## ......
如果我们需要通过这个 Dockerfile 文件构建 Tomcat 镜像,可以在构建时通过docker build
的--build-arg
选项来设置参数变量:
> docker build --build-arg TOMCAT_MAJOR=8 --build-arg TOMCAT_VERSION=8.0.53 -t tomcat:8.0 ./tomcat
环境变量
环境变量通过ENV
指令定义:
FROM debian:stretch-slim
## ......
ENV TOMCAT_MAJOR 8
ENV TOMCAT_VERSION 8.0.53
## ......
RUN wget -O tomcat.tar.gz "https://www.apache.org/dyn/closer.cgi?action=download&filename=tomcat/tomcat-$TOMCAT_MAJOR/v$TOMCAT_VERSION/bin/apache-tomcat-$TOMCAT_VERSION.tar.gz"
与参数变量只能影响构建过程不同,环境变量不仅能够影响构建,还能够影响基于此镜像创建的容器。
环境变量设置的实质,其实就是定义操作系统环境变量,所以在运行的容器里,一样拥有这些变量,而容器中运行的程序也能够得到这些变量的值
由于环境变量在容器运行时依然有效,所以运行容器时我们还可以对其进行覆盖。
在创建容器时使用-e
或--env
选项,可以对环境变量的值进行修改或定义新的环境变量:
> docker run -e MYSQL_ROOT_PASSWORD=my-secret-pw -d mysql:5.7
ENV
指令所定义的变量,永远会覆盖ARG
所定义的变量,即使它们定义时的顺序是相反的
合并命令
在构建镜像时,RUN
指令有两种写法:
RUN apt-get update; \
apt-get install -y --no-install-recommends $fetchDeps; \
rm -rf /var/lib/apt/lists/*;
# 或
RUN apt-get update
RUN apt-get install -y --no-install-recommends $fetchDeps
RUN rm -rf /var/lib/apt/lists/*
而我们更常见的是第一种形式,这就要从镜像构建的过程说起了。
看似连续的镜像构建过程,其实是由多个小段组成的。每当一条能够形成对文件系统改动的指令在被执行前,Docker 先会基于上条命令的结果启动一个容器,在容器中运行这条指令的内容,之后将结果打包成一个镜像层,如此反复,最终形成镜像
所以,构建而来的镜像是由多个镜像层叠加而得的,而这些镜像层其实就是在我们 Dockerfile 中每条指令所生成的。
因此,绝大多数镜像会将命令合并到一条指令中,因为这样不但减少了镜像层的数量,也减少了镜像构建过程中反复创建容器的次数,提高了镜像构建的速度。
构建缓存
Docker 在镜像构建的过程中,还支持一种缓存策略来提高镜像的构建速度。
由于镜像是多个指令所创建的镜像层组合而得,那么如果我们判断新编译的镜像层与已经存在的镜像层未发生变化,那么我们完全可以直接利用之前构建的结果,而不需要再执行这条构建指令,这就是镜像构建缓存的原理
基于这个原则,我们在条件允许的前提下,更建议将不容易发生变化的搭建过程放到 Dockerfile 的前部,充分利用构建缓存提高镜像构建的速度。
另外,指令的合并也不宜过度,而是将易变和不易变的过程拆分,分别放到不同的指令里。
当不希望 Docker 在构建镜像中使用构建缓存时,可以通过--no-cache
选项禁用:
> docker build --no-cache ./webapp
搭配 ENTRYPOINT 和 CMD
ENTRYPOINT
和CMD
两个命令都是用来指定基于此镜像所创建容器里主进程的启动命令,而它们的区别在于,ENTRYPOINT
指令的优先级高于CMD
指令。当ENTRYPOINT
和CMD
同时在镜像中被指定时,CMD
里的内容会作为ENTRYPOINT
的参数,两者拼接之后,才是最终执行的命令。
之所以ENTRYPOINT
和CMD
要分成两个不同的命令,是因为它们的设计目的是不同的:
ENTRYPOINT
:主要用于对容器进行一些初始化CMD
:用于真正定义容器中主程序的启动命令
以Redis
镜像为例:
## ......
COPY docker-entrypoint.sh /usr/local/bin/
ENTRYPOINT ["docker-entrypoint.sh"]
## ......
CMD ["redis-server"]
可以看到,CMD
指令定义的正是启动Redis
的服务程序,而ENTRYPOINT
使用的则是外部引入的脚本文件docker-entrypoint.sh
,内容如下:
#!/bin/sh
set -e
# first arg is `-f` or `--some-option`
# or first arg is `something.conf`
if [ "${1#-}" != "$1" ] || [ "${1%.conf}" != "$1" ]; then
set -- redis-server "$@"
fi
# 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 "$@"
脚本的最后一条命令exec "$@"
其作用是运行一个程序,而运行命令就是ENTRYPOINT
脚本的参数,所以实际执行的就是CMD
里的命令。
13. 使用 Docker Compose 管理容器
Docker Compose
在 Docker 开发中最常使用的多容器定义和运行软件就是 Docker Compose。
如果说 Dockerfile 是将容器内运行环境的搭建固化下来,那么 Docker Compose 就可以理解为将多个容器运行的方式和配置固化下来。
在 Docker Compose 里,我们通过一个docker-compose.yml
配置文件,将所有与应用系统相关的软件及它们对应的容器进行配置,之后使用 Docker Compose 提供的命令进行启动,就能让 Docker Compose 将刚才我们所提到的那些复杂问题解决掉。
安装 Docker Compose
Docker Compose 是一个由 Python 编写的软件。通过下面的命令下载 Docker Compose 到应用执行目录,并附上运行权限,这样 Docker Compose 就可以在机器中使用了:
> sudo curl -L "https://github.com/docker/compose/releases/download/1.22.0/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose
> sudo chmod +x /usr/local/bin/docker-compose
> sudo docker-compose version
docker-compose version 1.21.2, build a133471
docker-py version: 3.3.0
CPython version: 3.6.5
OpenSSL version: OpenSSL 1.0.1t 3 May 2016
也可以通过pip
安装:
> sudo pip install docker-compose
Docker Compose 的基本使用逻辑
简单来说,使用 Docker Compose 的步骤共分为三步:
- 如果需要,编写容器所需镜像的
Dockerfile
(也可以使用现有镜像) - 编写用于配置容器的
docker-compose.yml
- 使用
docker-compose
命令启动应用栈
1. 编写 docker-compose.yml
一个简单的例子:
version: '3'
services:
webapp:
build: ./image/webapp
ports:
- "5000:5000"
volumes:
- ./code:/code
- logvolume:/var/log
links:
- mysql
- redis
redis:
image: redis:3.2
mysql:
image: mysql:5.7
environment:
- MYSQL_ROOT_PASSWORD=my-secret-pw
volumes:
logvolume: {}
2. 启动和停止
对与开发而言,最常使用的就是docker-compose up
和docker-compose down
命令:
docker-compose up
docker-compose up
命令类似于 Docker Engine 中的docker run
。它会根据docker-compose.yml
中配置的内容,创建所有的容器、网络、数据卷等内容,并将它们启动。
默认情况下,docker-compose up
会在前台运行,可以使用-d
选项使其在后台运行:
> sudo docker-compose up -d
需要注意的是,docker-compose
命令默认会识别当前控制台所在目录内的docker-compose.yml
文件,而且会以当前目录的名字作为组装的应用项目的名称。可以通过-f
选项来指定配置文件名,通过-p
选项来定义项目名:
> sudo docker-compose -f ./compose/docker-compose.yml -p myapp up -d
docker-compose down
docker-compose down
命令用于停止所有的容器,并将它们删除,同时清除网络等配置内容:
> sudo docker-compose down
3. 容器命令
除了启动和停止命令之外,Docker Compose 还为我们提供了很多直接操作服务的命令,服务可以看成是一组相同容器的集合。
可以使用docker-compose logs
命令查看容器中主进程的输出内容:
> sudo docker-compose logs nginx
通过docker-compose create/start/stop
可以实现与docker create/start/stop
相似的效果,只不过操作的对象由 Docker Engine 中的容器变为了 Docker Compose 中的服务:
> sudo docker-compose create webapp
> sudo docker-compose start webapp
> sudo docker-compose stop webapp
更新中…