Docker: Getting Started

又到了不得不迁移服务器的时候, 为了一键批量部署 4 个服务, 我花了两小时以 4 倍速看完了 docker 基础, 当然实践的过程远远超过学习时所花的时间

Why Docker?

  • 开发团队与认为, 团队之间经常互相扯皮, 主要的原因就是因为环境和配置有一定的不同
    • 比如公钥秘钥, 各类环境变量
    • 对于多个集群, 运维需要重复安装很多个环境, 最麻烦的是有一些项目会用到不同版本的环境
  • Docker 可以将一部分 代码/配置/系统/环境变量/数据 等等一系列东西全部包含进去.
    • 这个地方的 Docker Image 甚至可以把软件也一起安装了
    • 从此以后提交给运维的就是一个完全打包好的镜像
  • 对比以前的虚拟技术
    • 传统虚拟机 (e.g. Virtual Box)
      • 虚拟一套硬件
        • 其实在宿主机里面还模拟了虚拟机的内核和底层
      • 启动慢, 占用高, 步骤多
    • Docker
      • 一次构建随处运行
      • 用的是 Linux 容器虚拟化
      • 更少的抽象层: 容器内没有自己的内核, 直接使用宿主机的内核
      • 容器之间相互隔离

Concept

dockerfile + 源码 是原材料, image 是交付品, container 是运行示例

  • Docker
    • 是运行的载体以及管理引擎, 所有的操作都通过 Docker Daemon 处理
  • Image
    • 实际上就是一个 Template, 一般来说是只读的模板
    • 打包好的环境一般就是一个镜像文件
    • 通过这个镜像生成 docker 容器
  • Container
    • Image 实例化后的一个 Instance
    • 单个 Container 可以单独启动/关闭/停止/删除
    • 可以把容器看成一个简易版的 Linux 环境 + 运行在其中的应用程序
  • Repository
    • 实际上就是 Image 的内容
    • DockerHub 是最大的公开仓库
    • 国内可以使用阿里云和网易云的镜像

Installation

Install Docker Engine on Ubuntu | Docker Documentation

Ubuntu

基于 EC2, Ubuntu 20.04

sudo apt install docker.io

Cheatsheet

# 手动进入容器
sudo docker run -it [IMAGE] /bin/bash
# 一些情况下 bash 不够用, 改为 sh
sudo docker run -it [IMAGE] /bin/sh

Usage

docker --help 检查所有命令

docker run

docker run [OPTIONS] IMAGE [COMMAND] [ARG……]

docker run hello-world

# -i 交互模式运行, -t 分配一个伪输入终端, 一般一起使用
docker run -it centos
docker run -it centos --name mycentos
# 启动终端并执行自定义命令
docker run -it centos npm run dev

# 后台运行, 不弹出交互窗口也不切换
# 后台运行必须有一个持续的进程, 不然就会自动退出
docker run -d

# 打开一个新终端, 运行 tomcat
# -p 将 8080 映射到 8080
# [外部 DOCKER PORT]:[内部 IMAGE PORT]
docker run -it -p 8080:8080 tomcat

# -P 随机分配端口, 启动后可能直接看不到端口, 需要 docker ps 来查看
docker run -P tomcat
docker ps

# 携带环境变量
docker run --env VAR1=value1 --env VAR2=value2 ubuntu

默认会下载 latest 版本, 可以带上特定的 tag: docker run hello-world: latest

  1. 则检查本地是否存在这个 hello-world image, 如果有, 实例化产生 container 并运行
  2. 如果还没有, 就会去默认 registry 拉取下来, 如果可以找到, 那么拉下来, 实例化产生 container 并运行

docker image

# 列出本地所有镜像
docker images

# 列出本地所有镜像 包括中间层
docker images -a

# 列出本地所有镜像 ID
docker images -aq

# 到 dockerhub 搜索镜像关键字
docker search tomcat

docker rm/rmi

# 删除一个本地的镜像, 但是如果有容器正在使用, 将不会删除
docker rmi hello-world

# 删除多个镜像
docker rmi a:latest b:latest c d e f
docker rmi -f ${docker ps -a -q}

# 删除容器, 注意不带 i 就是删除容器, 带了 i 就是删除镜像
docker rm

# 删除多个容器
docker rm -f ${docker ps -a -q}
docker ps -a -q | xargs docker rm

