Docker 入门教程

Docker 入门教程

July 31, 2018

该教程仅对docker常用操作进行了总结归纳

一、 什么是Docker

Docker是2013年,dotCloud用Golang开发的基于LXC的高级容器引擎

1.1 什么是LXC

Linux Container的简写。可以提供轻量级的虚拟化,以便隔离进程和资源,而且不需要提供指令解释机制以及全虚拟化的其他复杂性。相当于C++中的NameSpace

1.2 什么是容器

是一个相对独立的运行环境

1.3 容器和虚拟机的区别

1

Docker 虚拟机
启动速度 秒级 分钟级
启动方式 启动应用 启动Guest OS + 启动应用
交付、部署 Docker镜像(Dockerfile) 虚拟机镜像
更新 修改Dockerfile或发布新镜像 向虚拟机推送补丁升级包
操作系统 Linux x64 Linux、Windows、Mac OS等
磁盘占用率 低 高
性能利用率 高 中
成熟度 发展中 高
稳定性 发展中 高
隔离性 中 高
数据保留 删除后销毁 删除后销毁

二、 Docker的3个概念

Docker Git OOD
Image Repository Code Class
Container Local Code Instance
Registry GitHub/GitLab ​

2.1 Image

  • 镜像,Docker Image是一系列操作的集合,每一组操作形成一个layer,每个layer有一个id,并说明该layer依赖的前一个layer的id,多个layer堆叠形成Image,注意理解一组操作的意义

    18

    2

  • Image是静态的,不能直接使用。像面向对象中的类,也像Git中放在仓库中的代码

  • 可以给Image设置Repository,同一个Image ID可以有多个Repository,Repository的命名规范是“Registry Name/Image Name”,若Registry Name为空,则默认Registry为Docker HUB3

  • 可以给Image设置Tag,同一个Image ID可以有多个标签Tag,若tag为空,则默认Tag为latest,表示最新的镜像4

  • Image可以在本地通过Container Commit生成,亦可用Dockerfile生成,还可以从Registry Pull下来

2.2 Container

  • 容器,Docker Container 是Image的实例化对象
  • Container是动态的,可以使用的。像面向对象中的对象,也像Git中被pull到本地的代码,而且代码可以编译和运行
  • Container经过一系列操作后,并不会影响Image,如果需要生成新的Image,需要做类似git的流程对Container进行Commit操作
  • Container删除会导致Container中的数据消失,从Image启动新的Container是一个全新的容器,不能发现或共享其他Container中的数据5

2.3 Registry

  • 仓库,Docker Registry 是存放Image的地方
  • Registry像GitHub、Gitlab一样,有官方提供的Docker HUB ,也可以自己搭建
  • Registry里面的项目有公开和私有两种权限,公开项目下的镜像可以被任何人pull,私有项目的镜像只能被Registry分配权限的用户pull6

三、 Docker 版本与安装

7

8

四、 Docker常用指令

4.1 Image 指令

  • docker images

    查询所有镜像

    9

  • docker tag

    “复制”一个Image副本,并重新命名(包括Repository Name、Image Name和Tag)

    • docker tag f06 my/redis:4

      f06a5773f01e 可以简写开头的几位

    • docker tag redis:latest redis:4

    • docker tag redis my/redis

      表示把redis:latest 复制为 my/redis:latest

  • docker rmi

    删除指定的镜像

    10

    • docker rmi redis:4

      删除redis:4 这个Image,若该Image ID在本地有其他名称或Tag,则只移除这个Tag

    • docker rmi f06

      删除Image ID缩写为f06的 Image,该Image ID的所有副本(不同的Repository、Image Name和Tag)都会被删除

    • docker rmi -f f06

      默认情况下,rmi指令只能删除没有Container的Image;通过-f 参数可以强制删除有Container但Container是停止状态的Image;不能直接删除有运行状态Container的Image,必须先停止、删除Container后再删除Image

  • docker history

    查询指定镜像的操作历史

    11

    • docker history f06

      查看镜像的所有历史操作,甚至可以通过该指令看出镜像是如何构建出来的,但对于Container的操作在这里无法体现

  • docker inspect

    查询指定镜像的详细信息

    12

    • docker inspect f06

