Docker 零基础入门

1,854 阅读19分钟

Docker 最初是 dotCloud 公司创始人 Solomon Hykes 在法国期间发起的一个公司内部项目,它是基于 dotCloud 公司多年云服务技术的一次革新,并于 2013 年 3 月以 Apache 2.0 授权协议开源。

Docker 是一个开源的应用容器引擎,使用 Go 语言 进行开发实现,它不同于与 KVM 和 Xen,docker 基于 Linux 内核的 cgroup,namespace,以及 AUFS 类的 Union FS 等技术,对进程进行封装隔离,属于 操作系统层面的虚拟化技术。

Docker 容器内的应用进程直接运行于宿主的内核,容器内没有自己的内核,而且也没有进行硬件虚拟。所以它非常的轻量。使用 docker 可以解决我们的软件开发中的依赖和开发环境统一等问题。

安装

docker 分为两个版本,docker-ce(社区版) 和 docker-ee(企业版),docker-ce 是免费的支持周期 7 个月,ee 需要付费,支持周期 24 个月。

安装 docker 可以直接去查看官网安装文档页面

国内从 Docker Hub 拉取镜像会很慢,我们可以更换镜像源,对于 windows 和 mac 可以直接去设置 daemon 中设置 registry mirrors。

linux 可以修改 /etc/docker/daemon.json 文件。

{
  "registry-mirrors": [
    "https://dockerhub.azk8s.cn",
    "https://reg-mirror.qiniu.com"
  ]
}

然后重启就可以了。

概念

docker 中有三个主要概念,镜像(Image),容器(Container)和仓库(repository)。

镜像

Docker 镜像是一个特殊的文件系统,除了提供容器运行时所需的程序、库、资源、配置等文件外,还包含了一些为运行时准备的一些配置参数(如匿名卷、环境变量、用户等)。镜像不包含任何动态数据,其内容在构建之后也不会被改变。

容器

镜像就好像是 而容器就是 实例

容器的实质是进程,但与直接在宿主执行的进程不同,容器进程运行于属于自己的独立的 命名空间。因此容器可以拥有自己的 root 文件系统、自己的网络配置、自己的进程空间,甚至自己的用户 ID 空间。容器内的进程是运行在一个隔离的环境里,使用起来,就好像是在一个独立于宿主的系统下操作一样。

比如一个运行 nginx 的容器

容器运行时,是以镜像为基础层,在其上创建一个当前容器的存储层,我们可以称这个为容器运行时读写而准备的存储层为 容器存储层。当容器消亡时,容器存储层也随之消亡,上面的存储的信息都会被删除。

如果想数据不随着容器退出而被删除,可以使用数据卷或绑定宿主目录,在这些位置的读写会跳过容器存储层,直接对宿主(或网络存储)发生读写,其性能和稳定性更高。容器消亡,数据卷不会消亡。

仓库

镜像构建完成后,就可以发布到远程仓库中去,它来集中的存储、分发镜像,这样就可以被他人下载使用了,Docker Registry 就是这样的服务

一个 Docker Registry 中可以包含多个 仓库(Repository)每个仓库可以包含多个 标签(Tag);每个标签对应一个镜像。

镜像的命名使用 命名空间(用户名)/仓库名:标签 的形式,比如 jwilder/nginx-proxy:latest。如果我们直接使用 jwilder/nginx-proxy 也是代表 jwilder/nginx-proxy:latest。因为不给出标签,将以 latest 作为默认标签。

官方镜像没有命名空间这一段,我们可以直接使用 centos 这样的镜像名。

最常使用的 Registry 公开服务是官方的 Docker Hub,这也是默认的 Registry。我们可以去上面查看镜像说明,评论等信息。

原理

我们安装的 Docker 分为两个部分 Docker Client 和 Docker Server。我们通过 Client 发送命令到 Server,由 Server 来创建镜像,运行容器等工作。

docker 是基于 Linux 内核的 cgroup,namespace。

  • Linux Namespace 是 Linux提供的一种内核级别环境隔离的方法,它用来隔离一个进程或一组进程的资源,如 硬盘,网络等。
  • cgroup(control groups)限制每个进程的资源使用如 cpu,内存等。

由于 windows 和 mac 上面没有上述的技术,所以 windows 和 mac 上的 docker 其实是运行在一个 linux 虚拟机中的。