docker pull

# 下载一个 image
docker pull centos

# 检查已经下载的 images
docker images

docker ps

# 列出当前 docker 所有正在运行的 container, 注意不是 image
docker ps

# 列初当前正在运行的, 以及以前历史运行过的. 这个地方可以通过状态来识别是否已经在运行.
docker ps -a

# 列出上一次运行的 container
docker ps -l

# 列出最近 50 个 container
docker ps -n 50

exit

# 退出容器, 终止容器并退出.
exit

# 不中止容器并退出
(Ctrl+P+Q)

docker start/restart/stop/kill

docker start [CONTAINER/NAME]
docker restart [CONTAINER/NAME]

# 自然关闭
docker stop [CONTAINER/NAME]

# 强制停止
docker kill [CONTAINER/NAME]

docker logs


# 查看日志, -t 包含时间戳, -f 跟随显示, --tail 默认尾部全部
docker logs -f -t --tail [CONTAINER]

docker logs -f -t --tail 3 [CONTAINER]

docker top

# 查看容器内运行的进程
docker top [CONTAINER]

docker inspect

# 查看容器内运行的细节
docker inspect[CONTAINER]

docker exec/attach

# 进入容器
docker attach [CONTAINER]

# 不进入容器就在容器中执行 ls -l /tmp, 并将结果返回宿主机
docker exec [CONTAINER] ls -l /tmp

docker cp

# 将容器内的 /tmp/test.log 拷贝到宿主机的 /root 文件夹
docker cp [CONTAINER]: /tmp/test.log /root

docker build

# 注意结尾有一个点符号

docker build -t [IMAGE_NAME]:TAG .

# -f [DOCKER_FILE] 如果不添加的话就会自动寻找文件名为 DockerFile 的文件
docker build -f [DOCKER_FILE] -t [IMAGE_NAME]:TAG .

* Other command

# 提交 container 副本令其成为一个新的 image
docker commit

# 列出镜像历史
docker history

Docker Image Architecture

镜像是一个 UnionFS (联合文件系统): 实际上是一种分层的, 高性能的, 轻量级的文件系统, 它支持对文件系统的修改作为一次提交来一层层叠加

一个 Image 可能引用了多个其他的镜像, 并且引用的镜像可以被多个 Image 引用 (类似 npm -g 的 模式)

  • 最底层是 bootfs (boot file system): 主要包含 bootloader 和 kernel
  • 上一层是 rootfs (root file system): 在 bootfs 之上, 包含的就是典型 Linux 系统中的 /dev, /proc, bin, /etc 等标准目录和文件, rootfs 就是不同操作系统的发行版
    • rootfs 相比其他系统的发布包来说会小很多, 因为只需要包含最基本的命令工具以及程序库就可以. 毕竟底层直接使用 host 的内核.

Data Volume Containers

一般来说, 容器里面产生的内容和数据在容器关闭之后会直接消失. 然后就需要将一些数据保存出来做持久化.

可以使用的方式:

  1. 直接命令添加
  2. dockerfile

docker run -v

# 如果对应的 path 没有会自动生成, 可以让 container 内部的一个 path 和外部 host 的一个 path 建立 binding
docker run -it -v [HOST_PATH]:[CONTAINER_PATH] [IMAGE]

# 添加多个 binding
docker run -it -v [HOST_PATH]:[CONTAINER_PATH] -v [HOST_PATH]:[CONTAINER_PATH] [IMAGE]

# 建立 binding 之后使用 inspect 可以从 HostConfig.Binds 里面找到
docker inspect [CONTAINER]

# 限定权限的模式, :ro 代表 read only
docker run -it -v [HOST_PATH]:[CONTAINER_PATH]:ro [IMAGE]

Volume From

# 首先创建一个容器
docker run -it --name CONTAINER_1 [IMAGE]

# 然后在容器里面创建一些文件

# 根据相同的 Image 但是根据 CONTAINER_1 进行扩展
docker run -it --volumes-from CONTAINER_1  --name CONTAINER_2 [IMAGE]

随后两个 CONTAINER 里面会有相同的文件, 在其中任何一个 CONTAINER 里面修改会影响另一个

数据卷的生命周期持续到所有 CONTAINER 的引用消失为止