4.2 Container 指令

  • docker run

    从Image运行一个新Container

    • docker run --name myRedis f06

      从Image运行容器并通过–name 命名为myRedis

    • docker run -it f06 bash

      从Image运行容器,随机名称,-it 是 -i和-t,表示交互并重定向到控制台,bash表示容器入口,不填则进入镜像的默认入口

    • docker run -d f06

      -d 表示后台执行

  • docker ps

    查看容器列表

    13

    • docker ps

      查看仅在运行状态的Container

    • docker ps -a

      查看所有状态的Container

  • docker inspect

    查看指定容器的详细信息

    • docker inspect myRedis
  • docker exec

    14

    进入指定的容器

    • docker exec -it myRedis bash

      以交互模式进入myRedis 容器的bash

    • docker exec -u root -it myRedis bash

      同上,但以root用户登录

  • docker stop/start/restart

    停止/启动/重启容器

    • docker stop myRedis
    • docker start myRedis
    • docker restart myRedis
  • docker rm

    删除指定的容器

    • docker rm myRedis

      删除名为myRedis 且状态为退出、停止的容器

    • docker rm 3c7

      删除ID简写为3c7且状态为退出、停止的容器

    • docker rm -f myRedis

      强制删除名为myRedis 的容器,不管容器是否正在运行

4.3 Registry 指令

  • docker login

    登录指定的docker registry

    • docker login -u test -p xxx hub.docker.com

      以用户test,密码为xxx登录Docker HUB

  • docker pull

    从指定的docker registry pull指定的Image,对于私有镜像,必须先用docker login登录registry

    • docker pull test/redis:4

      从Docker HUB的test用户拉取redis:4 镜像

    • docker pull a.b.com/test/redis:4

      从a.b.com这个registry的test用户拉取redis:4 镜像

  • docker push

    把指定Image push到docker registry,必须先用docker login登录registry

    • docker tag redis:latest a.b.com/test/common/redis:4

    • docker push a.b.com/test/common/redis:4

      把redis:lastet复制为a.b.com/test/common/redis:4,并push到a.b.com的test用户的common项目下,镜像名为redis,Tag为4

4.4 练习

  • 从Docker HUB pull一个Nginx Image
  • 运行Nginx Container,并访问默认页面
  • 进入容器删除 /usr/share/nginx/html/index.html ,提交容器到新镜像
  • 关闭容器,从旧镜像启动Nginx Container,访问页面并观察
  • 关闭容器,从新镜像启动Nginx Container,访问页面并观察
  • 注册Docker HUB账号,将新镜像提交至自己的Docker HUB

五、 Docker Volume

由于Docker Container释放后不能保存数据,并且Container之间不能共享数据,所以需要“外接一个存储”用于数据的持久化和共享。这就是挂载卷Volume

PS:因为非Linux环境的Docker都是基于Linux虚拟机,所以在非Linux环境下使用Docker Volume需注意以下几点:

  • 如果使用DockerToolBox,需要给虚拟机挂载共享目录,只有这个目录可以作为Volume
  • 如果使用https://github.com/sonicrang/Docker_FrontEnd 我已经在控制台封装好设置虚拟机共享目录的功能,方法为控制台输入1,即可将本机目录例如"e:\test"挂到虚拟机"/develop"下,只有"/develop"目录可以作为Volume