分层存储

因为镜像包含操作系统完整的 root 文件系统,其体积往往是庞大的,因此在 Docker 设计时,就充分利用 Union FS 的技术,将其设计为分层存储的架构。一个镜像其实是由多层文件系统联合组成。

镜像构建时,会一层层构建,前一层是后一层的基础。每一层构建完就不会再发生改变,后一层上的任何改变只发生在自己这一层。

删除前一层文件的操作,实际不是真的删除前一层的文件,而是仅在当前层标记为该文件已删除。在最终容器运行的时候,虽然不会看到这个文件,但是实际上该文件会一直跟随镜像。

镜像

当我们要在本机运行一个容器时,比如使用 nginx 镜像启一个容器,我们可以执行docker run nginx 命令,docker 会首先查看本地镜像缓存中是否存在该镜像,如果没有就从远程仓库下载 nginx:latest 镜像下来,保存到本地的镜像缓存中,然后通过该镜像启动运行一个容器。

镜像就像是一个特殊文件系统,它想一个文件系统快照,当启动运行一个容器时, docker 首先会在宿主机的硬盘上划分一片区域,它只能被该容器访问, 然后镜像上的文件快照放入这一片区域中。然后运行镜像的启动命令。

使用

安装好 docker 后,我们可以使用

docker version

查看版本信息。

docker info

查看全系统的信息,比如配置和当前状态信息。

docker --help

查看帮助信息。

docker 有两种命令形式。

docker <command> (options) # 老命令形式
docker <command> <sub-command> (options) # 新命令形式

比如 docker ps 等同于 docker container ls

镜像

docker search 关键字
# 可以通过关键字搜索镜像。
docker pull 镜像
# 可以把远程的镜像拉取到本地。
docker images
# 可以列出本地的镜像,列表中的镜像体积总和并非是所有镜像实际硬盘消耗。
# 因为 Docker 镜像是多层存储结构,并且可以继承、复用。
docker system df
# 可以查看镜像,容器和数据卷占用内存信息

虚悬镜像(dangling image)

镜像列表中有可能看见一个特殊的镜像,仓库名和标签,均为 <none>。这是因为由于新旧镜像同名,旧镜像名称被取消,从而出现仓库名、标签均为 <none> 的镜像。

中间层镜像

docker images -a

可以显示包括中间层镜像在内的所有镜像,多个顶层镜像可能依赖同一个中间层镜像,所以中间层镜像不可以随便删除,如果一个中间层镜像没有被依赖,那么它就会被自动删除。

删除镜像

docker rmi 镜像

如果有基于这个镜像启动的容器,会导致删除失败,需要使用 -f 参数强制删除。

删除镜像分为 UntaggedDeleted 两类。

一个镜像可以有多个标签,我们删除一个镜像实际上是取消标签,当该镜像所有的标签都被取消了,那么就会触发 Deleted 行为。

容器

容器是独立运行的一个或一组应用,以及它们的运行态环境。

docker create 镜像
# 通过镜像创建一个容器。
# `--name 名称` 可以给创建的容器一个名称,这样就不会是随机名称
docker start 容器 [...]
# 运行一个或多个容器。
docker run 镜像 [COMMAND]

# 通过镜像创建并启动一个容器,如果镜像不存在,会自动去远端仓库拉去镜像,
# 它就相当于`docker pull` `docker create` `docker start` 这三个命令。

常见的参数

  • --name 名称 设置容器名称
  • -d 在背景以守护进程运行
  • --rm 当容器推出时自动删除
  • -p 机器端口:容器端口 机器端口映射到容器端口
  • -e 变量名=变量值 设置环境变量
  • -i 保持开启 STDIN,让容器可以接受到我们键盘发出的命令
  • -t 分配一个伪终端
