Preface
Docker是什么?下面是官方的一段说明:
Docker is the world’s leading software containerization platform.
恩,很niubility
,引领世界软件容器化的平台…
本篇主要记录Docker的基础学习(安装、简单使用)
Containerization VS Virtualization
了解Docker之前,我们有必要了解一下容器化
容器相当于轻量级的虚拟机,但隔离性不如虚拟机。
Story
Long long ago…
Dev: “帮我构建几台跟生产环境一样的测试服务器”
Ops: “给我一个星期时间”
Dev: “明天用…”
Ops: “开发的这群傻叉新给的发布包又把系统CPU搞到100%了,应用又夯住了,都是些什么水平的人啊…”
Dev: “运维的这帮傻鸟技术太差,维护的是些什么稀烂的系统,在我这跑得好好的,上他们那应用就挂…”
Ops: “这是开发的锅…”
Dev: “这是运维的盘…”
Q:
- 线上线下环境不一致,线上JDK1.8.01,线下JDK1.8.02,数据库版本不统一等环境问题
- 单机安装和配置MySQL、Memcatched、MongoDB、Hadoop、GlusterFS、RabbitMQ、Node.js、Nginx已经够复杂,集群更不用说
最终引发的问题就是,我们的服务方是用户,受害方也是用户…
各司其职的同时也在两者之间形成了一面无形的墙,阻碍了开发和运维之间的沟通和协作,而Docker、DevOps的出现就是为了击碎这堵无形之墙。
Docker
核心理念:Build,Ship,and Run Any App,Anywhere
(Java的核心理念:Write once, run anywhere)
Docker是GO
语言编写的容器化的一种实现,是一个分布式应用构建、迁移和运行的开放平台,它允许开发或运维人员将应用和运行应用所依赖的文件打包到一个标准化的单元(容器)中运行。其他的容器实现有OpenVZ,Pouch(Ali
出品)等。
服务器好比运输码头:拥有场地和各种设备(服务器硬件资源)
服务器容器化好比作码头上的仓库:拥有独立的空间堆放各种货物或集装箱
(仓库之间完全独立,独立的应用系统和操作系统)
实现的核心技术: lcx、cgroup、namespaces…(Linux内核级别隔离技术)
注意点: 不能乱玩…遵循单一职责,无状态。
Docker实现DevOps的优势
优势一:
开发、测试和生产环境的统一化和标准化。镜像作为标准的交付件,可在开发、测试和生产环境上以容器来运行,最终实现三套环境上的应用以及运行所依赖内容的完全一致。
优势二:
解决底层基础环境的异构问题。基础环境的多元化造成了从Dev到Ops过程中的阻力,而使用Docker Engine可无视基础环境的类型。不同的物理设备,不同的虚拟化类型,不同云计算平台,只要是运行了Docker Engine的环境,最终的应用都会以容器为基础来提供服务。
优势三:
易于构建、迁移和部署。Dockerfile实现镜像构建的标准化和可复用,镜像本身的分层机制也提高了镜像构建的效率。使用Registry可以将构建好的镜像迁移到任意环境,而且环境的部署仅需要将静态只读的镜像转换为动态可运行的容器即可。
优势四:
轻量和高效。和需要封装操作系统的虚拟机相比,容器仅需要封装应用和应用需要的依赖文件,实现轻量的应用运行环境,且拥有比虚拟机更高的硬件资源利用率。
优势五:
工具链的标准化和快速部署。将实现DevOps所需的多种工具或软件进行Docker化后,可在任意环境实现一条或多条工具链的快速部署。
适合敏捷开发、持续交付
核心概念
以下是Docker的三个基本概念。
Image(镜像)
官方而言,Docker 镜像是一个特殊的文件系统,除了提供容器运行时所需的程序、库、资源、配置等文件外,还包含了一些为运行时准备的一些配置参数(如匿名卷、环境变量、用户等)。镜像不包含任何动态数据,其内容在构建之后也不会被改变。
对博主而言,它相当于就是个Java Class
(类)=.=
但它的存储结构类似Git
,一层一层地网上盖,删除一个文件并不会真的删除,只是在那个文件上面做了一个标记为已删除。在最终容器运行的时候,虽然不会看到这个文件,但是实际上该文件会一直跟随镜像。因此,在构建镜像的时候,需要额外小心,每一层尽量只包含该层需要添加的东西,任何额外的东西应该在该层构建结束前清理掉。
Container(容器)
通俗来说,如果镜像是类,那么容器就是这个类的实例了,镜像是静态的定义,容器是镜像运行时的实体。容器可以被创建、启动、停止、删除、暂停等。
容器也有其特性,例如存储,不指定数据卷(Volume
)的话,容器消亡数据也就跟着没了…
跟多特性请自行百度~
Repository(仓库)
仓库没啥好说的了,以 Ubuntu
镜像 为例,ubuntu
是仓库的名字,其内包含有不同的版本标签,如,14.04
, 16.04
。我们可以通过 ubuntu:14.04
,或者 ubuntu:16.04
来具体指定所需哪个版本的镜像。如果忽略了标签,比如 ubuntu
,那将视为 ubuntu:latest
安装
这里以Ubuntu为例(当然是因为博主用的是Ubuntu= =),版本的话Docker目前支持的Ubuntu版本最低为12.04LTS,但从稳定性上考虑,推荐使用14.04LTS或更高的版本。
使用脚本自动安装
在测试或开发环境中 Docker 官方为了简化安装流程,提供了一套便捷的安装脚本,Ubuntu 系统上可以使用这套脚本安装:
1 | curl -fsSL get.docker.com -o get-docker.sh |
执行这个命令后,脚本就会自动的将一切准备工作做好,并且把 Docker 安装在系统中
使用 APT 镜像源 安装
1 | sudo apt-get update |
鉴于国内网络问题,强烈建议使用国内源
国内源
1 | curl -fsSL https://mirrors.aliyun.com/docker-ce/linux/ubuntu/gpg | sudo apt-key add - |
以上命令会添加 稳定 版本的 Docker CE APT 镜像源,如果需要最新版本的 Docker CE 请将 stable
改为 edge
或者 test
。从 Docker 17.06 开始,edge
test
版本的 APT 镜像源也会包含稳定版本的 Docker
官方源
1 | curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo apt-key add - |
安装 Docker CE
1 | sudo apt-get update |
启动 Docker CE
1 | sudo systemctl enable docker |
建立 docker 用户组
默认情况下,docker
命令会使用 Unix socket 与 Docker 引擎通讯。而只有 root
用户和 docker
组的用户才可以访问 Docker 引擎的 Unix socket。出于安全考虑,一般 Linux 系统上不会直接使用 root
用户。因此,更好地做法是将需要使用 docker
的用户加入 docker
用户组。
建立 docker
组(貌似执行了自动安装脚本会自动建一个docker的用户组):
1 | sudo groupadd docker |
将当前用户加入 docker
组:
1 | sudo usermod -aG docker $USER |
加入docker
组之后要重启才能生效哦…
Mirror Acceleration
没有代理的话国内访问Docker Hub的速度实在感人,但Docker官方和国内很多云服务商都提供了加速器服务:
如阿里,注册并申请后会得到加速域名如https://vioqnt8w.mirror.aliyuncs.com
,然后正如官方说的一样,通过修改daemon
配置文件/etc/docker/daemon.json
来使用加速器:
1 | sudo mkdir -p /etc/docker |
查看生效:
1 | sudo docker info|grep "Registry Mirrors" -A 1 |
输出如下:
1 | Registry Mirrors: |
镜像的相关操作
获取
Docker Hub 上有大量的高质量的镜像可以用,我们可以通过以下的方式获取镜像:
1 | docker pull [选项] [Docker Registry地址]<仓库名>:<标签> |
选项可以通过docker pull --help
查看。
eg,从Docker Hub下载REPOSITORY
为java
的所有镜像:
1 | docker pull -a java |
列出
使用docker images [OPTIONS] [REPOSITORY[:TAG]]
列出已下载的镜像
列表包含了仓库名、标签、镜像 ID、创建时间以及所占用的空间
OPTIONS说明:
1 | -a :列出本地所有的镜像(含中间映像层,默认情况下,过滤掉中间映像层); |
eg:
1 | # 看到在 mongo:3.2 之后建立的镜像,想查看某个位置之前的镜像也可以,只需要把 since 换成 before 即可 |
虚悬镜像(dangling image)
举个例子:原来为 mongo:3.2
,随着官方镜像维护,发布了新版本后,重新 docker pull mongo:3.2
时,mongo:3.2
这个镜像名被转移到了新下载的镜像身上,而旧的镜像上的这个名称则被取消,从而成为了 <none>
。除了 docker pull
可能导致这种情况,docker build
也同样可以导致这种现象。由于新旧镜像同名,旧镜像名称被取消,从而出现仓库名、标签均为 <none>
的镜像。这类无标签镜像也被称为 虚悬镜像(dangling image) ,可以用下面的命令专门显示这类镜像:
1 | docker images -f dangling=true |
一般来说,虚悬镜像已经失去了存在的价值,是可以随意删除的,可以用下面的命令删除:
1 | docker rmi $(docker images -q -f dangling=true) |
Commit
从容器创建一个新的镜像:
1 | docker commit [OPTIONS] CONTAINER [REPOSITORY[:TAG]] |
OPTIONS说明:
1 | -a :提交的镜像作者; |
eg:
1 | docker commit -a "ybd" -m "my apache" a404c6c174a2 mymysql:v1 |
当我们修改了镜像文件提交时候,可以使用docker diff [OPTIONS] CONTAINER
查看修改了什么东西。
一般地,不推荐使用commit
来构建镜像,之前也提过,镜像是特殊的文件系统,改了东西之后原来的基础之上叠加,使之变得越来越臃肿。此外,使用 docker commit
意味着所有对镜像的操作都是黑箱操作,生成的镜像也被称为黑箱镜像,换句话说,就是除了制作镜像的人知道执行过什么命令、怎么生成的镜像,别人根本无从得知。一般我们会使用Dockerfile
定制镜像。
删除
删除镜像可以使用:
1 | docker rmi [OPTIONS] IMAGE [IMAGE...] |
OPTIONS说明:
1 | -f :强制删除; |
一般会组合使用:
1 | docker rmi $(docker images -q -f dangling=true) |
查看元数据
docker inspect : 获取容器/镜像的元数据。
1 | docker inspect [OPTIONS] CONTAINER|IMAGE [CONTAINER|IMAGE...] |
OPTIONS说明:
1 | -f :指定返回值的模板文件。 |
实例
获取镜像mysql:5.6的元信息。
1 | ~: docker inspect mysql:5.6 |
获取正在运行的容器mymysql的 IP。
1 | ~: docker inspect -f '' mymysql |
查看容器内部IP:
1 | docker inspect --format='{\{.NetworkSettings.IPAddress}}' CONTAINER |
标签
docker tag:
1 | docker tag IMAGE/CONTAINER TAG |
ex:
1 | 将同一IMAGE_ID的所有tag,合并为一个新的 |
保存镜像到归档文件
docker save : 将指定镜像保存成 tar 归档文件。
1 | docker save [OPTIONS] IMAGE [IMAGE...] |
OPTIONS说明:
1 | -o :输出到的文件。 |
实例
将镜像runoob/ubuntu:v3 生成my_ubuntu_v3.tar文档
runoob@runoob:~$ docker save -o my_ubuntu_v3.tar runoob/ubuntu:v3
导入镜像
Import
docker import : 从归档文件中创建镜像。
1 | docker import [OPTIONS] file|URL|- [REPOSITORY[:TAG]] |
OPTIONS说明:
1 | -c :应用docker 指令创建镜像; |
例如:
1 | docker import ubuntu.tar ybd/ubuntu:v1 |
Load
Usage: docker load [OPTIONS]
Load an image from a tar archive or STDIN
Options:
1 | -i, --input string Read from tar archive file, instead of STDIN |
区别
- 首先,docker import可以重新指定镜像的名字,docker load不可以
- 其次,我们发现导出后的版本会比原来的版本稍微小一些。那是因为导出后,会丢失历史和元数据。执行下面的命令就知道了:
显示镜像的所有层(layer)docker images --tree
执行命令,显示下面的内容。正你看到的,导出后再导入(exported-imported)的镜像会丢失所有的历史,而保存后再加载(saveed-loaded)的镜像没有丢失历史和层(layer)。这意味着使用导出后再导入的方式,你将无法回滚到之前的层(layer),同时,使用保存后再加载的方式持久化整个镜像,就可以做到层回滚(可以执行docker tag 来回滚之前的层)。
容器的相关操作
开启
docker run :创建一个新的容器并运行一个命令 docker create :创建一个新的容器但不启动它
1 | docker run [OPTIONS] IMAGE [COMMAND] [ARG...] |
docker run OPTIONS说明:
1 | -a stdin: 指定标准输入输出内容类型,可选 STDIN/STDOUT/STDERR 三项; |
例如,启动一个 bash 终端,允许用户进行交互:
1 | docker run -t -i ubuntu:14.04 /bin/bash |
当利用 docker run
来创建容器时,Docker 在后台运行的标准操作包括:
- 检查本地是否存在指定的镜像,不存在就从公有仓库下载
- 利用镜像创建并启动一个容器
- 分配一个文件系统,并在只读的镜像层外面挂载一层可读写层
- 从宿主主机配置的网桥接口中桥接一个虚拟接口到容器中去
- 从地址池配置一个 ip 地址给容器
- 执行用户指定的应用程序
- 执行完毕后容器被终止
启动一个es并指明healthcheck相关策略:
1 | docker run --rm -d \ |
暂停
docker pause :暂停容器中所有的进程。
docker unpause :恢复容器中所有的进程。
1 | docker pause [OPTIONS] CONTAINER [CONTAINER...] |
实例
暂停数据库容器db01提供服务。
1 | docker pause db01 |
恢复数据库容器db01提供服务。
1 | docker unpause db01 |
停止
docker stop :停止一个运行中的容器:
1 | docker stop [OPTIONS] CONTAINER [CONTAINER...] |
杀掉容器
docker kill :杀掉一个运行中的容器。
1 | docker kill [OPTIONS] CONTAINER [CONTAINER...] |
OPTIONS说明:
1 | -s :向容器发送一个信号 |
实例
杀掉运行中的容器mynginx
1 | docker kill -s KILL mynginx |
进入容器
使用docker exec :
1 | docker exec [OPTIONS] CONTAINER COMMAND [ARG...] |
OPTIONS说明:
1 | -d :分离模式: 在后台运行 |
例如进入ubuntu容器交互式模式:
1 | docker exec -it ubuntu /bin/sh |
或者使用docker attach:
1 | docker attach --sig-proxy=false CONTAINER |
attach
是可以带上--sig-proxy=false
来确保CTRL-D
或CTRL-C
不会关闭容器。
导出容器
导出容器快照
1 | docker export [OPTIONS] CONTAINER |
例如:
1 | docker export 7691a814370e > ubuntu.tar |
删除
1 | docker rm [OPTIONS] CONTAINER [CONTAINER...] |
OPTIONS说明:
1 | -f :通过SIGKILL信号强制删除一个运行中的容器 |
删除所有容器:
1 | docker rm $(docker ps -a -q) |
但这并不会删除运行中的容器
列出容器
1 | docker ps [OPTIONS] |
OPTIONS说明:
1 | -a :显示所有的容器,包括未运行的。 |
例如列出最近创建的5个容器信息:
1 | docker ps -n 5 |
列出所有创建的容器ID:
1 | docker ps -a -q |
下面是docker官方的filter参数:
Filter | Description |
---|---|
id |
Container’s ID |
name |
Container’s name |
label |
An arbitrary string representing either a key or a key-value pair. Expressed as <key> or <key>=<value> |
exited |
An integer representing the container’s exit code. Only useful with --all . |
status |
One of created , restarting , running , removing , paused , exited , or dead |
ancestor |
Filters containers which share a given image as an ancestor. Expressed as <image-name>[:<tag>] , <image id> , or <image@digest> |
before or since |
Filters containers created before or after a given container ID or name |
volume |
Filters running containers which have mounted a given volume or bind mount. |
network |
Filters running containers connected to a given network. |
publish or expose |
Filters containers which publish or expose a given port. Expressed as <port>[/<proto>] or <startport-endport>/[<proto>] |
health |
Filters containers based on their healthcheck status. One of starting , healthy , unhealthy or none . |
isolation |
Windows daemon only. One of default , process , or hyperv . |
is-task |
Filters containers that are a “task” for a service. Boolean option (true or false ) |
ex 列出所有状态为退出的容器:
1 | docker ps -q --filter status=exited |
查看日志
1 | docker logs [OPTIONS] CONTAINER |
OPTIONS说明:
1 | -f : 跟踪日志输出 |
数据拷贝
docker cp :用于容器与主机之间的数据拷贝。
1 | docker cp [OPTIONS] CONTAINER:SRC_PATH DEST_PATH|- |
OPTIONS说明:
1 | -L :保持源目标中的链接 |
实例
将主机/www/runoob目录拷贝到容器96f7f14e99ab的/www目录下。
1 | docker cp /www/runoob 96f7f14e99ab:/www/ |
将主机/www/runoob目录拷贝到容器96f7f14e99ab中,目录重命名为www。
1 | docker cp /www/runoob 96f7f14e99ab:/www |
将容器96f7f14e99ab的/www目录拷贝到主机的/tmp目录中。
1 | docker cp 96f7f14e99ab:/www /tmp/ |
Volume的相关操作
Usage: docker volume COMMAND
Command | Description |
---|---|
docker volume create | Create a volume |
docker volume inspect | Display detailed information on one or more volumes |
docker volume ls | List volumes |
docker volume prune | Remove all unused volumes |
docker volume rm | Remove one or more volumes |
ex 删除所有悬浮的volume:
1 | docker volume rm $(docker volume ls -q -f dangling=true) |
选择 -v 还是 -–mount 参数
Docker 新用户应该选择 --mount
参数,经验丰富的 Docker 使用者对 -v
或者 --volume
已经很熟悉了,但是推荐使用 --mount
参数。
使用 --mount
标记可以指定挂载一个本地主机的目录到容器中去。
1 | $ docker run -d -P \ |
上面的命令加载主机的 /src/webapp
目录到容器的 /opt/webapp
目录。这个功能在进行测试的时候十分方便,比如用户可以放置一些程序到本地目录中,来查看容器是否正常工作。本地目录的路径必须是绝对路径,以前使用 -v
参数时如果本地目录不存在 Docker 会自动为你创建一个文件夹,现在使用 --mount
参数时如果本地目录不存在,Docker 会报错。
Docker 挂载主机目录的默认权限是 读写
,用户也可以通过增加 readonly
指定为 只读
。
1 | docker run -d -P \ |
Network的相关操作
基本命令:
1 | docker network create |
下面先创建一个新的 Docker 网络。
1 | docker network create -d bridge my-net |
-d
参数指定 Docker 网络类型,有 bridge
overlay
。其中 overlay
网络类型用于 Swarm mode
容器链接网络:
1 | docker run -it --rm --name busybox1 --network my-net busybox sh |
创建一个Swarm mode网络:
1 | docker network create \ |
Dockerfile 详解
制作一个镜像可以使用
docker commit
和定制Dockerfile,但推荐的是写Dockerfile。因为
docker commit
是一个暗箱操作,除了制作镜像的人知道执行过什么命令、怎么生成的镜像,别人根本无从得知,而且会加入一些没用的操作导致镜像臃肿。
Build Images
首先在当前空目录创建一个Dockerfile:
1 | FROM ubuntu:latest |
然后在当前目录打开终端:
1 | docker build -t <repo-name>/<image-name>:<tag> . |
其中<repo-name>
表示仓库名,与远程仓库(如docker hub)名字要一致,<tag>
表示标签,不给默认latest
,都是可选项,例如可以写成这样:
1 | docker build -t <image-name> . |
看到Successfully built
就表示构建成功了
注意docker build
命令最后有一个 .
表示构建的上下文,镜像构建需要把上下文的东西上传到Docker引擎去构建。
Dockerfile 指令
From 指定基础镜像
所谓定制镜像,那一定是以一个镜像为基础,在其上进行定制。而 FROM
就是指定基础镜像,因此一个 Dockerfile
中 FROM
是必备的指令,并且必须是第一条指令。
在 Docker Hub上有非常多的高质量的官方镜像, 有可以直接拿来使用的服务类的镜像,如 nginx
、redis
、mongo
、mysql
、httpd
、php
、tomcat
等; 也有一些方便开发、构建、运行各种语言应用的镜像,如 node
、openjdk
、python
、ruby
、golang
等。 可以在其中寻找一个最符合我们最终目标的镜像为基础镜像进行定制。 如果没有找到对应服务的镜像,官方镜像中还提供了一些更为基础的操作系统镜像,如 ubuntu
、debian
、centos
、fedora
、alpine
等,这些操作系统的软件库为我们提供了更广阔的扩展空间。
除了选择现有镜像为基础镜像外,Docker 还存在一个特殊的镜像,名为 scratch
。这个镜像是虚拟的概念,并不实际存在,它表示一个空白的镜像。
RUN 执行命令
RUN
指令是用来执行命令行命令的。由于命令行的强大能力,RUN
指令在定制镜像时是最常用的指令之一。其格式有两种:
shell 格式:
RUN <命令>
,就像直接在命令行中输入的命令一样。刚才写的 Dockrfile 中的RUN
指令就是这种格式。1
RUN echo '<h1>Hello, Docker!</h1>' > /usr/share/nginx/html/index.html
exec 格式:
RUN ["可执行文件", "参数1", "参数2"]
,这更像是函数调用中的格式。
注意:
- RUN命令尽量精简,也就是像上面一样一个RUN(使用
$$ \
),如果分开写很多个RUN会导致镜像铺了很多层从而臃肿。 - RUN最后记住清理掉没用的垃圾,很多人初学 Docker 制作出了很臃肿的镜像的原因之一,就是忘记了每一层构建的最后一定要清理掉无关文件。
COPY 复制文件
格式:
COPY <源路径>... <目标路径>
COPY ["<源路径1>",... "<目标路径>"]
和 RUN
指令一样,也有两种格式,一种类似于命令行,一种类似于函数调用。
COPY
指令将从构建上下文目录中 <源路径>
的文件/目录复制到新的一层的镜像内的 <目标路径>
位置。比如:
1 | COPY package.json /usr/src/app/ |
ADD 更高级的复制文件
ADD
指令和 COPY
的格式和性质基本一致。但是在 COPY
基础上增加了一些功能。
比如 <源路径>
可以是一个 URL
,这种情况下,Docker 引擎会试图去下载这个链接的文件放到 <目标路径>
去。下载后的文件权限自动设置为 600
,如果这并不是想要的权限,那么还需要增加额外的一层 RUN
进行权限调整,另外,如果下载的是个压缩包,需要解压缩,也一样还需要额外的一层 RUN
指令进行解压缩。所以不如直接使用 RUN
指令,然后使用 wget
或者 curl
工具下载,处理权限、解压缩、然后清理无用文件更合理。因此,这个功能其实并不实用,而且不推荐使用。
如果 <源路径>
为一个 tar
压缩文件的话,压缩格式为 gzip
, bzip2
以及 xz
的情况下,ADD
指令将会自动解压缩这个压缩文件到 <目标路径>
去。
在某些情况下,这个自动解压缩的功能非常有用,比如官方镜像 ubuntu
中:
1 | FROM scratch |
但在某些情况下,如果我们真的是希望复制个压缩文件进去,而不解压缩,这时就不可以使用 ADD
命令了。
在 Docker 官方的最佳实践文档中要求,尽可能的使用 COPY
,因为 COPY
的语义很明确,就是复制文件而已,而 ADD
则包含了更复杂的功能,其行为也不一定很清晰。最适合使用 ADD
的场合,就是所提及的需要自动解压缩的场合。
另外需要注意的是,ADD
指令会令镜像构建缓存失效,从而可能会令镜像构建变得比较缓慢。
因此在 COPY
和 ADD
指令中选择的时候,可以遵循这样的原则,所有的文件复制均使用 COPY
指令,仅在需要自动解压缩的场合使用 ADD
。
CMD 容器启动命令
CMD
指令就是用于指定默认的容器主进程的启动命令的。
CMD
指令的格式和 RUN
相似,也是两种格式:
shell
格式:CMD <命令>
exec
格式:CMD ["可执行文件", "参数1", "参数2"...]
- 参数列表格式:
CMD ["参数1", "参数2"...]
。在指定了ENTRYPOINT
指令后,用CMD
指定具体的参数。
在运行时可以指定新的命令来替代镜像设置中的这个默认命令,比如,ubuntu
镜像默认的 CMD
是 /bin/bash
,如果我们直接 docker run -it ubuntu
的话,会直接进入 bash
。我们也可以在运行时指定运行别的命令,如 docker run -it ubuntu cat /etc/os-release
。这就是用 cat /etc/os-release
命令替换了默认的 /bin/bash
命令了,输出了系统版本信息。
在指令格式上,一般推荐使用 exec
格式,这类格式在解析时会被解析为 JSON 数组,因此一定要使用双引号 "
,而不要使用单引号。
如果使用 shell
格式的话,实际的命令会被包装为 sh -c
的参数的形式进行执行。比如:
1 | CMD echo $HOME |
在实际执行中,会将其变更为:
1 | CMD [ "sh", "-c", "echo $HOME" ] |
所以如果使用shell
格式会导致容器莫名退出,因为实际上执行的事sh
命令,而sh
命令执行完时候容器也就没有存在的意义。
ENTRYPOINT 入口点
ENTRYPOINT
的格式和 RUN
指令格式一样,分为 exec
格式和 shell
格式。
ENTRYPOINT
的目的和 CMD
一样,都是在指定容器启动程序及参数。ENTRYPOINT
在运行时也可以替代,不过比 CMD
要略显繁琐,需要通过 docker run
的参数 --entrypoint
来指定。
当指定了 ENTRYPOINT
后,CMD
的含义就发生了改变,不再是直接的运行其命令,而是将 CMD
的内容作为参数传给 ENTRYPOINT
指令,换句话说实际执行时,将变为:
1 | <ENTRYPOINT> "<CMD>" |
这个指令非常有用,例如可以把命令后面的参数传进来或启动容器前准备一些环境然后执行启动命令(通过脚本exec "$@"
)。
ENV 设置环境变量
格式有两种:
ENV <key> <value>
ENV <key1>=<value1> <key2>=<value2>...
这个指令很简单,就是设置环境变量而已,无论是后面的其它指令,如 RUN
,还是运行时的应用,都可以直接使用这里定义的环境变量。
ex:
1 | ENV NODE_VERSION 6 |
ARG 构建参数
格式:ARG <参数名>[=<默认值>]
构建参数和 ENV
的效果一样,都是设置环境变量。所不同的是,ARG
所设置的构建环境的环境变量,在将来容器运行时是不会存在这些环境变量的。但是不要因此就使用 ARG
保存密码之类的信息,因为 docker history
还是可以看到所有值的。
Dockerfile
中的 ARG
指令是定义参数名称,以及定义其默认值。该默认值可以在构建命令 docker build
中用 --build-arg <参数名>=<值>
来覆盖。
在 1.13 之前的版本,要求 --build-arg
中的参数名,必须在 Dockerfile
中用 ARG
定义过了,换句话说,就是 --build-arg
指定的参数,必须在 Dockerfile
中使用了。如果对应参数没有被使用,则会报错退出构建。从 1.13 开始,这种严格的限制被放开,不再报错退出,而是显示警告信息,并继续构建。这对于使用 CI 系统,用同样的构建流程构建不同的 Dockerfile
的时候比较有帮助,避免构建命令必须根据每个 Dockerfile 的内容修改。
VOLUME 定义匿名卷
格式为:
VOLUME ["<路径1>", "<路径2>"...]
VOLUME <路径>
之前我们说过,容器运行时应该尽量保持容器存储层不发生写操作,对于数据库类需要保存动态数据的应用,其数据库文件应该保存于卷(volume)中,后面的章节我们会进一步介绍 Docker 卷的概念。为了防止运行时用户忘记将动态文件所保存目录挂载为卷,在 Dockerfile
中,我们可以事先指定某些目录挂载为匿名卷,这样在运行时如果用户不指定挂载,其应用也可以正常运行,不会向容器存储层写入大量数据。
1 | VOLUME /data |
这里的 /data
目录就会在运行时自动挂载为匿名卷,任何向 /data
中写入的信息都不会记录进容器存储层,从而保证了容器存储层的无状态化。当然,运行时可以覆盖这个挂载设置。比如:
1 | docker run -d -v mydata:/data xxxx |
在这行命令中,就使用了 mydata
这个命名卷挂载到了 /data
这个位置,替代了 Dockerfile
中定义的匿名卷的挂载配置。
EXPOSE 声明端口
格式为 EXPOSE <端口1> [<端口2>...]
。EXPOSE
指令是声明运行时容器提供服务端口,这只是一个声明,在运行时并不会因为这个声明应用就会开启这个端口的服务。在 Dockerfile 中写入这样的声明有两个好处,一个是帮助镜像使用者理解这个镜像服务的守护端口,以方便配置映射;另一个用处则是在运行时使用随机端口映射时,也就是 docker run -P
时,会自动随机映射 EXPOSE
的端口。
此外,在早期 Docker 版本中还有一个特殊的用处。以前所有容器都运行于默认桥接网络中,因此所有容器互相之间都可以直接访问,这样存在一定的安全性问题。于是有了一个 Docker 引擎参数 --icc=false
,当指定该参数后,容器间将默认无法互访,除非互相间使用了 --links
参数的容器才可以互通,并且只有镜像中 EXPOSE
所声明的端口才可以被访问。这个 --icc=false
的用法,在引入了 docker network
后已经基本不用了,通过自定义网络可以很轻松的实现容器间的互联与隔离。
要将 EXPOSE
和在运行时使用 -p <宿主端口>:<容器端口>
区分开来。-p
,是映射宿主端口和容器端口,换句话说,就是将容器的对应端口服务公开给外界访问,而 EXPOSE
仅仅是声明容器打算使用什么端口而已,并不会自动在宿主进行端口映射。
WORKDIR 指定工作目录
格式为 WORKDIR <工作目录路径>
。
使用 WORKDIR
指令可以来指定工作目录(或者称为当前目录),以后各层的当前目录就被改为指定的目录,如该目录不存在,WORKDIR
会帮你建立目录。
之前提到一些初学者常犯的错误是把 Dockerfile
等同于 Shell 脚本来书写,这种错误的理解还可能会导致出现下面这样的错误:
1 | RUN cd /app |
如果将这个 Dockerfile 进行构建镜像运行后,会发现找不到 /app/world.txt
文件,或者其内容不是 hello
。原因其实很简单,在 Shell 中,连续两行是同一个进程执行环境,因此前一个命令修改的内存状态,会直接影响后一个命令;而在 Dockerfile 中,这两行 RUN
命令的执行环境根本不同,是两个完全不同的容器。这就是对 Dokerfile 构建分层存储的概念不了解所导致的错误。
之前说过每一个 RUN
都是启动一个容器、执行命令、然后提交存储层文件变更。第一层 RUN cd /app
的执行仅仅是当前进程的工作目录变更,一个内存上的变化而已,其结果不会造成任何文件变更。而到第二层的时候,启动的是一个全新的容器,跟第一层的容器更完全没关系,自然不可能继承前一层构建过程中的内存变化。
因此如果需要改变以后各层的工作目录的位置,那么应该使用 WORKDIR
指令。
USER 指定当前用户
格式:USER <用户名>
USER
指令和 WORKDIR
相似,都是改变环境状态并影响以后的层。WORKDIR
是改变工作目录,USER
则是改变之后层的执行 RUN
, CMD
以及 ENTRYPOINT
这类命令的身份。
当然,和 WORKDIR
一样,USER
只是帮助你切换到指定用户而已,这个用户必须是事先建立好的,否则无法切换。
1 | RUN groupadd -r redis && useradd -r -g redis redis |
HEALTHCHECK 健康检查
格式:
HEALTHCHECK [选项] CMD <命令>
:设置检查容器健康状况的命令HEALTHCHECK NONE
:如果基础镜像有健康检查指令,使用这行可以屏蔽掉其健康检查指令
HEALTHCHECK
支持下列选项:
--interval=<间隔>
:两次健康检查的间隔,默认为 30 秒;--timeout=<间隔>
:健康检查命令运行超时时间,如果超过这个时间,本次健康检查就被视为失败,默认 30 秒;--retries=<次数>
:当连续失败指定次数后,则将容器状态视为unhealthy
,默认 3 次。--start-period=<间隔>
: 应用的启动的初始化时间,在启动过程中的健康检查失效不会计入,默认 0 秒; (从17.05)引入
在 HEALTHCHECK [选项] CMD
后面的命令,格式和 ENTRYPOINT
一样,分为 shell
格式,和 exec
格式。命令的返回值决定了该次健康检查的成功与否:
0
:成功;1
:失败;2
:保留值,不要使用
容器启动之后,初始状态会为 starting
(启动中)。Docker Engine会等待 interval
时间,开始执行健康检查命令,并周期性执行。如果单次检查返回值非0或者运行需要比指定 timeout
时间还长,则本次检查被认为失败。如果健康检查连续失败超过了 retries
重试次数,状态就会变为 unhealthy
(不健康)。
注:
- 一旦有一次健康检查成功,Docker会将容器置回
healthy
(健康)状态 - 当容器的健康状态发生变化时,Docker Engine会发出一个
health_status
事件。
假设我们有个镜像是个最简单的 Web 服务,我们希望增加健康检查来判断其 Web 服务是否在正常工作,我们可以用 curl
来帮助判断,其 Dockerfile
的 HEALTHCHECK
可以这么写:
1 | FROM elasticsearch:5.5 |
踩坑
- Dockerfile里也需要注意权限问题(nodejs7版本以上不能正常安装hexo,需要创建用户并制定权限去安装)
- 在docker容器里如果是root用户对挂载的文件进行了操作,那么实际上挂载文件的权限也变成了root的
- 使用attach进入容器,退出的时候容器也跟着退出了。。。囧
- 每一个RUN是一个新的shell
su -
之前在启动脚本加了-
,导致环境变量以及工作目录都变了
Hexo Dockerfile
1 | FROM ubuntu:16.04 |
docker-entrypoint.sh
:
1 | #!/bin/sh |
修改Docker默认镜像,容器存放位置
方法一、软链接
默认情况下Docker的存放位置为:/var/lib/docker
可以通过下面命令查看具体位置:
1 | sudo docker info | grep "Docker Root Dir" |
解决这个问题,最直接的方法当然是挂载分区到这个目录,但是我的数据盘还有其他东西,这肯定不好管理,所以采用修改镜像和容器的存放路径的方式达到目的。
这个方法里将通过软连接来实现。
首先停掉Docker服务:
1 | systemctl restart docker |
然后移动整个/var/lib/docker
目录到目的路径:
1 | mv /var/lib/docker /root/data/docker |
这时候启动Docker时发现存储目录依旧是/var/lib/docker
,但是实际上是存储在数据盘的,你可以在数据盘上看到容量变化。
方法二、修改镜像和容器的存放路径
指定镜像和容器存放路径的参数是--graph=/var/lib/docker
,我们只需要修改配置文件指定启动参数即可。
Docker 的配置文件可以设置大部分的后台进程参数,在各个操作系统中的存放位置不一致,在 Ubuntu 中的位置是:/etc/default/docker
,在 CentOS 中的位置是:/etc/sysconfig/docker
。
如果是 CentOS 则添加下面这行:
1 | OPTIONS=--graph="/root/data/docker" --selinux-enabled -H fd:// |
如果是 Ubuntu 则添加下面这行(因为 Ubuntu 默认没开启 selinux):
1 | OPTIONS=--graph="/root/data/docker" -H fd:// |
最后重新启动,Docker 的路径就改成 /root/data/docker
了。
定期清理容器日志
通过logrotate服务实现日志定期清理和回卷
logrotate是个十分有用的工具,它可以自动对日志进行截断(或轮循)、压缩以及删除旧的日志文件。例如,你可以设置logrotate,让/var/log/foo日志文件每30天轮循,并删除超过6个月的日志。配置完后,logrotate的运作完全自动化,不必进行任何进一步的人为干预。
https://github.com/blacklabelops/logrotate
通过修改dockerd参数进行回卷和清理
在/etc/docker/daemon.json
中添加log-driver
以及log-opts
参数:
1 | { |
参数说明: 设置单个容器日志超过10M则进行回卷,回卷的副本数超过3个就进行清理。
重启docker
1 | sudo systemctl daemon-reload && sudo systemctl restart docker |
数据卷备份与恢复
数据卷备份
1 | docker run --rm \ |
--rm
: 执行完命令之后移除容器--volumes-from <Container>
: 连接要备份数据的容器-v $(pwd):/backup
: 挂载当前路径到容器 busybox 容器,数据将会备份到此路径busybox
: 非常小的镜像tar cvf /backup/backup.tar /data
: 将 /data 路径下的文件打包到 backup.tar
数据卷恢复
1、新建容器
1 | docker run -v /data --name <ContainerName> <Image> |
2、恢复数据
1 | docker run --rm \ |
注意:其中的路径 /data 仅为示例,具体需要备份的文件路径请结合自身需求。
使用Github自动构建Docker
一开始玩Docker总是用别人的镜像确实很爽
歪歪…
But,如果要定制个性化的Image那就必须要自己写Dockerfile了,但是每一次修改完Dockerfile,都要经过几个步骤:
Built -> Push -> Delete invalid images
对于程序猿而言做重复的事情是很恐怖的,所以博主选择Github自动构建Docker Image~
创建用于自动构建的仓库
在Github上面创建一个项目并把Dockerfile以及上下文需要用到的文件放到里面。
链接仓库服务
首先需要绑定一个仓库服务(Github):
1、登录Docker Hub
;
2、选择 Profile
> Settings
> Linked Accounts & Services
;
3、选择需要连接的仓库服务(目前只支持Github
和BitBucket
);
4、这时候需要授权,点击授权就可以了。
创建一个自动构建
自动构建需要创建对应的仓库类型
自动构建仓库也可以使用docker push
把已有的镜像上传上去
1、选择Create
> Create Automated Build
;
2、选择Github
;
3、接下来会列出User/Organizations
的所有项目,从中选择你需要的构建的项目(包含Dockerfile);
4、可以选择Click here to customize
自定义路径;
5、最后点击创建就可以了。
集成到Github
用过Github
自动构建当然需要Github
的支持啦,这里只需要在Github里面点两下就配置完成,很方便:
在Add Service
里面找到Docker
并添加
构建设置
勾选自动构建
系统会默认帮我们勾上自动构建选项:
这时候,当我们的Dockerfile有变动会自动触发构建:
还在构建过程中我们可以点击Cancel取消构建过程。
添加新的构建
Docker hub默认选择master分支作为latest版本,我们可以根据自己的标签或分支构建不同的版本:
(点击箭头位置会出现例子)
这样,当我们创建一个标签如1.0.2并push上去的时候会自动触发构建~
Git
标签相关请看:Git标签管理
远程触发构建
当然我们也可以远程触发构建,同样在Build Setting页面:
然后例子已经说的很清楚了
参考:https://docs.docker.com/docker-hub/builds/
使用代理构建镜像
有时候,我们构建镜像需要在镜像内安装一些软件,因为构建时采用的是bridge
模式,对于一些资源比较稀缺或需要科学上网才能安装的软件慢得简直无法忍受。对此,我们可以在构建时设置构建参数(--build-arg
)从而达到代理安装的目的。或者也可以用官方的Docker Hub自动构建,或者将Dockerfile上传到VPS进行构建=.=…但感觉没必要。
例如像下面Dockerfile
:
1 | FROM XXXXXX |
然后构建:
1 | docker build --build-arg HTTP_PROXY=192.168.6.113:8118 -t yangbingdong/oraclejdk8 . |
192.168.6.113:8118
是从Sock5转换过来的http代理
注意:镜像内软件安装完成时候需要将代理置空,所以上面示例最后两行后面的值是空的,否则接下来容器内发生的网络访问都会走代理…
Self Usage Docker Or Compose
以下是个人使用的一些容器运行命令或者docker-compose.yml
,不定时更新
Mysql
运行实例:
1 | MYSQL_DATA=${HOME}/data/docker/mysql/data && \ |
链接Mysql服务端需要安装终端客户端:
1 | sudo apt install mysql-client |
链接:
1 | mysql -h 127.0.0.1 -P 3306 -u root -p |
Redis
运行实例:
1 | REDIS_DATA=${HOME}/data/docker/redis/data && \ |
链接Redis服务端需要安装终端客户端:
1 | sudo apt install redis-tools |
链接:
1 | redis-cli |
Portainer
功能:管理容器与swarm集群
单机版:
1 | docker run -d -p 9000:9000 \ |
集群版:
1 | PORTAINER_DATA=${HOME}/data/docker/portainer/data |
Visualizer
1 | docker service create \ |
Nexus3
创建Volume :
1 | docker volume create --name nexus-data |
运行实例:
1 | NEXUS_PORT=8090 && \ |
查看启动日志:
1 | docker logs nexus |
备份:
1 | BAK_CONTAINER=ubuntu:latest && \ |
还原:
先要创建还原的Volume:
1 | docker volume create --name nexus-data1 |
然后:
1 | BAK_CONTAINER=ubuntu:latest && \ |
Shadowsocks
服务端:
1 | SS_PASSWORD=123456 && \ |
客户端:
1 | SS_IP=127.0.0.1 && \ |
加速需要开启BBR
Ngrok(服务端)
运行实例:
1 | NGROK_DATA=/root/docker/ngrok/data && \ |
Zookeeper集群
docker-compose.yml:
1 | version: '3.4' |
Kafka集群
docker-compose.yml
1 | version: '3.4' |
参考:http://www.blockchain4u.info/docker/2017/07/28/kafka-cluster-with-docker
Kafka Manager
docker-compose.yml:
1 | version: '3.4' |
Logrotate
功能:日志清理
单机运行:
1 | docker run -d \ |
docker-compose.yml:
1 | version: '3.4' |
ShowDoc
功能:API与数据字典管理
docker-compose.yml:
1 | version: '3.4' |
其中要把 ShowDoc 整个项目根目录所有文件拷贝到 data 里面,确保里面文件可执行 chmod -R 777 data
访问 localhost:4999/install
进行设置后把data里面的 install
目录删除防止再次安装。