挂载示例:

  • docker run -v /develop/nginx/log:/var/log/nginx --name myNginx nginx

    将宿主机的"/develop/nginx/log" 空目录挂载到容器的"/var/log/nginx",启动容器后,对于linux系统,可以在宿主机"/develop/nginx/log"看到容器内的日志文件;对于使用DockerToolBox或者Docker_FrontEnd,会在本机的"e:\test"看到容器中的内容。当容器删除后,宿主机目录中的日志文件会被保留下来

    15

  • docker run -v nginx:/var/log/nginx --name myNginx nginx

    将宿主机的空目录"/var/lib/docker/volumn/nginx" 挂载到容器的"/var/log/nginx",也就是说宿主机的目录可以填写为相对目录,相对于一个docker的默认安装目录

  • docker run -v /var/log/nginx --name myNginx nginx

    将宿主机的空目录"/var/lib/docker/volumn/containerID" 挂载到容器的"/var/log/nginx",也就是说宿主机的目录可以不填,默认为docker安装目录对应的containerID目录

  • docker run -v /develop/nginx/html:/usr/share/nginx/html --name myNginx nginx

    注意在之前的例子中,我们把空目录挂载到容器的空文件夹中(因为log是运行容器后才产生的)。但在这个例子中,我们把空目录挂载到容器的非空文件夹"/usr/share/nginx/html",由于挂载源是空文件夹,会覆盖掉挂载目标也就是"/usr/share/nginx/html"的内容,这样就导致nginx的html文件丢失,所以切记不要用一个空目录挂载到容器在运行前的非空目录下,如果我们要在挂载源控制nginx的html文件内容,那就必须要保证挂载源非空且存在html文件

  • docker run -v /develop/nginx/html/index.html:/usr/share/nginx/html/index.html:ro --name myNginx nginx

    将宿主机的"/develop/nginx/html/index.html"文件以只读的形式挂载到容器的"/usr/share/nginx/html/index.html"

六、 容器网络

6.1 访问容器

  • 对于linux下的docker,本机IP+端口就可以访问容器,比如127.0.0.1就可以访问nginx容器
  • 对于DockerToolBox和Docker_FrontEnd,要访问虚拟机的IP+端口,默认为192.168.99.100

但是此时输入URL还不可以访问容器,因为端口还没有映射

6.2 端口映射

  • docker run -p 80:80 --name myNginx nginx

    把容器的80端口映射到宿主的80端口,这样就可通过宿主机IP:80访问nginx容器

  • docker run -p 8080:80 --name myNginx nginx

    把容器的80端口映射到宿主的8080端口,这样就可通过宿主机IP:8080访问nginx容器

  • docker run -p 8080:80 -p 8081:81 --name myNginx nginx

    把容器的80端口映射到宿主的8080端口,同时把容器81端口映射到宿主的8081端口

  • docker run -p 8080-8085:80-85 --name myNginx nginx

    把容器的80、81、82、83、84、85端口映射到宿主的8080、8081、8082、8083、8084、8085端口

  • docker run -P --name myNginx nginx

    把容器的所有开放端口随机映射到宿主

6.3 容器间通讯

相同宿主机下有两个容器A、B

容器A内部端口80映射到宿主的8080

容器B访问容器A的方法如下:

  • 宿主IP:8080

    如 curl 192.168.99.100:8080

  • 容器A IP:80

    如 curl 172.17.0.2

    容器A的IP获取需要使用命令docker inspect A

    16

七、 Dockerfile

在2.1小结我们提到Image其实是一系列操作的层堆叠起来。但是传统的commit方式不能很好的呈现Image是如何构建的。试想一个场景,搭建一个软件运行环境的Image,这个Image会随着业务、时间不断更新,如果运维人员想看Image究竟是怎样堆叠起来的,只能用history指令,不利于查看为维护。所以这又引出了2.1小结提到的另一个概念Dockerfile

7.1 Dockerfile编写方式

下面的示例是将一个golang程序打包成Image

# FROM关键字 说明原始镜像
FROM golang:1.9.2-alpine3.6 

# RUN关键字用于执行命令
RUN apk add --no-cache git
RUN go get github.com/golang/dep/cmd/dep

# COPY Dockfile上下文目录的Gopkg.lock Gopkg.toml 到镜像的/go/src/project目录下,注意如果是目录,最后不要忘了/
COPY Gopkg.lock Gopkg.toml /go/src/project/
# WORKDIR 关键字相当于切换目录
WORKDIR /go/src/project/
RUN dep ensure -vendor-only