比如: 此时创建 CONTAINER_3, 然后做一些修改, 然后删除 CONTAINER_1 和 CONTAINER_2, CONTAINER_3 里面依然可以看到修改

Dockerfile

Usage

使用 dockerfile 的主要的步骤:

  1. dockerfile: 编写一个 dockerfile
  2. docker build: 构建的时候引用这个 dockerfile, 生成 image
  3. docker run: image 随后可以构建 container

可以看见在多数情况下,只有构建的时候会使用到这个 dockerfile

特性:

  • 指令结构: 一段大写的单词后面带上一堆参数
  • 从上到下执行
  • 每一条指令都会新建一个镜像层, 并且对镜像进行提交
  • 执行完毕后会提交一个新的镜像层, 并且给予刚提交的镜像运行一个新的容器

Reference

  • FROM
    • 设置一个初始的镜像, 在这个镜像上扩展
    • scratch 代表最原始的镜像
  • WORKDIR
    • 设置在镜像里面的工作目录
  • ADD/COPY
    • 将宿主机目录下的文件拷贝进镜像
    • ADD 命令 比 COPY 多一个步骤, 会自动处理 URL 和解压 tar 压缩包
  • VOLUME
    • 用于数据保存和持久化
    • 会在容器里面创建新的
  • CMD
    • 格式
      • shell 格式: CMD <Command>
      • exec 格式: CMD ["executable file", "arg1", "arg2", ……]
    • Dockerfile 中多个 CMD 只会执行最后一个
      • 一般会在 dockerfile 末尾加一段 CMD 命令, 使得不带参数跑 docker run 的时候运行这段 CMD 命令
      • docker run [IMAGE] 就会默认执行最后一段 CMD
      • docker run [IMAGE] npm run dev 忽略原本 docker file 中的 CMD 并执行 npm run dev
  • ENTRYPOINT
    • 和 CMD 类似, 区别在于这个是一定执行不会有替换最后一段 CMD 的情况, 并且会追加组合对应的命令
    • ENTRYPOINT 的好处:
      • 如果在 dockerfile 中设定了 CMD ["npm", "run","dev"] 随后想要在 docker run 的时候就无法给最后一段 CMD 添加额外参数
        • CMD ["npm", "start"] + docker run [IMAGE] -iCMD ["npm", "-i","start"]
        • 这个时候必须使用 docker run 执行完整的命令或者重新 build container
      • 但是 ENTRYPOINT 可以实现
        • ENTRYPOINT ["npm", "start"] + docker run [IMAGE] -i = ENTRYPOINT ["npm", "-i","start"]
  • ONBUILD
    • 当 build 一个继承镜像的时候触发, 父镜像在被子镜像继承后触发父镜像的 onbuild
  • ENV
    • 环境变量

Example 1

一个简单的 dockerfile:

FROM centos
VOLUME ["/folder1","/folder2"]
CMD echo "Done"
CMD /bin/bash

然后 build 一个新的 image

# 注意尾部有一个点用于当前目录
docker build -f [DOCKERFILE] -t [IMAGE_NAME] . 

# 然后 docker run 跑起一个新的 container
# 新的 container 里面就会带上 folder1 和 folder2 两个目录
docker run -it [IMAGE]

# 需要注意的是, 就算这里没有使用 -v 来 bind host 的目录, docker 依然会生成一个目录用于数据持久化, 这个自动生成的逻辑只有通过 dockerfile 才会执行

Example 2

FROM centos
ENV mypath /tmp # 设置一个环境变量
WORKDIR $mypath # 设置容器内的工作目录, 设定到 /tmp

RUN yum -y install vim
RUN yum -y install net-tools

EXPOSE 80

CMD /bin/bash

Example 3

dockerFile1

FROM centos
ONBUILD RUN echo "this is father image" # 父镜像的 ONBUILD

假设使用上方 dockerfile 构建了一个镜像 centos1: docker build -f dockerfile1 -t centos1 .

然后编写一个新的 dockerFile:

dockerFile2

FROM centos1 # 继承刚才创建的镜像

创建一个新的镜像: docker build -f dockerfile2 -t centos2 .

这个时候就会触发父镜像里面的 ONBUILD

Troubleshotting

RUN 命令没有显示输出

加上一段参数即可

docker build -t hello-world ./ **--progress=plain --no-cache**