200字
Docker
2026-02-02
2026-02-02

Docker的英文翻译是“码头工人”,即搬运工,它搬运的东西就是我们常说的集装箱Container,Container里面装的是任意类型的App。我们的开发人员可以通过Docker将App变成一种标准的、可移植的、自管理的组件,我们可以在任何主流的操作系统中开发、调试和运行。

从概念上来看,Docker和传统的虚拟机比较类似,只是更轻量级,更方便使用。Docker和虚拟机最主要的区别有以下几点:

  • 虚拟化技术依赖的是物理CPU和内存,是硬件级别的;Docker是构建在操作系统层面的,复用操作系统的容器化技术,所以Docker同样可以运行在虚拟机上面。

  • 虚拟机中的操作系统是一个完整的操作系统镜像,比较复杂;而Docker比较轻量级,我们可以用Docker部署一个独立的redis,就类似于在虚拟机当中安装一个redis应用,但Docker部署的应用是完全隔离的。

  • 传统的虚拟机技术是通过快照来保存状态的;而Docker引入了类似于源码管理的机制,将容器历史版本一一记录下来,切换成本非常之低。

  • 传统的虚拟化技术在构建系统的时候非常复杂;而Docker可以通过一个简单的Dockerfile文件来构建整个容器,更重要的是Dockerfile可以手动编写,这样应用程序开发都可以通过发布Dockerfile来定义应用的环境和依赖,对于持续交付非常有利。

img

img

Docker运行在物理机上与运行在虚拟机上的对比:

img

Docker的应用场景

一个做好的应用容器长的就像一个装好了一组特定应用的虚拟机一样,比如我们现在想用redis,那我就找个装好了redis的容器就可以了,然后运行起来,我就能直接使用了。

那为什么不直接安装一个redis呢?肯定是可行的,但是有的时候根据每个人电脑的不同,操作系统的不同,redis的安装方法也各不相同。而且万一机器故障,应用迁移,所有当前机器上安装的应用还需要在新的机器上再全部重新安装一次。但是如果使用容器就要简单多了,你就相当于有了一个快速运行起来的虚拟机,而且方便快速移植。只要你能运行容器,redis的配置就省了。

Docker的特性:

  • 标准化

    • 保证一致的运行环境

    • 弹性伸缩,快速扩容

    • 方便迁移

    • 持续集成、持续交付与持续部署

  • 高性能

    • 不需要进行硬件虚拟以及运行完整的操作系统

  • 轻量级

    • 快速启动

  • 隔离性

    • 进程隔离

Docker Engine

Docker Engine是一个C/S架构的应用程序,主要包含以下几个组件:

常驻后台进程Dockerd

一个用来与Dockerd交互的REST API Server

命令行CLI接口,通过与REST API进行交互(docker命令)

img

Docker架构说明

Docker使用C/S体系的架构,Docker客户端与Docker守护进程通信,Docker守护进程负责构建,运行和分发Docker容器。Docker客户端和守护进程可以在同一个系统上运行,也可以将Docker客户端连接到远程Docker守护进程。Docker客户端和守护进程使用REST API通过unix套接字或网络接口进行通信。

下面是Docker核心组件及元素说明:

Docker Daemon: dockerd,用来监听Docker API的请求和管理Docker对象,比如镜像、容器、网络和Volume。

Docker Client:docker,docker client是我们和Docker进行交互的最主要的方式方法,比如我们可以通过docker run 命令来运行一个容器,然后我们的docker client会把命令发送给上面的Dockerd,主它来负责处理。

Docker Registry:用来存储Docker镜像的仓库,Docker hub是官方提供的一个公共仓库,而且Docker默认也是从Docker Hub上查找镜像的。当然你也可以很方便的运行一个私有仓库,当我们使用docker pull或者docker run命令时,就会从我们配置的docker镜像仓库中去拉取镜像,使用docker push时,会将我们构建的镜像推送到对应的镜像仓库中。

Images:镜像,镜像是一个只读模板 ,带有创建docker容器的说明。一般来说,镜像会基于另外的一些基础镜像构建并加上一些额外的自定义功能。比如你可以构建一个基于centos的镜像,然后在这个基础镜像上面安装一个Nginx应用,这样就可以构建成属于我们自己的镜像了。

Containers:容器,容器是一个镜像的运行实例。可以使用Docker REST API或者CLI来操作容器。容器的实质是进程,但与直接在宿主机执行的进程不同,容器进行运行于属于自己独立的命名空间。因此容器可以拥有自己的root文件系统、自己的网络配置、自己的进程空间,甚至自己的用户id空间。容器内的进程是运行在一个隔离的环境里,使用起来,就好像在一个独立于宿主的系统下操作一样。这种特性使得容器封装的应用比直接在宿主机运行更加案例。

img

另外,关于Docker的底层技术支持:Namespaces(实现隔离)、CGROUPS(资源限制)、UnionFS(镜像和容器分层),可以参考:《docker核心技术与实现原理》,以及《docker官方文档》

一、Docker的基本配置

Docker的部署

Docker版本介绍

  • docker-io:docker早期版本,支持到1.13,在centos 6.x系统上只能使用docker-io

  • moby:社区维护的开源版本

  • docker-ce:docker官方维护的开源版本

  • docker-ee:docker商业版本

Ubuntu安装docker-ce

Plan1

# step 1: 安装必要的一些系统工具
sudo apt-get update
sudo apt-get install ca-certificates curl gnupg
​
# step 2: 信任 Docker 的 GPG 公钥
sudo install -m 0755 -d /etc/apt/keyrings
curl -fsSL https://mirrors.aliyun.com/docker-ce/linux/ubuntu/gpg | sudo gpg --dearmor -o /etc/apt/keyrings/docker.gpg
sudo chmod a+r /etc/apt/keyrings/docker.gpg
​
# step 2: 如果上面方法不生效,使用如下方法
curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo apt-key add -
​
# Step 3: 写入软件源信息
echo \
  "deb https://mirrors.aliyun.com/docker-ce/linux/ubuntu \
  "$(. /etc/os-release && echo "$VERSION_CODENAME")" stable" | \
  sudo tee /etc/apt/sources.list.d/docker.list > /dev/null
 
# Step 4: 安装Docker
sudo apt-get update
sudo apt-get install docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin

Plan2

# step 1: 安装必要的一些系统工具
sudo apt-get update
sudo apt-get -y install apt-transport-https ca-certificates curl software-properties-common
# step 2: 安装GPG证书
curl -fsSL https://mirrors.aliyun.com/docker-ce/linux/ubuntu/gpg | sudo apt-key add -
# Step 3: 写入软件源信息
sudo add-apt-repository "deb [arch=amd64] https://mirrors.aliyun.com/docker-ce/linux/ubuntu $(lsb_release -cs) stable"
# Step 4: 更新并安装Docker-CE
sudo apt-get -y update
sudo apt-get -y install docker-ce
​
# 安装指定版本的Docker-CE:
# Step 1: 查找Docker-CE的版本:
# apt-cache madison docker-ce
#   docker-ce | 17.03.1~ce-0~ubuntu-xenial | https://mirrors.aliyun.com/docker-ce/linux/ubuntu xenial/stable amd64 Packages
#   docker-ce | 17.03.0~ce-0~ubuntu-xenial | https://mirrors.aliyun.com/docker-ce/linux/ubuntu xenial/stable amd64 Packages
# Step 2: 安装指定版本的Docker-CE: (VERSION例如上面的17.03.1~ce-0~ubuntu-xenial)
# sudo apt-get -y install docker-ce=[VERSION]

参考:docker-ce镜像docker-ce下载地址docker-ce安装教程-阿里巴巴开源镜像站 (aliyun.com)

Ubuntu安装docker-ce

# step 1: 安装必要的一些系统工具
sudo yum install -y yum-utils device-mapper-persistent-data lvm2
# Step 2: 添加软件源信息
sudo yum-config-manager --add-repo https://mirrors.ustc.edu.cn/docker-ce/linux/centos/docker-ce.repo
sed -i 's@download.docker.com@mirrors.ustc.edu.cn/docker-ce@g' /etc/yum.repos.d/docker-ce.repo
# Step 3: 更新并安装 Docker-CE
sudo yum makecache fast
sudo yum -y install docker-ce
# Step 4: 开启Docker服务
sudo service docker start
​
# 注意:
# 官方软件源默认启用了最新的软件,您可以通过编辑软件源的方式获取各个版本的软件包。例如官方并没有将测试版本的软件源置为可用,你可以通过以下方式开启。同理可以开启各种测试版本等。
# vim /etc/yum.repos.d/docker-ce.repo
#   将 [docker-ce-test] 下方的 enabled=0 修改为 enabled=1
#
# 安装指定版本的Docker-CE:
# Step 1: 查找Docker-CE的版本:
# yum list docker-ce.x86_64 --showduplicates | sort -r
#   Loading mirror speeds from cached hostfile
#   Loaded plugins: branch, fastestmirror, langpacks
#   docker-ce.x86_64            17.03.1.ce-1.el7.centos            docker-ce-stable
#   docker-ce.x86_64            17.03.1.ce-1.el7.centos            @docker-ce-stable
#   docker-ce.x86_64            17.03.0.ce-1.el7.centos            docker-ce-stable
#   Available Packages
# Step2 : 安装指定版本的Docker-CE: (VERSION 例如上面的 17.03.0.ce.1-1.el7.centos)
# sudo yum -y install docker-ce-[VERSION]

二进制安装docker-ce