docker run --name webserver --rm -d -p 80:80 nginx
# 启动 nginx 服务器,这时候去浏览器输入 127.0.0.1 可以看见默认的 nginx 页面
docker run -it nginx sh
# nginx 容器启动命令为 sh,而不是默认命令,它重写了默认执行的命令。
docker run -it --name hi busybox echo hi
# 它会打印 hi 然后结束
# 我们可以使用 docker start hi 来重新运行这个容器,它还会打印 hi
# 然而 docker start 命令不可以重写容器的启动命令
docker ps
# 打印出容器列表
# `-a` 显示所有容器列表,即使不在运行
docker stats
# 显示容器实时的资源使用情况
# `-a` 参数 让输出显示到屏幕上,而不是只是返回一个 id
docker top 容器
# 显示容器运行进程
docker logs 容器
# 容器守护态运行时,没有输出信息,可以通过这个命令打印容器的输出信息。
# `-f` 参数为不退出,一直等待最新的日志输出。
docker stop 容器 [...]
# 停止一个或多个容器
# 它发送一个 SIGTERM 信号个容器,容器中的那个进程收到信号后
# 可能会执行一些其他操作才停止,所以有时需要等一会才停止容器
docker kill 容器
# 发送一个 SIGKILL 信号给容器
# 它的意思是让容器中的进程马上关闭,不要做一些其他的事
# 如果我们使用 stop 命令,容器 10 秒中没有响应,docker 会自动执行 kill 命令
docker restart 容器 [...]
# 重启一个或多个容器
docker exec [OPTIONS] CONTAINER COMMAND [ARG...] 
# 在运行的容器中,执行一个命令

如果我们进入一个在运行的容器,比如上面 webserver 容器。我们可以执行。

docker exec -it webserver sh
docker rm 容器
# 删除容器
# 如果是在运行的容器可以加 `-f` 参数,发送 SIGKILL 信号给容器
docker container prune
# 清理所有处于终止状态的容器
docker system prune
# 当我们 docker 使用久了,可以使用这个命令来清理一下空间

制作镜像

制作镜像的一种简单的方法是直接通过一个容器生成自己镜像。

我们首先可以启动一个容器

docker run --name webserver -d -p 80:80 nginx

然后进入容器中,然后更改容器中 nginx 默认的 index 页面

docker exec -it webserver bash
/# echo '<h1>Hello, Docker!</h1>' > /usr/share/nginx/html/index.html
/# exit

退出后我们用 diff 命令,查看变动过的文件

$ docker diff webserver         
C /root                            
A /root/.bash_history              
C /usr                             
C /usr/share                       
C /usr/share/nginx                 
C /usr/share/nginx/html            
C /usr/share/nginx/html/index.html 
C /run                             
A /run/nginx.pid                   
C /var                             
C /var/cache                       
C /var/cache/nginx                 
A /var/cache/nginx/fastcgi_temp    
A /var/cache/nginx/proxy_temp      
A /var/cache/nginx/scgi_temp       
A /var/cache/nginx/uwsgi_temp      
A /var/cache/nginx/client_temp     

最后我们使用 commit 命令制作镜像

docker commit [OPTIONS] CONTAINER [REPOSITORY[:TAG]]

# `-a` 作者信息
# `-m` 提交信息
docker commit \
    -a "John Hannibal Smith <hannibal@a-team.com>" \
    -m "修改了默认网页" \
    webserver \
    nginx:v2

执行 docker images 可以查看到我们刚才制作的镜像。

docker history nginx:v2
# 可以查看镜像内的历史记录

我们的制作的新镜像,就相当于把容器的存储层保存起来,放在原来镜像的基础之上。

使用 commit 制作镜像,对镜像的操作都是黑箱操作,我们不知道具体到底更改了什么,而且还容易把很多无用的东西也打包进来,这会让镜像非常臃肿。

Dockerfile

为了解决上述问题,我们可以通过 Dockerfile 来构建镜像。Dockerfile 把构建镜像的把每一层修改、安装、构建、操作的命令都写在一个文件中,它让镜像构建非常的透明。

我们编写 Dockerfile 给 docker client,docker client 给 docker server,由 docker server 来帮我们制作镜像。

我们新建一个文件夹在里面创建名称为 Dockerfile 的文件,在里面写上如下内容。

FROM nginx

RUN echo '<h1>Hello, Docker!</h1>' > /usr/share/nginx/html/index.html
RUN echo '111' > /111
ENV ENV1=env1 ENV2=env2
RUN echo '222' > /222
RUN echo $ENV1 > env1

Dockerfile 是一个文本文件,其内包含了一条条的 指令,每一条指令构建一层,因此每一条指令的内容,就是描述该层应当如何构建。

FROM

DockerfileFROM 是必备的指令,并且必须是第一条指令!