# COPY Dockfile上下文目录的所有文件到镜像的/go/src/project目录下
COPY . /go/src/project/
RUN go build -o /bin/project

# 暴露端口80,参考6.2小结,如果使用-P映射(大写P),用以告诉docker,容器的80端口是开放的,可以映射出去
EXPOSE 80
# 设置入口,相当于通过镜像启动容器后,进入"/bin/project",该配置可以被docker run --entrypoint xxx app 强行覆盖
ENTRYPOINT ["/bin/project"]
# 执行命令,该配置可以被docker run app xxx 强行覆盖
CMD ["--help"]

什么是Dockerfile的上下文?需要了解Dockerfile的构建方式

7.2 Dockerfile构建方式

  • docker build -t myRepo/myAPP:latest .

    注意最后的那个点".",查找当前目录名为Dockerfile的文件,构建Image并命名为myRepo/myAPP:latest

  • docker build -f myRepo/myAPP:latest -f myDockerfile

    查找当前目录名为myDockerfile的文件,构建Image并命名为myRepo/myAPP:latest

在上面两个例子中,Dockfile的上下文就是指Dockerfile文件所在的目录,先看下图:

17

左右两幅图目录结构不同,如果Dockerfile具有同样的内容:

FROM XXX
COPY . /target

那么左边的目录结构,最终被放到镜像"/target"目录中的文件应该只有"run.sh"这个文件,而右边的目录结构,最后在"/target"中的是"document"和"source_code"的文件夹及内容。

八、 Docker Compose

试想一个业务场景,我们有一个服务容器A需要连接数据库,把数据库装到A的镜像里不是一个明智的选择,一般情况下我们会再搭建一个容器B作为数据库,那么这时,两个容器如何关联?这里的关联不仅包括如何保证两个容器如何同时管理,还包括两个容器如何通讯。这就引出来容器编排的概念,我们需要对容器进行一系列的调整,以满足我们的业务需要。

Docker Compose 需要额外安装,详情见https://docs.docker.com/compose/install/

Docker Compose是用于管理多容器的Docker应用。看一个示例:

docker-compose.yaml

# 标明用docker compose的哪个版本,不同版本兼容的指令有区别
version: '3'
# 服务编排
services:
  # 服务名称
  nginx:
    # 镜像地址
    image: nginx
    # 容器名称,不填则为“服务名_序号”
    container_name: nginx
    # 设置volume
    volumes:
    - nginx.conf:/etc/nginx/nginx.conf:ro
    # 端口映射
    ports:
    - "8080:80"

  api:
    image: xxx.com/api:latest
    container_name: api

nginx.conf

	server{
		listen 80;
		server_name xxx.yyy.com;
		location / {
        	proxy_pass http://api;
   		}
	}

上面的服务编排就是对api容器做了一个nginx反向代理,可以看到使用docker-compose可以让容器间的通讯更加简单,直接用Container Name加Container内部端口即可。

使用docker-compose必须将文件命名成"docker-compose.yaml",使用方法如下:

首先进入"docker-compose.yaml"文件所在目录

  • docker-compose up -d

    根据编排生成所需的容器并启动,-d表示后台执行

  • docker-compose start

    启动编排的容器

  • docker-compose stop

    停止编排的容器

  • docker-compose restart

    重启编排的容器

  • docker-compose pull

    强制拉取编排所需的镜像,因为在上例中,api使用的是latest镜像,当又有新的lastest镜像被push到Registry时,如果本地又存在一个旧的api:latest,那么docker compose 不会自动更新镜像,需要强制用"docker-compose pull"强制拉取,再执行"docker-compose up"

  • docker-compose build

    docker compose的容器镜像还可以直接用Dockerfile

    version: '3'
    services:
      api:
        build: ./
        container_name: api

    表示编排容器时,需要先在本地用Dockerfile构建一个镜像,如果Dockerfile文件发生改变,而本地又有旧镜像,那么docker compose 同样不会自动更新镜像,需要执行"docker-compose build"强制构建,再执行"docker-compose up"

最后更新于