wget https://download.docker.com/linux/static/stable/x86_64/docker-24.0.6.tgz
tar xf docker-24.06.tgz
mv docker/* /usr/bin/
groupadd --system docker

编写containerd的启动文件/etc/systemd/system/containerd.service内容如下:

[Unit]
Description=containerd container runtime
Documentation=https://containerd.io
After=network.target local-fs.target
​
[Service]
ExecStartPre=-/sbin/modprobe overlay
ExecStart=/usr/bin/containerd
​
Type=notify
Delegate=yes
KillMode=process
Restart=always
RestartSec=5
# Having non-zero Limit*s causes performance problems due to accounting overhead
# in the kernel. We recommend using cgroups to do container-local accounting.
LimitNPROC=infinity
LimitCORE=infinity
LimitNOFILE=infinity
# Comment TasksMax if your systemd version does not supports it.
# Only systemd 226 and above support this version.
TasksMax=infinity
OOMScoreAdjust=-999
​
[Install]
WantedBy=multi-user.target

编写docker的启动文件/etc/systemd/system/docker.service内容如下:

[Unit]
Description=Docker Application Container Engine
Documentation=https://docs.docker.com
After=network-online.target docker.socket firewalld.service containerd.service time-set.target
Wants=network-online.target containerd.service
Requires=docker.socket
​
[Service]
Type=notify
# the default is not to use systemd for cgroups because the delegate issues still
# exists and systemd currently does not support the cgroup feature set required
# for containers run by docker
ExecStart=/usr/bin/dockerd -H fd:// --containerd=/run/containerd/containerd.sock
ExecReload=/bin/kill -s HUP $MAINPID
TimeoutStartSec=0
RestartSec=2
Restart=always
​
# Note that StartLimit* options were moved from "Service" to "Unit" in systemd 229.
# Both the old, and new location are accepted by systemd 229 and up, so using the old location
# to make them work for either version of systemd.
StartLimitBurst=3
​
# Note that StartLimitInterval was renamed to StartLimitIntervalSec in systemd 230.
# Both the old, and new name are accepted by systemd 230 and up, so using the old name to make
# this option work for either version of systemd.
StartLimitInterval=60s
​
# Having non-zero Limit*s causes performance problems due to accounting overhead
# in the kernel. We recommend using cgroups to do container-local accounting.
LimitNOFILE=infinity
LimitNPROC=infinity
LimitCORE=infinity
​
# Comment TasksMax if your systemd version does not support it.
# Only systemd 226 and above support this option.
TasksMax=infinity
​
# set delegate yes so that systemd does not reset the cgroups of docker containers
Delegate=yes
​
# kill only the docker process, not all processes in the cgroup
KillMode=process
OOMScoreAdjust=-500
​
[Install]
WantedBy=multi-user.target

编写docker的socket文件/etc/systemd/system/docker.socket内容如下:

[Unit]
Description=Docker Socket for the API
​
[Socket]
# If /var/run is not implemented as a symlink to /run, you may need to
# specify ListenStream=/var/run/docker.sock instead.
ListenStream=/var/run/docker.sock
SocketMode=0660
SocketUser=root
SocketGroup=docker
​
[Install]
WantedBy=sockets.target

启动docker:

systemctl daemon-reload
systemctl start docker

docker的配置文件

docker的配置文件为/etc/docker/daemon.json,在安装时这个文件并不会被自动创建;通常我们也不需要修改该文件,docker就可以运行的很好。

下面简单列一下daemon.json的配置项与说明:

{
"api-cors-header": "", 
"authorization-plugins": [],
"bip": "",
"bridge": "",
"cgroup-parent": "",
"cluster-store": "",
"cluster-store-opts": {},
"cluster-advertise": "",
#启用debug的模式,启用后,可以看到很多的启动信息。默认 false
"debug": true, 
"default-gateway": "",
"default-gateway-v6": "",
"default-runtime": "runc",
"default-ulimits": {},
"disable-legacy-registry": false,
#容器DNS的地址,在容器的/etc/resolv.conf 文件中可查看
"dns": ["192.168.80.1"], 
"dns-opts": [],
"dns-search": [],
"exec-opts": [],
"exec-root": "",
"fixed-cidr": "",
"fixed-cidr-v6": "",
#已废弃,使用data-root代替
"graph": "/var/lib/docker", 
#Docker运行时使用的根路径,根路径下的内容稍后介绍,默认/var/lib/docker
"data-root": "/var/lib/docker", 
"group": "", #Unix套接字的属组,仅指/var/run/docker.sock 
"hosts": [], #设置容器hosts
"icc": false,
"insecure-registries": [],
"ip": "0.0.0.0",
"iptables": false,
"ipv6": false,
"ip-forward": false,
"ip-masq": false,
"labels": ["nodeName=node-121"],
"live-restore": true,
"log-driver": "",
"log-level": "",
"log-opts": {
   "max-file": "5",
   "max-size": "50m"
},
"max-concurrent-downloads": 3,
"max-concurrent-uploads": 5,
"mtu": 0,
"oom-score-adjust": -500,
"pidfile": "", #Docker守护进程的PID文件
"raw-logs": false,
"registry-mirrors": ["xxxx"], #镜像加速的地址,增加后在 docker info 中可查看。
"runtimes": {
"runc": {
"path": "runc"
},
"custom": {
"path": "/usr/local/bin/my-runc-replacement",
"runtimeArgs": [
"--debug"
]
}
},
"selinux-enabled": false, #参考:Docker的启动参数
"storage-driver": "overlay2",
"storage-opts": [],
"swarm-default-advertise-addr": "",
"tls": true, #参考:Docker的启动参数
"tlscacert": "", #参考:Docker的启动参数
"tlscert": "", #参考:Docker的启动参数
"tlskey": "", #参考:Docker的启动参数
"tlsverify": true, #参考:Docker的启动参数
"userland-proxy": false,
"userns-remap": ""
}

一个生产的配置示例:

{
    "exec-opts": ["native.cgroupdriver=systemd"],
    "log-driver": "json-file",
    "log-level": "info",
    "log-opts": {
        "max-size": "100m",
        "max-file": "10"
    },
    "bip": "169.254.123.1/24",
    "oom-score-adjust": -1000,
    "registry-mirrors": ["https://pqbap4ya.mirror.aliyuncs.com"],
    "storage-driver": "overlay2",
    "storage-opts":["overlay2.override_kernel_check=true"],
    "live-restore": true
}

二、Docker镜像与容器管理

镜像管理

镜像命名规范

无论我们对镜像做何种操作,首先它得有个名字。我们在前面使用docker run来运行容器的时候,就需要传递一个镜像名称,容器基于该镜像来运行。

一个完整的镜像名称由两部分组成:

<image name> = <repository>:[tag]

其中repository包含如下内容:

[Docker Registry地址/][项目目录/]<名称>

所以一个完整的镜像命名如下:

[Docker Registry地址/][项目目录/]<名称>:[标签]

示例:

hub.breezey.top/op-base/openresty:1.11.2.4
hub.breezey.top/op-base/openresty-php:1.11.2.4-7.0.27
mysql:5.6
ubuntu

当没指明镜像tag时,默认为latest,但latest没有任何特殊含义,在docker hub上很多repository将latest作为最新稳定版本的别名,但这只是一种约定,不是强制规定,一个repository可以有多个tag,而多个tag也可能对应同一个镜像

镜像基本操作

1.获取镜像

docker pull centos:7    #直接从docker hub获取镜像
docker pull registry.cn-zhangjiakou.aliyuncs.com/breezey/mysql:8.0    #从dockerpool获取镜像

2.列出与查看镜像

docker image ls
docker inspect centos:7   #获取镜像的详细信息

3.删除镜像

(注:如果镜像有容器生成,需要先删除容器)

#如果一个镜像有多个tag,只会删除指定的tag,镜像本身不会删除,如果docker rmi后指定镜像ID,则所有tag都会被删除
docker rmi centos:6.6   
​
# 删除无标签镜像(即为none)
docker rmi $(docker images -q --filter "dangling=true")

4.推送镜像

# 为镜像打tag
docker tag nginx:1.25.2 registry.cn-zhangjiakou.aliyuncs.com/library/nginx:1.25.2
docker login registry.cn-zhangjiakou.aliyuncs.com  # 登录镜像仓库,要推送镜像至指定仓库,需要具备相应的权限
docker push registry.cn-zhangjiakou.aliyuncs.com/library/nginx:1.25.2

5.导出和载入镜像

# 将本地镜像导出
docker save -o centos_6.6.tar centos:6.6
​
# 将本地文件导入镜像
docker load --input centos_6.6.tar

6.通过docker commit提交一个新镜像

docker commit -m "Add a new file" -a "Breeze" a925cb40b3f0 test  #使用a925cb40b3f0容器生成一个名为test的镜像
-a:指定作者
-m:相关说明信息
-p:提交时暂停容器运行

容器管理

1.运行一个容器示例:

# 启动一个httpd容器,使其在后台运行并将其80端口映射到宿主机80端口
docker run -d -p 80:80 httpd

2.将容器在前台运行:

# 启动一个ubuntu 22.04的容器,打印完"hello world"即退出
docker run ubuntu:22.04 /bin/echo " hello world "
​
# 在前台运行容器并进入容器与容器交互
docker run ubuntu:22.04 /bin/bash

需要说明的是,容器是为任务而生的。一个容器建议只运行一个进程,而且这个进程需要在容器的前台运行,不能通过daemon的方式运行。如果进程退出,容器也会随之停止

3.容器的启动过程说明:

  • 检查本地是否存在指定的镜像,如果没有就从指定的仓库下载

  • 利用镜像启动一个容器

  • 分配一个文件系统,并在只读的镜像层外面挂载一层可读写层

  • 从宿主机配置的网桥接口中桥接一个虚拟接口到容器中去

  • 从地址池配置一个IP给容器

  • 执行用户指定的程序

  • 执行完毕后停止容器

4.将容器放入后台运行:

docker run -d ubuntu:16.04 /bin/bash -c "while true; do echo hello world; sleep 1;done"

5.docker run常用选项说明

-t:配置一个伪终端并绑定到容器的标准输入上
-i:让容器的标准输入保持打开
-d:将容器放入后台运行
-e:为容器传递环境变量
--restart:设置容器的重启策略
--name:为容器指指定名称
--rm:容器停止时自动删除

6.查看当前节点上的容器状态

docker ps    #查看当前正在运行的容器 
选项:
    -a:查看所有容器,包括停止的
    -q:只显示容器ID
    -l:显示最后一次创建的容器

7.进入容器

docker attach <容器name>    # 多个窗口同时attach到一个窗口时,会同步显示,该指令已废弃
docker exec -it <容器id/容器name> /bin/bash

8.运行容器的最佳实践

容器按用途大致可分为两类:

  • 服务类容器,如webserver、database等

  • 工具类容器,如curl容器、redis-cli容器等

通常而言,服务类容器需要长期运行,所以使用daemon的方式运行;而工作类环境通常是给我们提供一个临时的工作环境,所以一般以run –ti的方式在前台运行

容器的启停操作

# 容器的创建:
  docker create 
​
# 容器的启动:
  docker start <容器id>
​
# 容器的停止:
  docker stop <容器id>
  docker kill <容器id>
​
# 容器的重启:
  docker restart <容器id>
​
# 容器的删除:
  docker rm <容器id>
  选项:
      -f:强行终止并删除一个运行中的容器
        -v:删除容器挂载的数据卷 
​
# 暂停容器:
  docker pause <容器id>
​
# 从暂停中恢复:
  docker unpause <容器id>

容器生命周期管理

img

容器资源限制

一个docker host上会运行若干容器,每个容器都需要CPU、内存和 IO 资源。对于 KVM,VMware等虚拟化技术,用户可以控制分配多少 CPU、内存资源给每个虚拟机。对于容器,Docker 也提供了类似的机制避免某个容器因占用太多资源而影响其他容器乃至整个 host 的性能。

内存限制

启动一个ubuntu容器,限制内存为200M, 内存与swap的总和为300M:

docker run -it -m 200M --memory-swap 300M ubuntu:22.04

选项说明:

-m:允许分配的内存大小
--memory-swap:允许分配的内存和swap的总大小
--memory-swapiness:控制内存与swap置换的比例

需要说明的是,如果启用了--memory-swap参数,相当于使用了swap,则实际内存限制并不生效,要想限制生效,可以不启动该参数,且将--memory-swappiness置为0

下面是一个压测示例:

docker run –it –m 200M –memory-swapiness 0 progrium/stress –-vm 1 –-vm-bytes 180M
选项:
  --vm:设置内存工作线程数
  --vm-byptes:设置单个内存工作线程使用的内存大小

上面的示例中,--vm-bytes为180M,容器工作正常;如果将其修改为230M,则容器OOM退出

参考:Docker 内存资源限制

CPU限制

默认情况下,所有容器可以平等的使用宿主机cpu资源且没有限制。docker可以通过-c--cpu-shares设置容器使用的cpu的权重。如果不指定,默认为1024。

与内存限额不同,通过 -c 设置的 cpu share 并不是 CPU 资源的绝对数量,而是一个相对的权重值。某个容器最终能分配到的 CPU 资源取决于它的 cpu share 占所有容器 cpu share 总和的比例。

换句话说:通过cpu share可以设置容器使用CPU的优先级。

例如,在host中启动了两个容器:

docker run --name container_A -c 1024 ubuntu
​
docker run --name container_B -c 512 ubuntu

container_A的cpu share 1024,是 container_B 的两倍。当两个容器都需要 CPU 资源时,container_A可以得到的 CPU 是container_B的两倍。

需要特别注意的是,这种按权重分配CPU只会发生在CPU 资源紧张的情况下。如果container_A 处于空闲状态,这时,为了充分利用CPU资源,container_B 也可以分配到全部可用的 CPU。

下面是一个压测示例:

# --cpu用于设置cpu工作线程的数量,有几个核就设置为几
docker run --name "container_A" -c 1024 progrium/stress --cpu 1
docker run --name "container_B" -c 512 progrium/stress --cpu 1

两个容器运行起来之后,可以通过在宿主机上使用top查看cpu的资源消耗可以看到两个容器的cpu消耗。

关于cpu资源的更多限制可以参考这里:https://blog.opskumu.com/docker-cpu-limit.html

io 限制

Block IO 是另一种可以限制容器使用的资源。Block IO 指的是磁盘的读写,docker 可通过设置权重、限制 bps 和 iops 的方式控制容器读写磁盘的带宽,下面分别讨论。

需要说明的是,目前Block IO限额只对direct IO(不使用文件缓存)有效

下面是限制bps和iops的参数说明:

  • --device-read-bps,限制读某个设备的 bps。

  • --device-write-bps,限制写某个设备的 bps。

  • --device-read-iops,限制读某个设备的 iops。

  • --device-write-iops,限制写某个设备的 iops。

bps是byte per second,每秒读写的数据量

iops是io per second,每秒io的次数

简单示例:

# 创建一个容器,限制写的bps为30M
docker run -it --device-write-bps /dev/sda:30MB ubuntu
​
# 容器中,执行如下操作查看效果,然后可以通过取消限制,来查看对比效果:
time dd if=/dev/zero of=test.out bs=1M count=800 oflag=direct

关于io资源的更多限制可以参考这里:Docker 磁盘 IO 资源限制

三、Docker网络

简介

当Docker进程启动时,会在主机上创建一个名为docker0的虚拟网桥,此主机上启动的docker容器会连接到这个虚拟网桥上。虚拟网桥的工作方式和物理交换机类似,这样主机上的所有容器就通过交换机连接在了一个二层网络中。从docker0子网中分配一个ip给容器使用,并设置docker0的ip地址为容器的默认网关。在主机上创建一对虚拟网卡veth pair设备,docker将veth pair设备的一端放在新创建的容器中,并命名为eth0(容器的网卡),另一端放在主机中,以vethxxx这样类似的名字命名,并将这个网络设备加入到docker0网桥中。可以通过brctl show命令查看。而这种网络模式即称之为bridge网络模式。

除了bridge模式以外,docker原生网络,还支持另外两种模式: none和host

可以通过如下方法查看docker的网络:

root@k8s-m:~# docker network ls
NETWORK ID          NAME                DRIVER              SCOPE
93392a2471d3        bridge              bridge              local
05c5ebb39921        host                host                local
3cb2552dc6e9        none                null                local

bridge网络

bridge网络基本用法

网络结构示意图:

img

查看bridge网络信息:

# 通过如下命令会列出bridge网络的相关信息,其中"Containers"字段的表示是信息是指当前节点上有哪些容器使用了该网络
docker network inspect bridge

创建使用bridge网络容器的示例:

docker run -d --name web1 --net bridge nginx

基于bridge网络的容器访问外部网络

默认情况下,基于bridge网络容器即可访问外部网络,这是因为默认情况下,docker使用了iptables的snat转发来实现容器对外部的访问(需要内核开启net.ipv4.ip_forward=1)

外部网络访问基于bridge网络的容器

如果想让外界可以访问到基于bridge网络创建的容器提供的服务,则必须要告诉docker要使用的端口。

可以通过如下方法查看镜像会使用哪些端口:

docker inspect nginx | jq .[]."ContainerConfig"."ExposedPorts"

在创建容器的时候可以指定这个容器的端口与主机端口的映射关系:

docker run -d --name web -p 8888:80 nginx
-p: 可以指定主机与容器的端口关系,冒号左边是主机的端口,右边是映射到容器中的端口
-P:该参数会分配镜像中所有的会使用的端口,并映射到主机上的随机端口

这种端口映射基于iptables的dnat实现

查看容器的端口情况:

docker port web

如果创建容器时,-p参数后面只一个指定端口,意思是主机会随机一个端口,映射到容器的该指定端口:

docker run -d --name web -p 80 nginx

下面是一个基于端口映射的示例:

docker run -d --dns 8.8.8.8 -p 8080:80 -p 2022:22 --name webserver1 httpd:2.4
​
docker run -d --dns 8.8.8.8 -P --name webserver1 httpd:2.4

none网络

故名思议,none网络就是什么都没有的网络。使用none网络的容器除了lo,没有其他任何网卡,完全隔离。用于既不需要访问外部服务,也不允许外部服务访问自己的应用场景。

查看none网络信息:

docker network inspect none

创建使用none网络容器的示例:

docker run -d --name web_none --net none nginx

host网络

使用host网络的主机,与宿主机共享网络地址,可以获得最好的数据转发性能。缺点是,同一个宿主机上的多个容器共享同一个ip地址,如果多容器使用相同的端口,需要自行解决端口冲突问题。

同样的,可以通过如下方式查看host网络信息:

docker network inspect host

创建一个使用host网络容器的示例:

# 可以看到该容器没有自己的IP地址,因为它直接使用宿主机IP地址
docker run -d --name web_host -net host nginx

自定义网络

Docker除了提供三种的默认网络模式之外,也允许用户针对一些特定的应用场景去创建一些自定义的网络。这样属于这个网络的容器就可以单独隔离出来,它们之间可以相互通信,而不在这个网络的容器就不能直接访问到它们。一个容器可以属于多个网络,同一个自定义网络下的容器可以通过各自的容器名访问到对方,因为会使用到docker内嵌的一个dns功能。

Docker提供三种自定义网络驱动:

  • bridge

  • overlay

  • macvlan

自定义bridge网络

创建一个自定义网络

1、创建一个叫作my_net的自定义网络:

docker network create --driver bridge my_net    #--driver用于指定网络类型

可以通过docker network ls 查看到新创建的my_net网络相关信息,Subnet表示这个网络下的子网IP段,那么基于my_net自定义网络创建的容器IP都会以该IP段开头。

2、基于my_net网络创建容器:

docker run -d --name web2 --net my_net nginx

通过指定子网和网关的方式创建自定义网络

1、通过指定子网和网关的方式创建my_net2网络:

docker network create --driver bridge --subnet 172.22.16.0/24 --gateway 172.22.16.1 my_net2

2、创建一个容器使用my_net2网络:

docker run --it --network=my_net2 busybox

3、创建一个容器的使用my_net2网络的同时指定其ip地址:

docker run --it --network=my_net2 --ip=172.22.16.8 busybox

使用自定义网络与默认网络互通

假设我们在默认的bridge网络中,还有一个httpd的容器:

docker run -d --name webserver httpd:2.4

此时默认网络中的容器与my_net2网络中的容器是无法互相通信的。宿主机上网络结构如下:

img

docker_network_custom_bridge

如果想让默认bridge网络的httpd与my_net2中的容器通信,可以给httpd容器添加一块自定义网络的网卡,使用如下指令:

docker network connect my_net2 webserver

如果要将webserver新添加的这块网卡移除,可以使用如下命令:

docker network disconnect bridge webserver

同一台宿主机容器互联

同一台宿主机上的容器互联有两种方式,第一种是基于ip,默认情况下,同一个宿主机上的容器ip是互通的。另一种方式是使用--link实现:

docker run -d --name db1 -e "MYSQL_ROOT_PASSWORD=123456" -P mysql:5.6
​
docker run -d --link db1:db1 --name webserver1 httpd:2.4

四、Docker跨平台镜像构建

简介

随着公司业务发展,一个应用需要被同时部署在arm和x86两种cpu架构的平台之上。这就要求应用的容器镜像需要同时满足在不同的平台上运行。

这里至少有四种办法来实现:

  1. 在不同cpu架构的系统上各构建一次镜像,打上不同平台的tag,在使用时,根据目标集群的cpu架构类型选择对应的镜像;

  2. 在当前系统上通过虚拟化技术模拟不同的cpu架构,目前主流的模拟器是开源的QEMU, 其支持ARM、Power-PC、RISC-V等常见的cpu架构;通过模拟一个完整的操作系统创建通用的ARM虚拟机来完成编译

  3. 模拟目标硬件的用户空间。 在 Linux 上,QEMU 除了可以模拟完整的操作系统之外,还有另外一种模式叫用户态模式(User mod)。该模式下 QEMU 将通过 binfmt_misc 在 Linux 内核中注册一个二进制转换处理程序,并在程序运行时动态翻译二进制文件,根据需要将系统调用从目标 CPU 架构转换为当前系统的 CPU 架构。最终的效果看起来就像在本地运行目标 CPU 架构的二进制文件。

  4. 交叉编译器。

docker从19.03引入了一个实验性插件,通过模拟目标硬件的用户空间以支持跨平台的docker镜像构建。

部署构建器依赖

buildx

docker通过buildx插件支持跨平台构建, 要使用buildx,需要docker版本不低于19.03,另外,在19.03版本中,还需要通过设置环境变量DOCKER_CLI_EXPERIMENTAL来启用该插件:

export DOCKER_CLI_EXPERIMENTAL=enabled

在20.x版本中,默认已启用。

验证是否开启:

# docker buildx version 
github.com/docker/buildx v0.5.1-docker 11057da37336192bfc57d81e02359ba7ba848e4a

buildx现在已经是docker的默认构建器

binfmt_misc

如果使用的是linux系统,需要手动启用binfmt_misc;如果使用的是docker桌面版,则该功能默认启用。

升级内核

需要说明的是,binfmt_misc要求linux内核至少是4.x以上版本,否则安装会报错,升级内核操作如下:

# 添加内核的yum源
rpm -import https://www.elrepo.org/RPM-GPG-KEY-elrepo.org
rpm -Uvh http://www.elrepo.org/elrepo-release-7.0-2.el7.elrepo.noarch.rpm
​
# 列出可升级的内核版本
yum --disablerepo="*" --enablerepo="elrepo-kernel" list available
​
# 安装4.4版本
yum install -y --enablerepo=elrepo-kernel install kernel-lt.x86_64 kernel-lt-devel.x86_64
​
# 查看当前支持的内核版本
[root@cephfsNode1 ~]# cat /boot/grub2/grub.cfg |grep menuentry
if [ x"${feature_menuentry_id}" = xy ]; then
  menuentry_id_option="--id"
  menuentry_id_option=""
export menuentry_id_option
menuentry 'CentOS Linux (5.4.109-1.el7.elrepo.x86_64) 7 (Core)' --class centos --class gnu-linux --class gnu --class os --unrestricted $menuentry_id_option 'gnulinux-3.10.0-514.26.1.el7.x86_64-advanced-32860528-52a7-4814-897e-d56563da7040' {
menuentry 'CentOS Linux (3.10.0-862.2.3.el7.x86_64) 7 (Core)' --class centos --class gnu-linux --class gnu --class os --unrestricted $menuentry_id_option 'gnulinux-3.10.0-514.26.1.el7.x86_64-advanced-32860528-52a7-4814-897e-d56563da7040' {
menuentry 'CentOS Linux (3.10.0-514.26.1.el7.x86_64) 7 (Core)' --class centos --class gnu-linux --class gnu --class os --unrestricted $menuentry_id_option 'gnulinux-3.10.0-514.26.1.el7.x86_64-advanced-32860528-52a7-4814-897e-d56563da7040' {
menuentry 'CentOS Linux (0-rescue-740e85aedb5cd75e077b2e0be6e3f6eb) 7 (Core)' --class centos --class gnu-linux --class gnu --class os --unrestricted $menuentry_id_option 'gnulinux-0-rescue-740e85aedb5cd75e077b2e0be6e3f6eb-advanced-32860528-52a7-4814-897e-d56563da7040' {
​
# 修改默认的启动内核
[root@cephfsNode1 ~]# grub2-set-default 'CentOS Linux (5.4.109-1.el7.elrepo.x86_64) 7 (Core)'
​
# 查看是否修改成功
[root@cephfsNode1 ~]# grub2-editenv list 
saved_entry=CentOS Linux (5.4.109-1.el7.elrepo.x86_64) 7 (Core)
​
# 重启
[root@cephfsNode1 ~]# reboot
​
# 开机后执行检查 
[root@cephfsNode1 ~]# uname -r 
5.4.109-1.el7.elrepo.x86_64

部署binfmt_misc

直接使用docker安装即可:

docker run --privileged --rm tonistiigi/binfmt --install all

验证binfmt_misc是否开启:

# ll /proc/sys/fs/binfmt_misc/
total 0
-rw-r--r-- 1 root root 0 Apr  6 16:09 jexec
-rw-r--r-- 1 root root 0 Apr  6 16:28 qemu-aarch64
-rw-r--r-- 1 root root 0 Apr  6 16:28 qemu-arm
-rw-r--r-- 1 root root 0 Apr  6 16:28 qemu-ppc64le
-rw-r--r-- 1 root root 0 Apr  6 16:28 qemu-s390x
--w------- 1 root root 0 Apr  6 16:09 register
-rw-r--r-- 1 root root 0 Apr  6 16:09 status

验证是否启用了相应的处理器:

# cat  /proc/sys/fs/binfmt_misc/qemu-arm
enabled
interpreter /usr/bin/qemu-arm
flags: OCF
offset 0
magic 7f454c4601010100000000000000000002002800
mask ffffffffffffff00fffffffffffffffffeffffff

需要说明的是,上面除了使用tonistiigi/binfmt之外,社区还提供了一个模拟了更多cpu架构的镜像,项目地址: https://github.com/multiarch/qemu-user-static

可使用如下方式启动:

docker run --rm --privileged multiarch/qemu-user-static --reset -p yes

注意,无论使用哪种方式,内核都需要升级

配置docker以支持跨平台构建

在较新版本的docker中(我这里使用的是docker 20.7),docker要支持跨平台需要为docker引擎配置containerd image store,需要修改docker的配置文件/etc/docker/daemon.json添加如下配置:

{
  "features": {
    "containerd-snapshotter": true
  }
}

之后再重启docker:

systemctl restart docker

检查是否修改成功:

docker info -f '{{ .DriverStatus }}'

返回如下结果即说明修改成功:

[[driver-type io.containerd.snapshotter.v1]]

参考: containerd image store with Docker Engine | Docker Docs

构建多平台镜像

编写dockerfile

以一个简单的hello.go程序为例

package main
import (
        "fmt"
        "runtime"
)
​
func main() {
        fmt.Printf("Hello, %s!\n", runtime.GOARCH)
}

Dockerfile示例如下:

# cat Dockerfile 
FROM golang:alpine AS builder
RUN mkdir /app
ADD hello.go /app/hello.go
WORKDIR /app
RUN go build -ldflags '-w -s' -o hello hello.go
​
FROM alpine
RUN mkdir /app
WORKDIR /app
COPY --from=builder /app/hello .
CMD ["./hello"]

这是一个多阶段构建 Dockerfile,使用 Go 编译器来构建应用,并将构建好的二进制文件拷贝到 alpine 镜像中。

下面使用buildx构建支持arm、arm64以及amd64多架构的docker镜像:

docker buildx build -t hub.dz11.com/library/hello:multiarch --platform=linux/arm,linux/arm64,linux/amd64 . --push

就可以通过 docker pull hub.dz11.com/library/hello:multiarch 拉取刚刚创建的镜像了,Docker 将会根据你的 CPU 架构拉取匹配的镜像。

需要说明的是,对于这个多架构镜像的支持对镜像仓库也有要求,目前harbor需要v2.0及以上版本才支持。公有云上目前已知阿里云acr支持。

验证

获取镜像:

docker pull hub.dz11.com/library/hello:multiarch

查看镜像的digests:

# docker buildx imagetools inspect hub.dz11.com/library/hello:multiarch
Name:      hub.dz11.com/library/hello:multiarch
MediaType: application/vnd.docker.distribution.manifest.list.v2+json
Digest:    sha256:2558e1aced4bbca1bdd341f8ac23795565c3028a4b24dfbc95335ce28289d795
           
Manifests: 
  Name:      hub.dz11.com/library/hello:multiarch@sha256:ddcae8630328ef08256bcc55b15e10cda4e6d1aaeb620f76d4831698f4cb70d2
  MediaType: application/vnd.docker.distribution.manifest.v2+json
  Platform:  linux/arm/v7
             
  Name:      hub.dz11.com/library/hello:multiarch@sha256:faaaca16d36bbe2d871fc8d7ebfa496dc2a80518938b76656affe06cc02af9a2
  MediaType: application/vnd.docker.distribution.manifest.v2+json
  Platform:  linux/arm64
             
  Name:      hub.dz11.com/library/hello:multiarch@sha256:66868e27a5095a181b53ca76fa7ef9e5e40caf6a69f4f21b948bb6697840f5b8
  MediaType: application/vnd.docker.distribution.manifest.v2+json
  Platform:  linux/amd64
​

模拟不同平台运行容器并观察输出:

# docker run --rm hub.dz11.com/library/hello:multiarch@sha256:ddcae8630328ef08256bcc55b15e10cda4e6d1aaeb620f76d4831698f4cb70d2
Unable to find image 'hub.dz11.com/library/hello:multiarch@sha256:ddcae8630328ef08256bcc55b15e10cda4e6d1aaeb620f76d4831698f4cb70d2' locally
hub.dz11.com/library/hello@sha256:ddcae8630328ef08256bcc55b15e10cda4e6d1aaeb620f76d4831698f4cb70d2: Pulling from library/hello
07389e51ea05: Pull complete 
bb912632da08: Pull complete 
0b8a8f7a4f75: Pull complete 
Digest: sha256:ddcae8630328ef08256bcc55b15e10cda4e6d1aaeb620f76d4831698f4cb70d2
Status: Downloaded newer image for hub.dz11.com/library/hello@sha256:ddcae8630328ef08256bcc55b15e10cda4e6d1aaeb620f76d4831698f4cb70d2
WARNING: The requested image's platform (linux/arm/v7) does not match the detected host platform (linux/amd64) and no specific platform was requested
Hello, arm!
​
[root@app.a1-61-27.pub.unp ~/hello]
# docker run --rm hub.dz11.com/library/hello:multiarch@sha256:faaaca16d36bbe2d871fc8d7ebfa496dc2a80518938b76656affe06cc02af9a2
Unable to find image 'hub.dz11.com/library/hello:multiarch@sha256:faaaca16d36bbe2d871fc8d7ebfa496dc2a80518938b76656affe06cc02af9a2' locally
hub.dz11.com/library/hello@sha256:faaaca16d36bbe2d871fc8d7ebfa496dc2a80518938b76656affe06cc02af9a2: Pulling from library/hello
912815139b61: Pull complete 
cff4c0e7ad17: Pull complete 
808145d5da75: Pull complete 
Digest: sha256:faaaca16d36bbe2d871fc8d7ebfa496dc2a80518938b76656affe06cc02af9a2
Status: Downloaded newer image for hub.dz11.com/library/hello@sha256:faaaca16d36bbe2d871fc8d7ebfa496dc2a80518938b76656affe06cc02af9a2
WARNING: The requested image's platform (linux/arm64) does not match the detected host platform (linux/amd64) and no specific platform was requested
Hello, arm64!
​
[root@app.a1-61-27.pub.unp ~/hello]
# docker run --rm hub.dz11.com/library/hello:multiarch@sha256:66868e27a5095a181b53ca76fa7ef9e5e40caf6a69f4f21b948bb6697840f5b8
Unable to find image 'hub.dz11.com/library/hello:multiarch@sha256:66868e27a5095a181b53ca76fa7ef9e5e40caf6a69f4f21b948bb6697840f5b8' locally
hub.dz11.com/library/hello@sha256:66868e27a5095a181b53ca76fa7ef9e5e40caf6a69f4f21b948bb6697840f5b8: Pulling from library/hello
Digest: sha256:66868e27a5095a181b53ca76fa7ef9e5e40caf6a69f4f21b948bb6697840f5b8
Status: Downloaded newer image for hub.dz11.com/library/hello@sha256:66868e27a5095a181b53ca76fa7ef9e5e40caf6a69f4f21b948bb6697840f5b8
Hello, amd64!

要想在一个节点上启动三种不同cpu架构的镜像,需要验证节点安装binfmt_misc

附录

ubuntu跨平台构建arm镜像异常

ubuntu的基础镜像,在使用docker buildx跨平台构建arm镜像时,抛出了如下异常:

11:15:59  #17 117.1 dpkg: 处理归档 /tmp/apt-dpkg-install-BC07v3/30-libini-config5_0.6.1-2_arm64.deb (--unpack)时出错:
11:15:59  #17 117.1  dpkg-deb --control 子进程返回错误状态 1
11:15:59  #17 117.1 Error while loading /usr/sbin/dpkg-split: No such file or directory
11:15:59  #17 117.1 Error while loading /usr/sbin/dpkg-deb: No such file or directory

查了下,需要在构建节点上安装qemu-user-static模拟器:

docker run --rm --privileged multiarch/qemu-user-static --reset -p yes

之后再配置跨平台构建器:

docker buildx create --name builder --driver docker-container --driver-opt=network=host --use
docker buildx inspect --bootstrap

此时,即可正常构建

buildx常用构建指令

通过buildx,我们可以一次性为一个应用生成多平台镜像,也可以一次只生成一个平台的镜像,并将镜像保存在本地:

docker buildx build -t hub.dz11.com/library/hello:multiarch --platform=linux/arm -o type=docker .
docker buildx build -t hub.dz11.com/library/hello:multiarch --platform=linux/arm64 -o type=docker .
docker buildx build -t hub.dz11.com/library/hello:multiarch --platform=linux/amd64 -o type=docker .

参考

五、Docker Volume详解

数据卷基本使用

docker提供数据卷来实现数据共享与持久化,而数据卷的挂载有两种方式:

  • 挂载主机目录(Bind mounts)

  • 数据卷(Data Volumes)

数据卷是一个可供容器使用的特殊目录,它绕过文件系统,可以提供很多有用的特性:

  • 数据卷可以在容器之间共享和重用

  • 对数据卷的修改会立马生效

  • 对数据卷的更新不会影响镜像

  • 卷会一直存在,只到没有容器使用

挂载主机目录

挂载一个宿主机目录作为数据卷

将宿主机的/data目录挂载至容器的/webapp目录:

docker run -d -P --name web -v /data/nginx:/usr/share/nginx/html  nginx:1.25.2 /bin/bash 

除了支持目录挂载,也支持文件挂载:

docker run -d -P --name web -v /data/nginx:/usr/share/nginx/html -v /etc/localtime:/etc/localtime nginx:1.25.2 /bin/bash 

docker挂载数据卷还支持指定权限:

  • rw:以读写方式挂载,默认权限;

  • ro:以只读方式挂载

示例如下:

docker run -d -P --name web -v /data/nginx:/usr/share/nginx/html:ro -v /etc/localtime:/etc/localtime:ro nginx:1.25.2 /bin/bash 

目录以只读的方式挂载之后,目录里面的文件只读,目录本身也只读;文件以只读的方式挂载之后,文件不能被修改,但可以被删除

数据卷管理

bind mounts的方式是将一个宿主机的目录挂载进容器,而数据库则刚好反过来,将一个容器的目录挂载至宿主机上的目录

数据卷基本操作

# 列出当前主机上的数据卷
docker volume ls
​
# 创建数据卷
docker volume create --driver local test-vol
​
# 查看数据卷详情
docker volume inspect test-vol
​
# 挂载数据库
docker run -d -v test-vol:/usr/share/nginx/html nginx:1.25.2
​
# 挂载一个匿名卷
# 匿名卷就是指没有名字的卷
docker run -d -v /usr/hsare/nginx/html nginx:1.25.2
​

数据卷删除操作

默认情况下,在删除容器时,docker并不会删除其数据卷,查看没有容器在使用的数据盘方法如下:

docker volume ls -f dangling=true

如果想要删除没有使用的数据盘,使用如下指令:

docker volume rm VOLUME_NAME

删除掉容器时,同时删掉数据盘,则使用:

docker rm -v container_name

清理主机上所有未被使用的数据卷:

docker volume prune -a 

设置lvm数据卷(卷存储限制)

创建lvm:

pvcreate /dev/sdb
vgcreate docker-volume /dev/sdb
lvcreate -L 10G -n docker-lv docker-volume
mkfs.ext4 /dev/docker-volume/docker-lv

挂载lvm:

mount /dev/docker-volume/docker-lv /data/docker-lv

创建卷:

docker volume create --driver local \
    --opt type=ext4 \
    --opt device=/data/docker-lv \
    --opt o=bind \
    lvm_volume

使用:

docker run --rm -v lvm_volume:/data ubuntu:22.04 /bin/bash
# 进入到容器后查看挂载
df -HT

限制docker容器存储

参考: storage-quota · 工作笔记

附录

参考: Volumes | Docker Docs

六、Docker Compose

Docker Compose介绍

Docker Compose是一个定义和运行多容器应用的单机编排工具。通过Docker Compose你可以使用一个单一的YAML文件来配置多个应用服务,通过一条命令,就可以将所有配置的服务全部启动起来。

使用Docker Compose的三个步骤:

  • 使用Dockerfile定义环境,这样可以确保其在任意地方运行

  • 使用docker-compose.yml文件定义服务,这样它们就可以在独立环境中一起运行

  • 运行docker-compose up使用docker-compose启动所有应用

Docker Compose可以管理应用的整个生命周期:

  • 启动、停止、重建服务

  • 查看服务的运行状态

  • 流式输出服务日志

  • 对服务执行一次性命令

Docker Compose安装

二进制安装:

下载地址:https://github.com/docker/compose/releases

pip安装:

pip install docker-compose

Docker Compose基本示例

1、基本文件及目录设置

  1. 创建一个目录:

mkdir composetest
cd composetest
  1. 在上面的目录中创建一个app.py文件,内容如下:

import time
​
import redis
from flask import Flask
​
​
app = Flask(__name__)
cache = redis.Redis(host='redis', port=6379)
​
​
def get_hit_count():
    retries = 5
    while True:
        try:
            return cache.incr('hits')
        except redis.exceptions.ConnectionError as exc:
            if retries == 0:
                raise exc
            retries -= 1
            time.sleep(0.5)
​
​
@app.route('/')
def hello():
    count = get_hit_count()
    return 'Hello World! I have been seen {} times.\n'.format(count)
​
if __name__ == "__main__":
    app.run(host="0.0.0.0", debug=True)
  1. 再创建一个pip.conf文件,内容如下:

[global]
index-url = https://mirrors.aliyun.com/pypi/simple/
​
[install]
trusted-host=mirrors.aliyun.com

2、创建一个Dockerfile

仍然在composetest目录中创建一个Dockerfile,内容如下:

FROM python:3.4-alpine
ADD . /code
WORKDIR /code
RUN pip install -r requirements.txt
CMD ["python", "app.py"]

3、通过docker-compose.yml定义服务

docker-compose.yml内容如下:

version: '3'
services:
  web:
    build: .
    ports:
     - "5000:5000"
  redis:
    image: "redis:alpine"

这个文件定义了两个服务:web和redis。

  • web服务使用当前目录的Dockerfile进行构建,并且映射web服务的5000端口到宿主机5000端口。

  • redis服务使用一个公共的redis镜像。

4、通过Docker Compose构建并启动服务

docker-compose up

这个时候可以通过http://127.0.0.1:5000来访问这个web服务。

5、 修改Compse文件,添加一个挂载点

修改docker-compose.yml,内容如下:

version: '3'
services:
  web:
    build: .
    ports:
     - "5000:5000"
    volumes:
     - .:/code
  redis:
    image: "redis:alpine"

添加了一个volumes配置项,将当前目录挂载至web容器的/code目录下。

然后我们通过docker-compose up重新构建应用,再次访问,会发现结果与上面完全相同。

6、 更新应用

我们修改本地的app.py,修改Hello World!Hello from Dokcer,如下:

return 'Hello from Docker! I have been seen {} times.\n'.format(count)

这个时候再次访问http://127.0.0.1:5000,发现访问内容也随之修改。

这是因为在上面我们将本地目录挂载进了容器,我们修改本地的app.py就相当于修改了容器内的文件。

Docker Compose常用命令说明

在上面的一个简单示例中,我们已经使用了docker-compose up来启动一个docker-compose.yml文件定义的服务。我们刚刚通过docker-compose up虽然启动了服务,当是docker-compose指令却在前台执行,如果需要将其放入后台运行,可以使用-d参数:

docker-compose up -d

docker-compose up还可以使用--scale参数实现服务的扩缩容:

[root@app composetest]# docker-compose up -d --scale web=2
Recreating composetest_web_1 ... 
Recreating composetest_web_1 ... done
Creating composetest_web_2   ... done

还可以通过-f选项指定compose文件:

[root@app tranning]# docker-compose -f test-compose.yml up -d
Creating network "tranning_default" with the default driver
Creating network "tranning_frontend" with the default driver
Creating network "tranning_backend" with the default driver
Creating tranning_visualizer_1 ... done
Creating tranning_redis_1      ... done
Creating tranning_worker_1     ... done
Creating tranning_db_1         ... done
Creating tranning_result_1     ... done
Creating tranning_vote_1       ... done

需要说明的是,如果使用自动扩容,则web服务不能做端口映射,否则会出现端口冲突的情况

下面我们说一说其他常用的docker-compose命令:

  1. docker-compose ps

[root@app composetest]# docker-compose ps 
       Name                      Command               State           Ports         
-------------------------------------------------------------------------------------
composetest_redis_1   docker-entrypoint.sh redis ...   Up      6379/tcp              
composetest_web_1     python app.py    
  1. docker-compose stop

[root@app composetest]# docker-compose stop
Stopping composetest_redis_1 ... done
Stopping composetest_web_1   ... done
​
[root@app composetest]# docker-compose stop web
Stopping composetest_web_1 ... done
  1. docker-compose start

[root@app composetest]# docker-compose start
Starting web   ... done
Starting redis ... done
​
[root@app composetest]# docker-compose start web
Starting web ... done
  1. docker-compose restart

[root@app composetest]# docker-compose restart
Restarting composetest_web_1   ... done
Restarting composetest_redis_1 ... done
  1. docker-compose run <service> [command]

[root@app composetest]# docker-compose run web env
PATH=/usr/local/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
HOSTNAME=72ad8a0682b7
TERM=xterm
LANG=C.UTF-8
GPG_KEY=97FC712E4C024BBEA48A61ED3A5CA953F73C700D
PYTHON_VERSION=3.4.9
PYTHON_PIP_VERSION=18.0
HOME=/root
  1. docker-compose down

[root@app composetest]# docker-compose down
Stopping composetest_redis_1 ... done
Stopping composetest_web_1   ... done
Removing composetest_redis_1 ... done
Removing composetest_web_1   ... done
Removing network composetest_default

通过--volumes还要以删除自动挂载的容器卷

  1. docker-compose build

默认情况下,我们写好了Dockerfile,第一次通过docker-compose启动的时候,会自动完成构建,但如果随后Dockerfile发生了改动,再次通过docker-compose来启动实现更新的话,docker-compose不会再次自动构建镜像,而是复用第一次生成的镜像,如果希望镜像能够被重新构建,需要单独执行docker-compose build

  1. docker-compose top

[root@app composetest]# docker-compose top
composetest_redis_1
UID    PID    PPID    C   STIME   TTY     TIME         CMD     
---------------------------------------------------------------
100   89653   89634   0   23:26   ?     00:00:00   redis-server
​
composetest_web_1
UID     PID    PPID    C   STIME   TTY     TIME                 CMD             
--------------------------------------------------------------------------------
root   89635   89619   0   23:26   ?     00:00:00   python app.py               
root   89742   89635   0   23:26   ?     00:00:00   /usr/local/bin/python app.py
  1. 其它

docker-compose rm       # 通过这种方式也能删除指定服务,但不会删除网络和volumes
docker-compose kill     # 强制杀死一个服务
docker-compose logs     # 用于查看日志

Docker Compose文件详解

通过之前的示例,其实我们可以看到,所有服务的管理,都是依靠docker-compose.yml文件来实现的。那么我们接下来就详细说一说docker-compose.yml文件中的常用指令。

compose文件使用yml格式,docker规定了一些指令,使用它们可以去设置对应的东西,主要分为了四个区域:

  • version:用于指定当前docker-compose.yml语法遵循哪个版本

  • services:服务,在它下面可以定义应用需要的一些服务,每个服务都有自己的名字、使用的镜像、挂载的数据卷、所属的网络、依赖哪些其他服务等等。

  • networks:应用的网络,在它下面可以定义应用的名字、使用的网络类型等。

  • volumes:数据卷,在它下面可以定义数据卷,然后挂载到不同的服务下去使用。

version

用于指定当前compose文件语法遵循哪个版本,下面这张表是不同的Compose文件版本兼容的Docker版本:

docker-compse

services

我们上面所说的所有服务的定义都是定义在services区域中,接下来,我们学习下services下常用的配置项

image

标明image的ID,这个image ID可以是本地也可以是远程的,如果本地不存在,compose会尝试pull下来

示例:

image: ubuntu
image: hub.dz11.com/library/tomcat:8

build

该参数指定Dockerfile文件的路径,compose会通过Dockerfile构建并生成镜像,然后使用该镜像

示例:

build: /path/to/build/dir

command

重写默认的命令,或者说指定启动容器的命令

示例:

command: "/run.sh"

链接到其他服务中的容器,可以指定服务名称和这个链接的别名,或者只指定服务名称

示例:

links:
  - db
  - db:database
  - redis

此时,在容器内部,会在/etc/hosts文件中用别名创建几个条目,如下:

172.17.2.100 db
172.17.2.100 database
172.17.2.100 redis

链接到compose外部启动的容器,特别是对于提供共享和公共服务的容器。在指定容器名称和别名时,external_links遵循着和links相同的语义用法

示例:

external_links:
  - redis_1
  - project_db_1: mysql
  - project_db_2: postgresql

ports

暴露端口,指定宿主机到容器的端口映射,或者只指定容器的端口,则表示映射到主机上的随机端口

注:当以 主机:容器 的形式来映射端口时,如果使容器的端口小于60,那可能会出现错误,因为YAML会将 xx:yy这样格式的数据解析为六十进制的数据,基于这个原因,时刻记得要将端口映射明确指定为字符串

示例:

ports:  
 - "3000"  
 - "8000:8000"  
 - "49100:22"  
 - "127.0.0.1:8001:8001" 

expose

暴露端口,但不需要建立与宿主机的映射,只是会向链接的服务提供

示例:

expose:
  - "3000"
  - "8000"

environment

加入环境变量,可以使用数组或者字典,只有一个key的环境变量可以在运行compose的机器上找到对应的值

示例:

environment:
  RACK_ENV: development
  SESSION_SECRET:

environment:
  - RACK_ENV: development
  - SESSION_SECRET:

env_file

从一个文件中引入环境变量,该文件可以是一个单独的值或者一个列表,如果同时定义了environment,则environment中的环境变量会重写这些值

示例:

env_file:
  - env
cat env
RACK_ENV: development

depends_on

定义当前服务启动时,依赖的服务,当前服务会在依赖的服务启动后启动

depends_on:
  - redis

ulimit

ulimits:
  nproc: 65535
  nofile:
    soft: 20000
    hard: 40000

deploy

该配置项在version 3里才引入,用于指定服务部署和运行时相关的参数

version: '3'
services:
  redis:
    image: redis:alpine
    deploy:
      replicas: 6
      update_config:
        parallelism: 2
        delay: 10s
      restart_policy:
        condition: on-failure

下面是deploy中常用参数的说明

replicas

指定副本数:

version: '3'
services:
  worker:
    image: dockersamples/examplevotingapp_worker
    deploy:
      replicas: 6

restart_policy

指定重启策略:

version: "3"
services:
  redis:
    image: redis:alpine
    deploy:
      restart_policy:
        condition: on-failure   #重启条件:on-failure, none, any
        delay: 5s   # 等待多长时间尝试重启
        max_attempts: 3 #尝试的次数
        window: 120s    # 在决定重启是否成功之前等待多长时间

update_config

定义更新服务的方式,常用于滚动更新

version: '3.4'
services:
  vote:
    image: dockersamples/examplevotingapp_vote:before
    depends_on:
      - redis
    deploy:
      replicas: 2
      update_config:
        parallelism: 2  # 一次更新2个容器
        delay: 10s  # 开始下一组更新之前,等待的时间
        failure_action:pause  # 如果更新失败,执行的动作:continue, rollback, pause,默认为pause
        max_failure_ratio: 20 # 在更新过程中容忍的失败率
        order: stop-first   # 更新时的操作顺序,停止优先(stop-first,先停止旧容器,再启动新容器)还是开始优先(start-first,先启动新容器,再停止旧容器),默认为停止优先,从version 3.4才引入该配置项

resources

限制服务资源:

version: '3'
services:
  redis:
    image: redis:alpine
    deploy:
      resources:
        limits:
          cpus: '0.50'
          memory: 50M
        reservations:
          cpus: '0.25'
          memory: 20M

healthcheck

执行健康检查

healthcheck:
  test: ["CMD", "curl", "-f", "http://localhost"]   # 用于健康检查的指令
  interval: 1m30s   # 间隔时间
  timeout: 10s  # 超时时间
  retries: 3    # 重试次数
  start_period: 40s # 启动多久后开始检查

network_mode

网络类型,可指定容器运行的网络类型

示例:

network_mode: "bridge"
network_mode: "host"
network_mode: "none"

dns

自定义dns服务

示例:

dns: 8.8.8

dns:
  - 223.5.5.5
  - 223.6.6.6

networks

网络决定了服务之间以及服务和外界之间如何去通信,在执行docker-compose up的时候,docker会默认创建一个默认网络,创建的服务也会默认的使用这个默认网络。服务和服务之间,可以使用服务的名字进行通信,也可以自己创建网络,并将服务加入到这个网络之中,这样服务之间可以相互通信,而外界不能够与这个网络中的服务通信,可以保持隔离性。

在networks中定义一个名为app_net,类型为driver的网络:

version: '2'

services:
  app:
    image: busybox
    command: ifconfig
    networks:
      app_net:
        ipv4_address: 172.16.238.10

networks:
  app_net:
    driver: bridge
    enable_ipv6: true
    ipam:
      driver: default
      config:
      - subnet: 172.16.238.0/24
        gateway: 172.168.238.254

volumes

version: "3.2"
services:
  web:
    image: nginx:alpine
    volumes:
      - type: volume
        source: mydata
        target: /data
        volume:
          nocopy: true
      - type: bind
        source: ./static
        target: /opt/app/static
​
  db:
    image: postgres:latest
    volumes:
      - "/var/run/postgres/postgres.sock:/var/run/postgres/postgres.sock"
      - "dbdata:/var/lib/postgresql/data"
​
volumes:
  mydata:
  dbdata:

Docker Compose案例实践

部署一个web集群

项目说明

这是一个典型的web项目,由一个haproxy容器加三个web容器组成。haproxy在前端充当负载均衡器,反向代理到后台三个服务服务。

基本目录结构

首先创建一个compose-haproxy-web的目录,然后在目录里面,创建两个子目录:haproxy和web。

在web目录里包含三个文件: Dockerfile、index.py、index.html

在haproxy目录里包含一个文件: haproxy.cfg

目录结构如下:

compose-haproxy-web/
├── docker-compose.yml
├── haproxy
│   └── haproxy.cfg
└── web
    ├── Dockerfile
    ├── index.html
    └── index.py
​
2 directories, 5 files

web目录下的index.py提供一个简单的http服务,打印出访问者的ip和实际的本地IP,内容如下:

#!/usr/bin/python
#authors: yeasy.github.com
#date: 2013-07-05
import sys
import BaseHTTPServer
from SimpleHTTPServer import SimpleHTTPRequestHandler
import socket
import fcntl
import struct
import pickle
from datetime import datetime
from collections import OrderedDict
​
class HandlerClass(SimpleHTTPRequestHandler):
  def get_ip_address(self,ifname):
    s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
    return socket.inet_ntoa(fcntl.ioctl(
      s.fileno(),
      0x8915, # SIOCGIFADDR
      struct.pack('256s', ifname[:15])
    )[20:24])
  def log_message(self, format, *args):
    if len(args) < 3 or "200" not in args[1]:
      return
    try:
      request = pickle.load(open("pickle_data.txt","r"))
    except:
      request=OrderedDict()
    time_now = datetime.now()
    ts = time_now.strftime('%Y-%m-%d %H:%M:%S')
    server = self.get_ip_address('eth0')
    host=self.address_string()
    addr_pair = (host,server)
    if addr_pair not in request:
      request[addr_pair]=[1,ts]
    else:
      num = request[addr_pair][0]+1
      del request[addr_pair]
      request[addr_pair]=[num,ts]
    file=open("index.html", "w")
    file.write("<!DOCTYPE html> <html> <body><center><h1><font color=\"blue\" face=\"Georgia, Arial\" size=8><em>HA</em></font> Webpage Visit Results</h1></center>");
    for pair in request:
      if pair[0] == host:
        guest = "LOCAL: "+pair[0]
      else:
        guest = pair[0]
      if (time_now-datetime.strptime(request[pair][1],'%Y-%m-%d %H:%M:%S')).seconds < 3:
        file.write("<p style=\"font-size:150%\" >#"+ str(request[pair][1]) +": <font color=\"red\">"+str(request[pair][0])+ "</font> requests " + "from &lt<font color=\"blue\">"+guest+"</font>&gt to WebServer &lt<font color=\"blue\">"+pair[1]+"</font>&gt</p>")
      else:
        file.write("<p style=\"font-size:150%\" >#"+ str(request[pair][1]) +": <font color=\"maroon\">"+str(request[pair][0])+ "</font> requests " + "from &lt<font color=\"navy\">"+guest+"</font>&gt to WebServer &lt<font color=\"navy\">"+pair[1]+"</font>&gt</p>")
    file.write("</body> </html>");
    file.close()
    pickle.dump(request,open("pickle_data.txt","w"))
​
if __name__ == '__main__':
  try:
    ServerClass = BaseHTTPServer.HTTPServer
    Protocol = "HTTP/1.0"
    addr = len(sys.argv) < 2 and "0.0.0.0" or sys.argv[1]
    port = len(sys.argv) < 3 and 80 or int(sys.argv[2])
    HandlerClass.protocol_version = Protocol
    httpd = ServerClass((addr, port), HandlerClass)
    sa = httpd.socket.getsockname()
    print "Serving HTTP on", sa[0], "port", sa[1], "..."
    httpd.serve_forever()
  except:
    exit()

web目录下index.html文件是一个空文件,在程序启动之后会用到。

web目录下Dockerfile内容如下:

FROM python:2.7
WORKDIR /code
ADD . /code
EXPOSE 80
CMD python index.py

haproxy目录下的haproxy.cfg内容如下:

global
    log 127.0.0.1 local0
    log 127.0.0.1 local1 notice
​
defaults
    log global
    mode http
    option httplog
    option dontlognull
    timeout connect 5000ms
    timeout client 50000ms
    timeout server 50000ms
​
listen stats
    bind 0.0.0.0:70
    stats enable
    stats uri /
​
frontend balancer
    bind 0.0.0.0:80
    mode http
    default_backend web_backends
​
backend web_backends
    mode http
    option forwardfor
    balance roundrobin
    server weba weba:80 check
    server webb webb:80 check
    server webc webc:80 check
    option httpchk GET /
    http-check expect status 200

配置docker-compose

docker-compose.yml内容如下:

version: '3'
services:
  weba:
    build: ./web
    expose:
    - 80
  
  webb:
    build: ./web
    expose:
    - 80
​
  webc:
    build: ./web
    expose:
    - 80
​
  haproxy:
    image: haproxy:latest
    volumes:
    - ./haproxy:/haproxy-override
    - ./haproxy/haproxy.cfg:/usr/local/etc/haproxy/haproxy.cfg:ro
    links:
    - weba
    - webb
    - webc
    ports:
    - "80:80"
    - "70:70"
    expose:
    - "80"
    - "70"

启动docker-compose:

docker-compose up -d

通过验证,我们可以看到服务正常部署,访问http://127.0.0.1:80也可以完成服务的正常访问。但是在前面,我们讲过,weba和webb以及webc都是由同一个image创建而来,那么我们是否可以使用deploy配置项,直接配置三副本呢?

我们验证一下,修改docker-compose.yml内容如下:

version: '3'
services:
  web:
    build: ./web
    expose:
    - 80
    deploy:
      replicas: 3

  haproxy:
    image: haproxy:latest
    volumes:
    - ./haproxy:/haproxy-override
    - ./haproxy/haproxy.cfg:/usr/local/etc/haproxy/haproxy.cfg:ro
    external_links:
    - compose-haproxy-web_web_1
    - compose-haproxy-web_web_2
    - compose-haproxy-web_web_3
    ports:
    - "80:80"
    - "70:70"
    expose:
    - "80"
    - "70"

执行docker-compose up -d:

root@ubuntu:~/trainning/compose-haproxy-web# docker-compose up -d
WARNING: Some services (web) use the 'deploy' key, which will be ignored. Compose does not support 'deploy' configuration - use `docker stack deploy` to deploy to a swarm.
compose-haproxy-web_web_1 is up-to-date
compose-haproxy-web_haproxy_1 is up-to-date

上面提示deploy不被支持,然后web也只启动了一个。这是为什么呢?

因为deploy在使用的时候,有一些限制,但你的compose文件中出现如下配置项时,deploy就无法使用:

  • build

  • cgroup_parent

  • container_name

  • devices

  • tmpfs

  • external_links

  • links

  • network_mode

  • restart

  • security_opt

  • stop_signal

  • sysctls

  • userns_mode

使用Docker Compose一键部署zabbix

version: '3'
services:
  mysql-server:
    hostname: mysql-server
    container_name: mysql-server
    image: mysql:5.7
    network_mode: host
    volumes:
      - /data/mysql5721/data:/var/lib/mysql
    command: --character-set-server=utf8
    environment:
      MYSQL_ROOT_PASSWORD: 123456
      MYSQL_DATABASE: zabbix
      MYSQL_USER: zabbix
      MYSQL_PASSWORD: zabbix
​
  zabbix-web-nginx-mysql:
    hostname: zabbix-web-nginx-mysql
    container_name: zabbix-web-nginx-mysql
    image: zabbix/zabbix-web-nginx-mysql:alpine-3.4.11
    network_mode: host
    depends_on:
      - mysql-server
      - zabbix-server
    ports:
      - 80:80
    environment:
      DB_SERVER_HOST: 127.0.0.1
      MYSQL_DATABASE: zabbix
      MYSQL_USER: zabbix
      MYSQL_PASSWORD: zabbix
      MYSQL_ROOT_PASSWORD: 123456
      ZBX_SERVER_HOST: 127.0.0.1
      PHP_TZ: Asia/Shanghai
​
  zabbix-server:
    hostname: zabbix-server-mysql 
    container_name: zabbix-server-mysql 
    image: zabbix/zabbix-server-mysql:alpine-3.4.11
    depends_on:
      - mysql-server
    network_mode: host
    ports:
      - 10051:10051
    environment:
      DB_SERVER_HOST: 127.0.0.1
      MYSQL_DATABASE: zabbix
      MYSQL_USER: zabbix
      MYSQL_PASSWORD: zabbix
      MYSQL_ROOT_PASSWORD: 123456
​
  zabbix-agent:
    hostname: zabbix-agent
    container_name: zabbix-agent
    image: zabbix/zabbix-agent:alpine-3.4.11
    network_mode: host
    environment:
      ZBX_HOSTNAME: monitor
      ZBX_SERVER_HOST: 127.0.0.1

七、Docker镜像异常处理

推送镜像异常处理

从docker hub上获取到镜像,然后直接推送到私有harbor镜像仓库时抛出如下异常:

Note: You’re trying to push a manifest list/index which references multiple platform specific manifests, but not all of them are available locally or available to the remote repository.
Make sure you have all the referenced content and try again
​

原因是当我们使用docker pull 去拉取一个支持多平台的镜像,由于平台镜像的manifest文件包含多个平台,而pull默认只会拉取当前操作系统的平台,类似执行了docker pull --platform linux/amd64的操作。去push的时候,由于manifest上定义的其它平台的镜像找不到,所以报错。

整体的解决办法可以参考: You‘re trying to push a manifest list/index which references multiple platform specific manifests,_pulling platform-specific manifests and conta-CSDN博客

我这里有个取巧的办法,直接基于sha256只下载指定平台镜像如下:

docker pull nginx:1.25.2@sha256:b4af4f8b6470febf45dc10f564551af682a802eda1743055a7dfc8332dffa595

sha256的值可以在docker hub官方找到。

之后打tag:

docker tag nginx:1.25.2@sha256:b4af4f8b6470febf45dc10f564551af682a802eda1743055a7dfc8332dffa595 hub.wanjiedata.com/library/nginx:1.25.2-amd64

然后再推送即可。

构建镜像异常处理

在执行docker build时,抛出如下异常:

runc run failed: unable to start container process: exec: "/bin/sh": stat /bin/sh: no such file or directory

执行如下操作修复:

docker system prune -a

注意: 执行此操作要慎重,它会清理掉所有的docker数据,包括容器,镜像,卷等

八、Docker升级内核导致cgroup异常

在配置docker跨平台构建时,需要将内核升级至4.x以上,我直接升级到了如下版本:

5.4.217-1.el7.elrepo.x86_64

此时docker看着正常启动,但在kubernetes集群当中,显示该节点NotReady,在检查状态时,显示container runtime is down

进一步检查docker的日志:

Your kernel does not support cgroup blkio weight

大概google了下,需要手动去加载cgroup,在节点上执行下面这个指令,然后重启节点即可:

curl https://pi-ops.oss-cn-hangzhou.aliyuncs.com/scripts/cgroupfs-mount.sh | bash

为了防止链接失效,把脚本内容也放下面:

#!/bin/sh
# Copyright 2011 Canonical, Inc
#           2014 Tianon Gravi
# Author: Serge Hallyn <serge.hallyn@canonical.com>
#         Tianon Gravi <tianon@debian.org>
# https://github.com/tianon/cgroupfs-mount/blob/master/cgroupfs-mount
​
source /etc/profile
​
set -e
​
modprobe xt_cgroup
# for simplicity this script provides no flexibility
​
# if cgroup is mounted by fstab, don't run
# don't get too smart - bail on any uncommented entry with 'cgroup' in it
if grep -v '^#' /etc/fstab | grep -q cgroup; then
  echo 'cgroups mounted from fstab, not mounting /sys/fs/cgroup'
  exit 0
fi
​
# kernel provides cgroups?
if [ ! -e /proc/cgroups ]; then
  exit 0
fi
​
# if we don't even have the directory we need, something else must be wrong
if ! mountpoint -q /sys/fs/cgroup; then
  mount -t tmpfs -o uid=0,gid=0,mode=0755 cgroup /sys/fs/cgroup
fi
​
cd /sys/fs/cgroup
​
mkdir -p {systemd,"cpu,cpuacct","net_cls,net_prio",devices,memory,cpuset,blkio,perf_event,hugetlb,pids,freezer}
​
cat << EOF >/tmp/fstab_cgroup
cgroup /sys/fs/cgroup/systemd cgroup rw,nosuid,nodev,noexec,relatime,xattr,release_agent=/usr/lib/systemd/systemd-cgroups-agent,name=systemd 0 0
cgroup /sys/fs/cgroup/cpu,cpuacct cgroup rw,nosuid,nodev,noexec,relatime,cpu,cpuacct 0 0
cgroup /sys/fs/cgroup/net_cls,net_prio cgroup rw,nosuid,nodev,noexec,relatime,net_cls,net_prio 0 0
cgroup /sys/fs/cgroup/devices cgroup rw,nosuid,nodev,noexec,relatime,devices 0 0
cgroup /sys/fs/cgroup/memory cgroup rw,nosuid,nodev,noexec,relatime,memory 0 0
cgroup /sys/fs/cgroup/cpuset cgroup rw,nosuid,nodev,noexec,relatime,cpuset 0 0
cgroup /sys/fs/cgroup/blkio cgroup rw,nosuid,nodev,noexec,relatime,blkio 0 0
cgroup /sys/fs/cgroup/perf_event cgroup rw,nosuid,nodev,noexec,relatime,perf_event 0 0
cgroup /sys/fs/cgroup/hugetlb cgroup rw,nosuid,nodev,noexec,relatime,hugetlb 0 0
cgroup /sys/fs/cgroup/pids cgroup rw,nosuid,nodev,noexec,relatime,pids 0 0
cgroup /sys/fs/cgroup/freezer cgroup rw,nosuid,nodev,noexec,relatime,freezer 0 0
EOF
 
mount -a --fstab /tmp/fstab_cgroup
​
​
# get/mount list of enabled cgroup controllers
for sys in $(awk '!/^#/ { if ($4 == 1) print $1 }' /proc/cgroups); do
  mkdir -p $sys
  if ! mountpoint -q $sys; then
    if ! mount -n -t cgroup -o $sys cgroup $sys; then
      rmdir $sys || true
    fi
  fi
done
​
# example /proc/cgroups:
#  #subsys_name  hierarchy  num_cgroups  enabled
#  cpuset  2  3  1
#  cpu  3  3  1
#  cpuacct  4  3  1
#  memory  5  3  0
#  devices  6  3  1
#  freezer  7  3  1
#  blkio  8  3  1
​
​
# enable cgroups memory hierarchy, like systemd does (and lxc/docker desires)
# https://github.com/systemd/systemd/blob/v245/src/core/cgroup.c#L2983
# https://bugs.debian.org/940713
if [ -e /sys/fs/cgroup/memory/memory.use_hierarchy ]; then
  echo 1 > /sys/fs/cgroup/memory/memory.use_hierarchy
fi
​
exit 0

附录

参考:Cgroup子系统无法挂载 · Kubernetes 学习笔记 (huweihuang.com)

九、在docker镜像中使用gosu

概述

gosu类似linux中的su和sudo命令,用于在容器中使用普通用户启动进程;相对于su和sudo,在容器中使用gosu具有以下优势 :

gosu启动命令时只有一个进程,所以docker容器启动时使用gosu,那么该进程可以做到PID等于1;

sudo启动命令时先创建sudo进程,然后该进程作为父进程去创建子进程,1号PID被sudo进程占据;

相对于su和sudo,gosu更加轻量级;

gosu代码仓库地址: GitHub - tianon/gosu: Simple Go-based setuid+setgid+setgroups+exec

使用

1.在dockerfile中安装gosu

gosu基于go语言编写,没有额外依赖,安装极其简单,下面是一个在dockerfile中安装gosu的示例:

...
RUN set -ex;
    cpuArch="$(arch)"; \
    case ${cpuArch} in \
      aarch64) wget -O /usr/bin/gosu https://breezey-public.oss-cn-zhangjiakou.aliyuncs.com/softwares/linux/gosu/gosu-${GOSU_VERSION}-arm64;; \
      x86_64) wget -O /usr/bin/gosu https://breezey-public.oss-cn-zhangjiakou.aliyuncs.com/softwares/linux/gosu/gosu-${GOSU_VERSION}-amd64;; \
    esac; \
    chmod +x /usr/bin/gosu; \
    gosu --version; \
    gosu nobody true

2.在entrypoint中使用gosu

gosu一般在启动进程时,改变进程所使用的用户,所以一般都会用在entrypoint脚本当中,下面是redis官方镜像的entrypoint脚本使用示例:

#!/bin/sh
set -e
...
# 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 "$@"

3.使用demo

下面是一个完整的使用demo

dockerfile示例如下:

FROM ytx-tcr.tencentcloudcr.com/devops/ubuntu:20.04
​
ADD docker-entrypoint.sh /docker-entrypoint.sh
RUN set -ex; \
    groupadd -g 555 www; \
    useradd -g www -u 555 www; \
    mkdir -p /ytx-data/app-demo/data; \
    chown www.www -R /ytx-data/app-demo/data; \
    chmod +x /docker-entrypoint.sh; \
    wget -O /usr/bin/gosu https://breezey-public.oss-cn-zhangjiakou.aliyuncs.com/softwares/linux/gosu/gosu-1.14-amd64; \
    chmod +x /usr/bin/gosu;\
    gosu --version; \
    gosu nobody true
VOLUME /ytx-data/app-demo/data
ENTRYPOINT ["/docker-entrypoint.sh"]
CMD ["/bin/sh","-c","sleep 3600"]

docker-entrypoint.sh内容如下:

#!/bin/bash
​
touch /ytx-data/app-demo/data/111
​
chown www.www -R /ytx-data/app-demo/data
​
if [ "$(id -u)" == '0' ];then
    exec /usr/bin/gosu www "$0" "$@"
fi
​
exec "$@"

十、使用alpine作为docker基础镜像

概述

Alpine Linux 是一个社区开发的面向安全应用的轻量级Linux发行版,基于Musl libc和busybox实现的一个完整的操作系统,其具备以下特点:

  • 小巧:和busybox一样小巧,最小的Docker镜像只有5MB;

  • 安全:面向安全的轻量发行版;

  • 简单:提供APK包管理工具,软件的搜索、安装、删除、升级都非常方便;

由于小巧、功能完备,其非常适合作为容器的基础镜像。

由于小巧、功能完备,其非常适合作为容器的基础镜像。

使用

1.软件包管理

1.1 镜像源配置

alpine linux使用apk作为软件包管理器,国内镜像源如下:

清华TUNA镜像源:https://mirror.tuna.tsinghua.edu.cn/alpine/

中科大镜像源:http://mirrors.ustc.edu.cn/alpine/

阿里云镜像源:http://mirrors.aliyun.com/alpine/

这里以配置清华源为例:

sed -i 's/dl-cdn.alpinelinux.org/mirrors.tuna.tsinghua.edu.cn/g' /etc/apk/repositories

1.2 apk基本操作

apk是与yum和apt类似的软件包管理工具,其基本操作如下:

#更新软件
apk update
#搜索某个软件
apk search xxx
#安装软件
apk add xxx
#卸载软件
apk del xxx
#查看使用帮助
apk -h

需要说明的是alpine默认shell是/bin/sh,没有安装bash,可通过如下指令安装:

apk add bash

2.时区设置

#安装timezone
apk add -U tzdata
#查看时区列表
ls /usr/share/zoneinfo
#拷贝需要的时区文件到localtime
cp /usr/share/zoneinfo/Asia/Shanghai /etc/localtime
#查看当前时间
date
#为了精简镜像,可以将tzdata删除了
apk del tzdata

附录

参考:Alpine Linux使用入门 - 小z博客

十一、docker容器rootfs空间限制

目前docker的rootfs的空间限制只在xfs文件系统上生效,而且xfs文件系统还需要开启prjquota,配置/etc/fstab内容如下:

/dev/sdb1 /var/lib/docker xfs prjquota,defaults 0 0

执行挂载

mount -a 

之后修改docker的配置文件内容如下:

# vim /etc/docker/daemon.json
​
{
  "storage-driver": "overlay2",
  "storage-opts": [
    "overlay2.size=10G"
  ],
}

也可以在docker容器启动时,使用命令行参数指定:

docker run -d --storage-opt size=10G busybox:1.28 /bin/sh -c "sleep 3600"

如果docker的数据目录位于根分区,则要求根分区为xfs文件系统并开启prjquota,需要修改内核启动项:

vim /etc/default/grub
GRUB_CMDLINE_LINUX_DEFAULT="rootflags=pquota"
​
update-grub
reboot

检查是否正常开启了prjquota:

cat /proc/mounts |grep prjquota

十二、shell脚本检测容器是否正常运行

#!/bin/bash
#监控容器的运行状态
#容器名称  传入参数
containerName=$1
#当前时间
now=`date +"%Y-%m-%d %H:%M:%S"`
# 查看进程是否存在
exist=`docker inspect --format '{{.State.Running}}' ${containerName}`
if [ "${exist}" != "true" ]
then {
    echo "${now} 重启docker容器,容器名称:${containerName}" >> /opt/listen/logs/mysl.log
    docker start ${containerName}
}
else
{
    echo "${now} 容器名称:${containerName} 正常" >>/opt/listen/logs/mysl.log
}
fi

十三、Docker Registry

部署docker镜像仓库

快速部署一个docker registry:

docker run --name=registry-cache -d --restart=always --net host -v /etc/localtime:/etc/localtime -v /data/registry:/var/lib/registry -p 5000:5000   registry:2 

docker registry的配置文件路径为/etc/docker/registry/config.yml,示例配置如下:

version: 0.1
log:
  fields:
    service: registry
storage:
  cache:
    blobdescriptor: inmemory
  filesystem:
    rootdirectory: /var/lib/registry
http:
  addr: :5000
  headers:
    X-Content-Type-Options: [nosniff]
proxy:
  on: false
health:
  storagedriver:
    enabled: true
    interval: 10s
    threshold: 3

参考:https://cloud.tencent.com/developer/article/1047253

镜像加速器

启动docker registry为镜像加速器模式:

docker run --name=registry-cache -d --restart=always --net host -v /etc/lcoaltime:/etc/localtime -v /data/registry:/var/lib/registry -p 5000:5000  -e REGISTRY_PROXY_REMOTEURL=https://registry-1.docker.io registry:2 

修改docker配置文件,添加加速器:

{
        "registry-mirrors": [
                "https://hub.breezey.top"
        ]
}

十四、docker buildx远程构建

docker buildx具备了非常丰富的特性,其中一个有趣的特性在于 它支持设置不同的构建驱动,包括使用 docker-container , Kubernetes 和 remote。

这样做的好处在于,如果你本地的计算资源不足,那么你可以选择使用 Kubernetes 集群进行构建(副本数可以自行设置)也可以使用 remote 的 Docker engine 进行构建(但是 remote 就只是 1 个副本了)。

下面是一个使用kubernetes构建的示例:

docker buildx create \
  --bootstrap \
  --name=kube \
  --driver=kubernetes \
  --driver-opt=namespace=buildkit,replicas=4

十五、Docker配置TLS连接

生成证书

1.安装cfssl

2.生成ca证书

创建ca-config.json文件,内容如下:

{
    "signing": {
        "default": {
            "expiry": "175200h"
        },
        "profiles": {
            "docker": {
                "expiry": "175200h",
                "usages": [
                    "signing",
                    "key encipherment",
                    "server auth",
                    "client auth"
                ]
            }
        }
    }
}

创建ca-csr.json文件内容如下:

{
    "key": {
        "algo": "rsa",
        "size": 2048
    },
    "names": [
        {
            "C": "CN",
            "L": "Wuhan",
            "ST": "Hubei",
          "O": "k8s",
          "OU": "System"
        }
    ]
}

执行如下命令生成ca证书:

cfssl gencert --initca ca-csr.json | cfssljson --bare ca

3.生成docker服务端证书

创建docker-csr.json,内容如下:

{
  "CN": "docker-server",
  "hosts": [
    "localhost",
    "0.0.0.0",
    "127.0.0.1",
    "10.200.0.105"
  ],
  "key": {
    "algo": "rsa",
    "size": 4096
  },
  "names": [
    {
      "C": "CN",
      "L": "Wuhan",
      "O": "docker",
      "OU": "System",
      "ST": "Hubei"
    }
  ]
}

生成服务端证书:

cfssl gencert -ca=ca.pem -ca-key=ca-key.pem -config=ca-config.json -profile=docker docker-csr.json | cfssljson -bare docker

4.生成docker客户端证书

创建docker-client-csr.json文件,内容如下:

{
  "CN": "docker-client",
  "hosts": [
    ""
  ],
  "key": {
    "algo": "rsa",
    "size": 4096
  },
  "names": [
    {
      "C": "CN",
      "L": "Wuhan",
      "ST": "Hubei",
      "O": "docker",
      "OU": "System"
    }
  ]
}

生成客户端证书:

cfssl gencert -ca=ca.pem -ca-key=ca-key.pem -config=ca-config.json -profile=docker docker-client-csr.json | cfssljson -bare docker-client

配置docker

1.服务端配置

将生成的ca.pemdocker.pem以及docker-key.pem文件复制到/etc/docker目录下:

cp  ca.pem docker.pem docker-key.pem /etc/docker/ -f

找到docker的启动文件docker.service,修改ExecStart内容如下:

ExecStart=/usr/bin/dockerd -H fd:// -H tcp://10.200.0.105:2375 --tls --tlscacert=/etc/docker/ca.pem --tlscert=/etc/docker/docker.pem --tlskey=/etc/docker/docker-key.pem  --containerd=/run/containerd/containerd.sock

重启docker:

systemctl daemon-reload
systemctl restart docker

2.客户端访问

docker -H 10.200.0.105 --tls --tlscacert=/etc/docker/ca.pem --tlscert=/etc/docker/docker-client.pem --tlskey=/etc/docker/docker-client-key.pem ps -a

十六、Dockerfile优化

参考:关于Dockerfile的最佳实践技巧

十七、Docker处理僵尸进程

附录

参考: 在docker中出现的僵尸进程怎么处理_docker 僵尸进程清理-CSDN博客

十八、runlike和whaler工具

安装:

pip install runlike

参考: Docker小工具: runlike 和 whaler-CSDN博客

Docker
作者
Administrator
发表于
2026-02-02
License
CC BY-NC-SA 4.0