它引入一个镜像作为我们要构建镜像的基础层,就好像我们首先要安装好操作系统,才可以在操作系统上面安装软件一样。

docker 存在一个特殊的镜像,名为 scratch。这个镜像是虚拟的概念,并不实际存在,它表示一个空白的镜像,如果你以 scratch 为基础镜像的话,意味着你不以任何镜像为基础,接下来所写的指令将作为镜像第一层开始存在。

RUN

RUN 指令是用来执行命令行命令的。每一个 RUN 指令都会新建立一层,在其上执行这些命令,我们频繁使用 RUN 指令会创建大量镜像层,然而 Union FS 是有最大层数限制的,不能超过 127 层,而且我们应该把每一层中我用文件清除,比如一些没用的依赖,来防止镜像臃肿。

我们可以这样使用 RUN 指令

FROM debian:stretch

RUN 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/* \
    && rm redis.tar.gz \
    && rm -r /usr/src/redis \
    && apt-get purge -y --auto-remove $buildDeps

ENV

ENV 指令用来设置环境变量,它有两种形式:

  • ENV <key> <value>
  • ENV <key1>=<value1> <key2>=<value2>...

定义了环境变量,那么在后续的指令中,就可以使用这个环境变量。

CMD

CMD 指令用来在启动容器的时候,指定默认的容器主进程的启动命令和参数。

它有两种形式

  • CMD echo 1
  • CMD ["npm", "run", "test"] 必须是双引号

第一种执行的命令会被包装程,CMD [ "sh", "-c", "echo 1" ] JSON 数组形式,一般推荐 JSON 数组形式。

容器中的应用都应该以前台执行,而不是启动后台服务,容器内没有后台服务的概念。

对于容器而言,其启动程序就是容器应用进程,容器就是为了主进程而存在的,主进程退出,容器就失去了存在的意义。

比如 CMD service nginx start 它等同于 CMD [ "sh", "-c", "service nginx start"] 主进程实际上是 shsh 也就结束了,sh 作为主进程退出了。

build 镜像

保存好文件后,我们在当前文件夹下面执行

docker build -t nginx:v3 .

build 命令用来制作镜像,-t 是给镜像打标签,-f 参数是指定 Dockerfile 路径,由于我们使用的是默认 Dockerfile 名称,所以可以不同填写该参数。

最后的 . 代表是当前路径,它指定镜像构建的上下文。我们刚才说过,真正制作镜像的是 docker server,当我们执行 build 命令时,docker client 会将上下文路径下的所有内容打包,然后上传给 docker server。这样当我们要在 Dockerfile 文件中执行 如 COPY 指令,就可以将上下文中的文件复制到镜像中去了。

一般应该会将 Dockerfile 置于一个空目录下,或者项目根目录下。如果该目录下没有所需文件,那么应该把所需文件复制一份过来。如果目录下有些东西确实不希望构建时传给 Docker 引擎,那么可以用 .gitignore 一样的语法写一个 .dockerignore

当我们执行完命令后控制台会输出

Sending build context to Docker daemon  2.048kB
Step 1/6 : FROM nginx
 ---> 719cd2e3ed04
Step 2/6 : RUN echo '<h1>Hello, Docker!</h1>' > /usr/share/nginx/html/index.html
 ---> Running in 9ddbf675e4ea
Removing intermediate container 9ddbf675e4ea
 ---> c03b5364b185
Step 3/6 : RUN echo '111' > /111
 ---> Running in b0668bd11f96
Removing intermediate container b0668bd11f96
 ---> 42155bdf8812
Step 4/6 : ENV ENV1=env1 ENV2=env2
 ---> Running in 3ec6c29227e7
Removing intermediate container 3ec6c29227e7
 ---> 8b6243cb0c19
Step 5/6 : RUN echo '222' > /222
 ---> Running in dce84eae5105
Removing intermediate container dce84eae5105
 ---> 0a428b843fed
Step 6/6 : RUN echo $ENV1 > env1
 ---> Running in 0716ba5f87c1
Removing intermediate container 0716ba5f87c1
 ---> 21d774552f8c
Successfully built 21d774552f8c
Successfully tagged nginx:v3

我们发现每个指令都会有类似 Running in 3ec6c29227e7 这样的输出。

这是当构建镜像时,docker 会启动一个临时的容器在上面执行指令,执行完毕后,我们看到 Removing intermediate container 3ec6c29227e7 代表把这个临时容器删除,然后执行下一个执行的时候把当前构建好的这一层作为它的基础层。

就像 docker commit 命令一样,保存它容器的存储层,作为新的镜像层。

当我们在执行一次 docker build -t nginx:v3 .

Sending build context to Docker daemon  2.048kB
Step 1/6 : FROM nginx
 ---> 719cd2e3ed04
Step 2/6 : RUN echo '<h1>Hello, Docker!</h1>' > /usr/share/nginx/html/index.html
 ---> Using cache
 ---> c03b5364b185
Step 3/6 : RUN echo '111' > /111
 ---> Using cache
 ---> 42155bdf8812
Step 4/6 : ENV ENV1=env1 ENV2=env2
 ---> Using cache
 ---> 8b6243cb0c19
Step 5/6 : RUN echo '222' > /222
 ---> Using cache
 ---> 0a428b843fed
Step 6/6 : RUN echo $ENV1 > env1
 ---> Using cache
 ---> 21d774552f8c
Successfully built 21d774552f8c
Successfully tagged nginx:v3

会发现输出 Using cache,这是 docker 知道我们的 Dockerfile 没有变化,直接使用我们上次构建的缓存层,这样就不同重新构建了。

如果我们修改一下 Dockerfile 文件

FROM nginx

RUN echo '<h1>Hello, Docker!</h1>' > /usr/share/nginx/html/index.html
RUN echo '111' > /111
ENV ENV1=env1 ENV2=env2
RUN echo '!!!'
RUN echo '222' > /222
RUN echo $ENV1 > env1

在中间新增了一条 RUN echo '!!!'。然后在构建一次。看它的输出。

Sending build context to Docker daemon  2.048kB
Step 1/7 : FROM nginx
 ---> 719cd2e3ed04
Step 2/7 : RUN echo '<h1>Hello, Docker!</h1>' > /usr/share/nginx/html/index.html
 ---> Using cache
 ---> c03b5364b185
Step 3/7 : RUN echo '111' > /111
 ---> Using cache
 ---> 42155bdf8812
Step 4/7 : ENV ENV1=env1 ENV2=env2
 ---> Using cache
 ---> 8b6243cb0c19
Step 5/7 : RUN echo '!!!'
 ---> Running in 04ec83f30f71
!!!
Removing intermediate container 04ec83f30f71
 ---> 6ee208c0cd94
Step 6/7 : RUN echo '222' > /222
 ---> Running in 0286f9787e42
Removing intermediate container 0286f9787e42
 ---> 64fd449dd753
Step 7/7 : RUN echo $ENV1 > env1
 ---> Running in 996734b219e5
Removing intermediate container 996734b219e5
 ---> 322db42707c6
Successfully built 322db42707c6
Successfully tagged nginx:v3

我们发现 RUN echo '!!!' 之后步骤都没有用缓存了,全部都重新构建了。

docker run -it nginx:v3 bash

我们执行这个命令去刚构建好的镜像中去看看。

/# ls
111  222  bin  boot  dev  env1  etc  home  lib  lib64  media  mnt  opt  proc  root  run  sbin  srv  sys  tmp  usr  var
/# echo $ENV1
env1

docker build 除了通过 Dockerfile 构建镜像,还可以通过 URL 构建,比如 git 仓库

docker build https://gitrepo.com/git.git#:app

这行命令指定了构建所需的 Git repo,并且指定默认的 master 分支,构建目录为 /app/

多阶段构建

多阶段构建允许我们在一个 Dockerfile 中编写多个镜像构建流程,并且后面的镜像可以复制前面文件。

比如我们现在有一个前端项目,我们要打包的时候需要用到 node 镜像帮我们打包生成最终的 html js css 文件,然后把最终把这几个文件复制到 nginx 镜像中,下次启动我们自定义的这个 nginx 镜像就可以看到我们的网站了。

这时候我们就需要用到两个 Dockerfile,一个用来打包,一个用来复制到 nginx 中。这样让部署过程很复杂。

这时候我们就可以使用多阶段构建。

我们需要安装好 nodejs 环境,然后在命令行执行

npm init react-app myapp

然后去 myapp 文件夹,执行 npm start 就可以看到默认页面了。

然后我们再 myapp 文件夹下编写 .dockerignore 文件

.idea/
.vscode/
.git/
build/
node_modules/
npm-debug.log*

然后就是我们的 Dockerfile

FROM node:alpine AS builder

WORKDIR /app

COPY ./package.json .
RUN npm install --registry=https://registry.npm.taobao.org
COPY . .
RUN npm run build

FROM nginx AS prod

COPY --from=builder /app/build /usr/share/nginx/html/

我们首先引入 node:alpinealpine 是一个非常小的 linux 镜像它只有 5MB 大小,我们选择的 node 镜像的 alpine 版本,alpine 版本的镜像代表镜像非常的小,没有安装一些非必须的软件等。

AS builder 默认每个阶段是没有名称的,我们可以使用 AS 给它一个名字,要引用它们可以直接使用它的名称,如果不指定的话,可以使用从 0 开始的数字引用它们。

比如 COPY --from=builder /app/build /usr/share/nginx/html/ 可以写成 COPY --from=0 /app/build /usr/share/nginx/html/

--from 不只可以是我们构建中各个阶段,也可以是任意镜像。

COPY --from=nginx:latest /etc/nginx/nginx.conf /nginx.conf

WORKDIR

WORKDIR 用来指定工作目录,如果目录不存在,WORKDIR 会帮你建立目录。

我们不想把我们前端的文件直接放在根目录中,而是想放在根目录下的 app 文件夹,我们就使用 WORKDIR 指令,以后我们使用 ./ 就是代表 /app 目录了。

COPY

COPY 可以将宿主机的文件复制到镜像中。

COPY 源路径... 目标路径

源路径可以是多个,也可以用通配符,文件匹配可以查看 filepath.Match 规则

COPY hom* /mydir/
COPY hom?.txt /mydir/

我们先把 package.json 文件复制到临时容器,在安装依赖,然后才把项目文件复制到镜像打包。

是因为这样要是我们的项目文件变动了而项目依赖没有变化,我们就可以使用缓存而不需要重新安装依赖。

build 镜像

最后我们运行 build 命令,构建镜像

docker build -t myapp:v1 .

如果我们只想构建 builder 阶段,可以使用 target 参数

docker build --target builder -t imagename:tag .

构建完成我们可以查看一下我们的镜像

$ docker images
REPOSITORY                 TAG                 IMAGE ID                 SIZE
myapp                      v1                  3d4af1e1a6bd             110MB
nginx                      latest              719cd2e3ed04             109MB

我们发现我们的构建镜像只比 nginx1MB,非常的轻量。

最后我们运行一下这个镜像

docker run -d --rm --name myapp -p 80:80 myapp:v1

打开浏览器 127.0.0.1 就可以看到效果。

数据管理

我们可以把主机目录作为数据卷挂载到容器中去,容器中访问挂在的文件时会被映射到我们的主机目录。

比如上面的前端项目,我们可以新建一个开发时的镜像叫 Dockerfile.dev

FROM node:alpine

WORKDIR /app

COPY package.json .
RUN npm install --registry=https://registry.npm.taobao.org
COPY . .

EXPOSE 3000

CMD ["npm", "run", "start"]

EXPOSE

EXPOSE <端口1> [<端口2>...]

申明要暴露的端口,这只是一个声明,在运行时并不会因为这个声明应用就会开启这个端口的服务。

build 镜像

docker build -t myapp:dev -f Dockerfile.dev .

构建好镜像后我们可以运行一个容器

docker run --rm --name myappdev -p 8000:3000 myapp:dev

然后我们访问本地的 8000 端口就可看到页面了。

这时候我们编辑我们项目文件夹的文件,然后刷新浏览器,会发现没有效果。因为浏览器中运行的页面是我们容器中的项目代码而不是我们本机的项目。

挂载主机目录

这时候我们需要将容器的项目目录映射到我们本机的项目目录,这样我们修改主机中的项目文件,就会反应到容器中。

echo 'CHOKIDAR_USEPOLLING=true' > .env
# 首先我们需要新建 `.env` 文件并在其中写入 `CHOKIDAR_USEPOLLING=true`
# 因为 watcher 在虚拟机内部需要使用轮询模式
docker run --rm --name myappdev -v /app/node_modules -v $(pwd):/app -p 8000:3000 myapp:dev

在 windows 系统中不能使用 $(pwd),需要把它替换成项目的绝对路径。

-v 的意思是将本机目录映射到容器中,

-v 本机目录(或文件):容器目录(或文件)
# 本地目录的路径必须是绝对路径
等同于 --mount type=bind,source=本机,target=容器

当我们加入 -v 后面只有一个文件夹或文件时,代表容器中这个文件夹映射到一个匿名数据卷中。

上面的 -v /app/node_modules -v $(pwd):/app 代表映射当前目录到容器的 /app 目录,然而 node_modules 文件夹映射到一个匿名数据卷中,这样就会让 node_modules 这个文件夹不会被映射到我们项目的文件夹。

默认挂在的是可读可写的,我们可以使用 ro 代表只可读。

-v /src/webapp:/opt/webapp:ro
# 等同于 --mount type=bind,source=/src/webapp,target=/opt/webapp,readonly

最后我们修改项目中 src/App.js 浏览器就会自动加载我们修改后的数据了。

数据卷

数据卷 是一个可供一个或多个容器使用的特殊目录,它绕过 union file system,可以提供很多有用的特性。

  • 数据卷 可以在容器之间共享和重用
  • 对 数据卷 的修改会立马生效
  • 对 数据卷 的更新,不会影响镜像
  • 数据卷 默认会一直存在,即使容器被删除
docker volume create vol1
# 创建一个数据卷
docker volume ls
# 查看数据卷列表
docker volume inspect vol1
# 查看数据卷详情
docker run -v vol1:/wepapp imagename:tag
# 挂载到容器里
# 等同于 --mount source=my-vol,target=/webapp
docker volume rm vol1
# 删除数据卷
docker volume prune
# 清理未使用的数据卷

VOLUME

Dockerfile 中有一个 VOLUME 指令,用来给容器中一个或多个文件夹挂在到,匿名数据卷中。

VOLUME ["<路径1>", "<路径2>"...]
VOLUME <路径>

这样运行时如果用户不指定挂载,其应用也可以正常运行,不会向容器存储层写入大量数据。保证了容器存储层的无状态化。

当然我们可以在运行时覆盖这个挂载设置。

docker run -v vol1:<路径>

网络管理

当容以运行一些网络应用,要让外部也可以访问这些应用,我们通过 -P-p 参数来指定端口映射。

-P 标记时,Docker 会随机映射一个 49000~49900 的端口到内部容器开放的网络端口。

docker run -d -p 127.0.0.1:80:80 nginx
# 映射到指定地址的指定端口
docker run -d -p 127.0.0.1::80 nginx
# 映射到指定地址的任意端口,本地主机会自动分配一个端口
docker run -d -p 80:80/udp nginx
# 还可以使用 udp 标记来指定 udp 端口
docker run -d -p 80:80 -p 81:81 nginx
# 映射多个端口
docker port 容器
# 查看容器映射端口配置

容器互联

当我们有多个容器时需要它们之间互相连接,比如有 web redismongodb 三个容器,web 服务器容器需要连接到 redismongodb 两个数据库。这时候可以使用 docker network 命令。

$ docker network ls # 查看网络列表
NETWORK ID          NAME                DRIVER              SCOPE
f0d07aa56d2c        bridge              bridge              local
ffff2c79740a        host                host                local
ecad6ae9e643        none                null                local

当 Docker 启动时,会自动在主机上创建一个 docker0 虚拟网桥,实际上是 Linux 的一个 bridge,可以理解为一个软件交换机。它会在挂载到它的网口之间进行转发。

我们看见默认有 3 个网络,名称为 bridge 是我们启动一个容器时会自动连接到的 docker 虚拟网络,它就是上面那个 docker0

host 代表直接跳过 docker 的虚拟网络,直接使用主机的接口,这样它少了安全保护,但是可以提高吞吐量,一般很少使用得到。

none 代表没有使用主机的任何网络,只使用 localhost

$ docker run -d --rm --name web1 nginx
# 启动一个容器
$ docker network inspect bridge
# docker network inspect 可以查看网络详情
...
"Containers": {
    "cb3cf771c859303492fb29fab10e77bf401e129a5ffa7fc9ab062a941a383b58": {
        "Name": "web1",
        "EndpointID": "c44757274959d248bba6ad42cd83eef2457ed9de8bb0c861514312b39881fb7b",
        "MacAddress": "02:42:ac:11:00:02",
        "IPv4Address": "172.17.0.2/16",
        "IPv6Address": ""
    }
}
...

我们看见我们刚才启动的容器加入到可这个默认的虚拟网络中。

docker run --rm alpine ping 172.17.0.2
# 我们可以尝试 ping 一下 web1
PING 172.17.0.2 (172.17.0.2): 56 data bytes
64 bytes from 172.17.0.2: seq=0 ttl=64 time=0.083 ms
64 bytes from 172.17.0.2: seq=1 ttl=64 time=0.064 ms
64 bytes from 172.17.0.2: seq=2 ttl=64 time=0.064 ms

除了使用默认网络,我们还可以自己创建虚拟网络。

docker network create [OPTIONS] 网络
# `-d` 指定驱动类型,默认 bridge

然后我们运行的时候可以使用 --network 参数指定一个或多个网络,

docker run --network 网络名 image

我们可以将不同的容器放在不同的网络中,同一个网络的容器可以互相沟通,这样我们就无需暴露我们主机的端口,容器在虚拟网络中沟通。

我们还可以将一个容器加入一个网络或者从一个网络中退出。

$ docker network connect net1 containerName
# 连接到 net1
$ docker network disconnect net1 containerName
# 断开 net1 连接

DNS

我们自己创建的网络会比默认的网络多一个功能,那就是 DNS 解析功能。在上面我们在默认 bridge 网络中容器 ping web1 需要使用 ip 地址。然而在我们自己创建的网络中只需要使用容器名就可以。

$ docker network create net1 # 创建一个网络
$ docker run -d --network net1 --rm --name server nginx
$ docker run -it --rm --network alpine ping server
PING server (172.18.0.2): 56 data bytes
64 bytes from 172.18.0.2: seq=0 ttl=64 time=0.160 ms
64 bytes from 172.18.0.2: seq=1 ttl=64 time=0.065 ms
64 bytes from 172.18.0.2: seq=2 ttl=64 time=0.066 ms

我们发现我们直接使用 server 这个容器名就可以连到我们的 nginx 服务器。

当我们的容器多的时候,这非常的方便,也不用担心容器 ip 会发生改变。

负载均衡

我们除了使用容器名连接,还可以使用 --network-alias 指定别名连接,并且多个容器可以使用同一个别名。这样当连接到这个别名的时候,会随机解析到一个其中一个容器,从而实现一个非常简单负载均衡。

$ docker network create net2
$ docker run -d --network net2 --network-alias search elasticsearch:2
$ docker run -d --network net2 --network-alias search elasticsearch:2
# 创建两个 elasticsearch 容器,它们使用同一个别名
$ docker run --rm --net net2 alpine nslookup search
# 看一下它们的 ip
Name:      search
Address 1: 172.19.0.3 search.net2
Address 2: 172.19.0.2 search.net2
$ docker run --rm --net net2 centos curl -s search:9200
# 多执行几次这个命令,会发现返回 name 会有不同。

发布

为了让他人也可以使用到我们的镜像,我们可以将将镜像发布到 Docker Hub 上。

首先我们需要登录到 docker hub

$ docker login -u username -p password
# 登录到一个 Docker registry
$ docker push username/imageName:Tag
# 将镜像推送到 Docker registry 上
$ docker pull username/imageName:Tag
# 然后就可以使用 pull 命令将镜像拉去到本地

在国内我们可以使用更快速的 阿里云镜像服务

$ docker login -u username registry.cn-shanghai.aliyuncs.com
# 登录到阿里云
$ docker tag myapp:v1 registry.cn-shanghai.aliyuncs.com/wopen/myapp:v1
# 给我们的镜像打再打个标签
$ docker push registry.cn-shanghai.aliyuncs.com/wopen/myapp:v1
# 推送到阿里云镜像服务。
$ docker pull registry.cn-shanghai.aliyuncs.com/wopen/myapp:v1
# 拉取到本地
$ docker logout registry.cn-shanghai.aliyuncs.com
# 从阿里云退出登录

更多

更多更详细的 docker 知识可以直接去查看 docker 文档

我们每次启动一个容器都要一堆参数,当要一次启动多个容器时更加麻烦,这时候就可以使用 Docker Compose

Docker Compose 零基础入门

Docker Swarm 零基础入门

Kubernetes 零基础入门