docker 容器技术使得开发测试非常方便,自 2013 年发布至今,一直广受瞩目。软件开发最大的麻烦事之一,就是环境配置。虚拟机虽然能够解决上面问题,但是有如下缺点:1.资源占用多,2.冗余步骤多,3. 启动慢;由于虚拟机存在这些缺点,Linux 发展出了另一种虚拟化技术:Linux 容器(Linux Containers,缩写为 LXC)。Linux 容器不是模拟一个完整的操作系统,而是对进程进行隔离。 或者说,在正常进程的外面套了一个保护层。对于容器里面的进程来说,它接触到的各种资源都是虚拟的,从而实现与底层系统的隔离。由于容器是进程级别的,相比虚拟机有很多优势。1. 启动快, 2, 资源占用少,3. 体积小. 总之,容器有点像轻量级的虚拟机,能够提供虚拟化的环境,但是成本开销小得多。Docker 属于 Linux 容器的一种封装,提供简单易用的容器使用接口。 它是目前最流行的 Linux 容器解决方案。下面介绍2020年08月以后,如何安装使用 docker,本篇以 Windows 10 wsl 2 Ubuntu-20.04 为例。

Docker 的主要用途,目前有三大类:

(1)提供一次性的环境。 比如,本地测试他人的软件、持续集成的时候提供单元测试和构建的环境;

(2)提供弹性的云服务。 因为 Docker 容器可以随开随关,很适合动态扩容和缩容;

(3)组建微服务架构。 通过多个容器,一台机器可以跑多个服务,因此在本机就可以模拟出微服务架构。

docker 安装

在线安装

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
sudo apt-get update
# 安装 apt 依赖包,用于通过HTTPS来获取仓库
sudo apt-get install \
apt-transport-https \
ca-certificates \
curl \
gnupg-agent \
software-properties-common
# 添加 Docker 的官方 GPG 密钥:
curl -fsSL https://mirrors.ustc.edu.cn/docker-ce/linux/ubuntu/gpg | sudo apt-key add -
# 验证秘钥
sudo apt-key fingerprint 0EBFCD88
sudo add-apt-repository \
"deb [arch=amd64] https://mirrors.ustc.edu.cn/docker-ce/linux/ubuntu/ \
$(lsb_release -cs) \
stable"

sudo apt update
sudo apt install docker-ce docker-ce-cli containerd.io
sudo service docker status
sudo service docker restart
# 把常用用户添加到 docker 组,这样运行 docker 命令时不需要添加 sudo,但需重启终端。注意,service 命令时仍然需要 sudo
# 添加指定用户
sudo usermod -a -G docker jinzhongxu
# 添加当前用户
sudo usermod -a -G docker $USER
# newgrp 命令用于在登录会话期间更改当前组 ID。用户的环境将被重新初始化
newgrp docker

# 测试运行 hello world 镜像
sudo docker run hello-world

# 验证是否安装成功。查看版本信息
docker version
# 或者
docker info

离线安装

除了在线安装外,有时候,离线安装也是很常见的。如内网服务器(不方便连接互联网)等。下面介绍在 Debian/Ubuntu 系统中安装的方法。

Debian 系统请访问 Docker 官网,并在 Install from a package 一节中,打开网址:https://download.docker.com/linux/debian/dists/ ,然后选择自己服务器版本,进入 pool/stable,选择 amd64, armhf, arm64, s390x 下载 .deb ,注意需要下载 4 个 .deb,分别是 docker-ce, docker-ce-cli, containerd.io, docker-compose-plugin.

1
2
3
4
5
6
7
# 下载必要的包
wget https://download.docker.com/linux/debian/dists/buster/pool/stable/amd64/docker-ce_20.10.9\~3-0\~debian-buster_amd64.deb
wget https://download.docker.com/linux/debian/dists/buster/pool/stable/amd64/docker-ce-cli_20.10.9\~3-0\~debian-buster_amd64.deb
wget https://download.docker.com/linux/debian/dists/buster/pool/stable/amd64/containerd.io_1.4.3-1_amd64.deb
wget https://download.docker.com/linux/debian/dists/buster/pool/stable/amd64/docker-compose-plugin_2.6.0\~debian-buster_amd64.deb
# 安装软件包
sudo dpkg -i *deb

Ubuntu 系统请访问 Docker 官网,并在 Install from a package 一节中,打开网址:https://download.docker.com/linux/ubuntu/dists/ ,然后选择自己服务器版本,进入 pool/stable,选择 amd64, armhf, arm64, s390x 下载 .deb ,注意需要下载 4 个 .deb,分别是 docker-ce, docker-ce-cli, containerd.io, docker-compose-plugin.

1
2
3
4
5
6
7
# 下载必要的包
wget https://download.docker.com/linux/ubuntu/dists/bionic/pool/stable/amd64/docker-ce_20.10.9~3-0~ubuntu-bionic_amd64.deb
wget https://download.docker.com/linux/ubuntu/dists/bionic/pool/stable/amd64/docker-ce-cli_20.10.9\~3-0\~ubuntu-bionic_amd64.deb
wget https://download.docker.com/linux/ubuntu/dists/bionic/pool/stable/amd64/containerd.io_1.4.6-1_amd64.deb
wget https://download.docker.com/linux/ubuntu/dists/bionic/pool/stable/amd64/docker-compose-plugin_2.6.0\~ubuntu-bionic_amd64.deb
# 安装软件包
sudo dpkg -i *deb
  • CentOS 等其他系统请访问 Docker 官网 获取安装方法。

docker-compose

docker compose 是用于定义和运行多容器 Docker 应用程序的工具。通过 Compose,您可以使用 YML 文件来配置应用程序需要的所有服务。然后,使用一个命令,就可以从 YML 文件配置中创建并启动所有服务。下载地址: github docker compose

Compose 使用的三个步骤:

  1. 使用 Dockerfile 定义应用程序的环境。
  2. 使用 docker-compose.yml 定义构成应用程序的服务,这样它们可以在隔离环境中一起运行。
  3. 最后,执行 docker-compose up 命令来启动并运行整个应用程序。

docker-compose 安装

1
2
3
4
5
wget https://github.com/docker/compose/releases/download/v2.10.2/docker-compose-linux-x86_64
sudo mv docker-compose-linux-x86_64 /usr/local/bin/docker-compose
sudo chmod +x /usr/local/bin/docker-compose
sudo ln -s /usr/local/bin/docker-compose /usr/bin/docker-compose
docker-compose -v

命令行容器自动生成 docker-compose.yml

使用 ghcr.io/red5d/docker-autocompose 来对命令行运行的容器生成 docker-compose.yml 文件,方便下次使用 docker-compose 运行。生成的配置文件包含非常丰富的参数,可以根据个人情况删除、更改。

1
2
3
4
5
# 对于单个容器
docker run --rm -v /var/run/docker.sock:/var/run/docker.sock ghcr.io/red5d/docker-autocompose registry > docker-compose.yml

# 对于多个容器,容器间的启动依赖需要手动增加
docker run --rm -v /var/run/docker.sock:/var/run/docker.sock ghcr.io/red5d/docker-autocompose redmine-redmine-1 redmine-mariadb-1 > docker-compose.yml

中文显示

在 docker 镜像中运行:

1
export LC_ALL="C.UTF-8"

此时,命令行可正常查看中文。在命令行设置,可对当前用户所有终端生效:

1
2
echo 'export LC_ALL="C.UTF-8"' >> ~/.bashrc
source ~/.bashrc

如下设置,可对全局生效,默认 root 用户

1
2
echo 'export LC_ALL="C.UTF-8"' >> /etc/profile
source /etc/profile

建议在创建容器时直接设置语言:

1
docker run -itd --name python -e LANG="C.UTF-8" ubuntu:python /bin/bash

时区

默认运行的容器时区都是 UTC,想要改为 CST,方法如下:

  1. 在构建镜像时,设置时区:
    1
    2
    ENV TZ=Asia/Shanghai
    RUN ...
  2. docker-compose 配置文件中,设置时区:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    # 方式一:
    environment:
    - SET_CONTAINER_TIMEZONE=true
    - CONTAINER_TIMEZONE=Asia/Shanghai

    # 方式二:
    environment:
    - TZ=Asia/Shanghai

  3. 在启动容器时,更改时区:
    1
    docker run -e TZ=Asia/Shanghai ...
  4. 对运行的容器更改时区:
    1
    2
    3
    4
    5
    6
    # 把宿主机的时区配置拷贝到容器中,假设我的容器名为 colab
    # /usr/share/zoneinfo/Asia/Shanghai -> ../PRC
    docker cp /usr/share/zoneinfo/PRC colab:/etc/localtime

    # 重启容器
    docker restart colab

查看时区:

1
date

如果上面方法不管用,那么请直接在容器中安装 tzdata:

1
2
3
4
5
# 安装时选择地区和时区
apt update && apt install tzdata -y

# 或者手动更改时区
dpkg-reconfigure tzdata

docker 中使用 GPU

想要在 docker 容器中使用宿主机的 GPU,除了宿主机配置 CUDA 环境和安装 docker 外,还需要安装如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
curl -s -L https://nvidia.github.io/nvidia-container-runtime/gpgkey | sudo apt-key add -

distribution=$(. /etc/os-release;echo $ID$VERSION_ID)

curl -s -L https://nvidia.github.io/nvidia-container-runtime/$distribution/nvidia-container-runtime.list | sudo tee /etc/apt/sources.list.d/nvidia-container-runtime.list

sudo apt update
sudo apt install nvidia-container-runtime nvidia-container-toolkit -y

sudo systemctl stop docker.service
sudo systemctl stop docker.socket

# 把运行时添加到 docker 中
# 或者把运行时添加到配置文件中,方法参见下一节 docker 使用: /etc/docker/daemon.json
dockerd --add-runtime=nvidia=/usr/bin/nvidia-container-runtime

sudo systemctl restart docker.service

另一种方法:

1
2
3
4
5
6
7
8
# Nvidia Docker
sudo apt install curl -y
distribution=$(. /etc/os-release;echo $ID$VERSION_ID)
curl -s -L https://nvidia.github.io/nvidia-docker/gpgkey | sudo apt-key add -
curl -s -L https://nvidia.github.io/nvidia-docker/$distribution/nvidia-docker.list | sudo tee /etc/apt/sources.list.d/nvidia-docker.list

sudo apt update && sudo apt install -y nvidia-container-toolkit
sudo systemctl restart docker

当安装完 nvidia-container-toolkit 后,运行时有时会出现:nvidia-container-cli: ldcache error: open failed: /sbin/ldconfig.real: no such file or directory: unknown.,解决方法:

1
2
# 以 root 身份执行
ln -s /sbin/ldconfig /sbin/ldconfig.real

创建容器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# 以镜像 ubuntu-xjz:v1 或 {IMAGE ID} 创建容器 ubuntu-xjz-python
# 映射容器端口 80 到主机端口 9092
# 使用多GPU训练深度学习模型,请添加 --shm-size 8G
# 把主机的目录 /home/jinzhongxu/Documents 挂载到容器目录 /data
# 不存在挂载点则自动创建
docker run -itd --name 'ubuntu-xjz-python' -p 9092:80 --gpus=all --shm-size 8G -v /home/jinzhongxu/Documents:/data ubuntu-xjz:v1 /bin/bash
# or
docker run -itd --name 'ubuntu-xjz-python' -p 9092:80 --gpus all --shm-size 8G -v /home/jinzhongxu/Documents:/data ubuntu-xjz:v1 /bin/bash

# 指定特定 GPU,如 device=1 表示第 2 个 GPU,注意编号从 0 开始,对应 nvidia-smi -L 列出的编号
docker run -itd --name 'ubuntu-xjz-python' -p 9092:80 --gpus "device=1" --shm-size 8G -v /home/jinzhongxu/Documents:/data ubuntu-xjz:v1 /bin/bash

# 指定两个 GPU,如第 2,3 个 GPU
docker run -itd --name 'ubuntu-xjz-python' -p 9092:80 --gpus '"device=1,2"' --shm-size 8G -v /home/jinzhongxu/Documents:/data ubuntu-xjz:v1 /bin/bash

# 指定前两个 GPU
docker run -itd --name 'ubuntu-xjz-python' -p 9092:80 --gpus 2 --shm-size 8G -v /home/jinzhongxu/Documents:/data ubuntu-xjz:v1 /bin/bash

测试在 docker 容器中是否可以使用 GPU

1
2
3
4
5
6
7
8
9
10
# nvidia/cuda:10.2-base 根据驱动版本进行选择,如 nvidia/cuda:11.0-base
docker run --gpus all --rm nvidia/cuda:10.2-base nvidia-smi
# or 只罗列 GPU 名称
docker run --gpus all --rm nvidia/cuda:10.2-base nvidia-smi -L

# 或者使用 runtime
docker run --runtime=nvidia -e NVIDIA_VISIBLE_DEVICES=all --shm-size 8G --rm nvidia/cuda:10.2-base nvidia-smi

# 指定 GPU,注意编号从 0 开始,对应 nvidia-smi -L 列出的编号
docker run --runtime=nvidia -e NVIDIA_VISIBLE_DEVICES=1,2 --shm-size 8G --rm nvidia/cuda:10.2-base nvidia-smi

打印出 GPU 信息说明验证成功,--gpus-e NVIDIA_VISIBLE_DEVICES 都能实现指定 GPU,但后者 api 接口更容易。更多请访问 NVIDIA 官网教程.

docker 使用

因 docker hub 在国外,为了加速访问需要给 docker 添加国内镜像,方法如下,打开如下文件,没有则自动创建:

1
sudo vim /etc/docker/daemon.json

添加如下内容

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
{
"registry-mirrors": ["https://docker.mirrors.ustc.edu.cn",
"https://registry.docker-cn.com",
"http://hub-mirror.c.163.com",
"https://mirror.baidubce.com"],
"insecure-registries": ["192.168.199.100:5000"],
"debug": true,
"experimental": false,
"runtimes": {
"nvidia": {
"path": "nvidia-container-runtime",
"runtimeArgs": []
}
}
}

注意:

  1. insecure-registries 为你本地网络私有仓库地址,根据个人情况设置;
  2. runtimes 为容器调用宿主机服务器 GPU 运行时,根据个人情况设置。

测试

1
docker pull tensorflow/tensorflow:latest

image

Docker 把应用程序及其依赖,打包在 image 文件里面。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
# 从 docker 官方拉取 image 文件 hello-world
docker image pull hello-world
# 运行这个 image 文件。该命令具有自动抓取 image 文件的功能。如果发现本地没有指定的 image 文件,就会从仓库自动抓取。因此,前面的 docker image pull 命令并不是必需的步骤。
docker container run hello-world
# 查看下载到本地的镜像文件
docker image ls
# 为本地的 image 标注用户名和版本。username 为 hub.docker.com 或 cloud.docker.com 注册的账户
docker image tag (IMAGE ID) (username)/(repository):(tag)
# 也可以不标注用户名,重新构建一下 image 文件。
docker image build -t (username)/(repository):(tag) .
# 发布 image 文件。
docker image push (username)/(repository):(tag)
# 删除不需要的本地镜像文件
docker image rm (IMAGE ID)

# 查看虚悬镜像(仓库名和标签都是none)的镜像,一般是无用或错误的镜像
docker image ls -f dangling=true
# 删除所有虚悬镜像
docker image prune

# 查看镜像名以registr开头的镜像
docker images -f reference="registr*"

# 更多请允许命令
docker image --help

container

image 文件生成的容器实例,本身也是一个文件,称为容器 (Container) 文件。 也就是说,一旦容器生成,就会同时存在两个文件: image 文件和容器文件。而且关闭容器并不会删除容器文件,只是容器停止运行而已。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
# 查看运行的容器
docker ps
docker container ls
# 只查看容器ID
docker ps -q
# 查看基于镜像ubuntu启动的所有容器
docker ps -f ancestor=ubuntu
# 查看容器名是myubuntu的容器,这里用*匹配所有字符
docker ps -f name="myubunt*"
# 查看退出的容器
docker ps -a -f status=exited
# 查看所有运行过的容器,包括正在运行的和停止运行的
docker ps -a
docker container ls -all
# 把运行的容器关闭. 相当于向容器里面的主进程发出 SIGTERM 信号,然后过一段时间再发出 SIGKILL 信号。
docker stop {CONTAINER ID}
docker container stop {CONTAINER ID}
# 把运行的容器关闭. 相当于向容器里面的主进程发出 SIGKILL 信号
docker kill {CONTAINER ID}
docker container kill {CONTAINER ID}
# 把停止运行的容器打开
docker start {CONTAINER ID}
docker container start {CONTAINER ID}
# 查看 docker 容器的输出,即容器里面 Shell 的标准输出。
docker container logs {CONTAINER ID}
# 从正在运行的 Docker 容器里面,将文件拷贝到本机。下面是拷贝到当前目录的写法。
docker container cp {CONTAINER ID}:{/path/to/file} .
# 进入运行中的ubuntu容器
# 推荐大家使用 docker exec 命令,因为此退出容器终端,不会导致容器的停止。
# 使用 docker attach {CONTAINER ID},当退出容器终端时,将导致容器停止。
docker exec -it {CONTAINER ID} /bin/bash
# 进入容器时指定登陆用户
docker exec -it -u root {CONTAINER ID} /bin/bash
# 拷贝 docker 容器中的文件,注意此时不要求容器为启动状态
docker cp {CONTAINER ID}:/opt/bitnami/redmine/config/settings.yml settings.yml
# 以后台(-d)方式打开新的容器并命名,-t 表示分配伪终端tty,-i 表示以交互模式打开,-p 表示把本机端口8000映射到容器端口80
docker run -itd --name='centos' -p 8000:80 {IMAGE ID} /bin/bash

# 使容器不联网
docker run --network none {IMAGE ID} /bin/bash

# 复用卷挂载
docker run --name myubuntu -v /tmp:/tmp ubuntu:22.04
docker run --name hello --volumes-from myubuntu ubuntu:22.04

# 删除不需要的容器
docker container rm {CONTAINER ID}
# 把容器转存镜像
# -a 提交的镜像作者;-m 说明文字;cv-ubuntu 镜像名;v1 镜像标签
docker commit -a "jinzhongxu" -m "my new cv image" {CONTAINER ID} cv-ubuntu:v1

# 删除所有的容器 -f 表示 force 强制删除 -q 表示只列出容器ID
docker container rm -f ${docker ps -aq}

# 更多请允许如下命令
docker container --help

注意:

  1. CONTAINER ID 和 IMAGE ID 只需要输入前几个字符,只要能够唯一识别它们;
  2. docker image 根据 image 文件生成容器的实例。同一个 image 文件,可以生成多个同时运行的容器实例。image 文件可以看作是容器的模板;
  3. image 是二进制文件。实际开发中,一个 image 文件往往通过继承另一个 image 文件,加上一些个性化设置而生成。举例来说,你可以在 Ubuntu 的 image 基础上,往里面加入 Apache 服务器,形成你的 image;
  4. 一般来说,为了节省时间,我们应该尽量使用别人制作好的 image 文件,而不是自己制作。即使要定制,也应该基于别人的 image 文件进行加工,而不是从零开始制作;

docker container 设置自启动一般推荐使用 always 参数:–restart=always,更多参数取值如下:

1
2
3
4
5
6
7
8
9
--restart
no
默认策略,在容器退出时不重启容器
on-failure
在容器非正常退出时(退出状态非0),才会重启容器
on-failure:3
在容器非正常退出时重启容器,最多重启3次
always
在容器退出时总是重启容器

具体的开启自启和取消自启的命令如下:

1
2
3
4
# 开启自启
docker update --restart=always {CONTAINER ID}
# 取消自启
docker update --restart=no {CONTAINER ID}

制作 docker image

需要用到 Dockerfile 文件。它是一个文本文件,用来配置 image. Docker 根据 该文件生成二进制的 image 文件。

首先,克隆项目

1
2
git clone https://github.com/xxx/demos.git
cd demos

在项目的根目录下,新建一个文本文件 .dockerignore, 把要排除的文件或文件夹路径按行写到文件里,参考如下,即说明那些不需要打包进入 image 文件。如果你没有路径要排除,这个文件可以不新建。

1
2
3
.git
node_modules
npm-debug.log

在项目的根目录下,新建一个文本文件 Dockerfile,

1
2
3
4
5
6
FROM node:11
COPY . /app
WORKDIR /app
RUN npm install --registry=https://registry.npm.taobao.org
EXPOSE 3000
CMD node demos/01.js

说明如下:

1
2
3
4
5
6
FROM node:11:该 image 文件继承官方的 node image,冒号表示标签,这里标签是 11,即 11 版本的 node。
COPY . /app:将当前目录下的所有文件(除了.dockerignore 排除的路径),都拷贝进入 image 文件的/app目录。
WORKDIR /app:指定接下来的工作路径为 /app。
RUN npm install:在/app目录下,运行 npm install 命令安装依赖。注意,安装后所有的依赖,都将打包进入 image 文件。
EXPOSE 3000:将容器 3000 端口暴露出来, 允许外部连接这个端口。
CMD node demos/01.js:表示容器启动后自动执行 node demos/01.js。RUN命令与CMD命令的区别在哪里?简单说,RUN命令在 image 文件的构建阶段执行,执行结果都会打包进入 image 文件;CMD命令则是在容器启动后执行。另外,一个 Dockerfile 可以包含多个 RUN 命令,但是只能有一个 CMD 命令。注意,指定了 CMD 命令以后,docker container run 命令就不能附加命令了(比如前面的 /bin/bash),否则它会覆盖 CMD 命令。

创建 image 文件. 有了 Dockerfile 文件以后,就可以使用 docker image build命令创建 image 文件了。

1
2
3
docker image build -t demo .
# 或者
docker image build -t demo:0.0.1 .

上面代码中,-t参数用来指定 image 文件的名字,后面还可以用冒号指定标签。如果不指定,默认的标签就是 latest. 最后的那个点表示 Dockerfile 文件所在的路径,上例是当前路径,所以是一个点。

如果运行成功,就可以看到新生成的 image 文件 demo了。

1
docker image ls

发布 image 的命令参考上面。

常见保留字

  • FROM: FROME ubuntu:22.04 构建新镜像基于的基础镜像。Dockerfile中第一条必须是 FROM
  • MAINTAINER: MAINTAINER xjz xjz@gmail.com 镜像维护者姓名和邮箱
  • RUN: RUN apt install vim -y 容器构建时(docker build)执行的命令,支持两种格式,一种是shell(等同于终端中执行的SHELL命令 RUN apt install vim -y),一种是exec命令 RUN ["./test.php", "dev", "offline"],后两个为参数,等同于 RUN ./test.php dev offline
  • ENV: ENV MYPATH /usr/local 配置环境变量,指定 export MYPATH=/usr/local
  • WORKDIR: WORKDIR /usr/local 指定在创建容器后,终端默认登录的工作目录
  • USER: USER root 指定该镜像以什么用户执行,不指定,默认为 root
  • VOLUME: VOLUME ["/tmp", "/tmp"] 作用同 -v /tmp:/tmp,指定挂载目录
  • ADD: ADD java.tar.gz /usr/local/java 把本地压缩包拷贝到镜像,并会自动解压缩tar压缩包。添加时并重命名 ADD docker_boot-0.0.1-SNAPSHOT.jar hello_docker.jar
  • COPY: COPY java /usr/local/java 把本地文件夹拷贝到镜像,不解压
  • EXPOSE: EXPOSE 80 启动容器是暴露的端口
  • CMD: CMD ./start-jupyter.sh or CMD ["./start-jupyter.sh", "run"] 容器启动命令,也支持shell命令和exec命令。指定容器启动后要做的事情。Dockerfile中可以有多个CMD指令,但只有最后一个生效,CMD会被docker run之后的参数替换
  • ENTRYPOINT: ENTRYPOINT ["/bin/ping", "-c", "3"] 也是用来指定一个容器启动时要运行的命令,类似于CMD,但是ENTRYPOINT不会被docker run后面的命令覆盖,如果docker run后面的命令行参数会被当作参数送给ENTRYPOINT指令指定的程序。其实ENTRYPOINT也可以被覆盖:docker run --entrypoint hostname ubuntu:22.04。有些命令运行后就退出了,如 /etc/init.d/ssh start,有些命令会一直运行,如 /bin/bash。如果想要容器启动后一直运行应该选择常驻(一直运行)的命令作为ENTRYPOINT或CMD,如上面的 /bin/bash,或者最后增加上常驻命令。

ENTRYPOINT 和 CMD 搭配使用,ENTRYPOINT 用作执行命令,CMD用作传参数例子:

1
2
3
# 只有ENTRYPOINT和CMD都用Exec表示法, 才能得到预期的效果
ENTRYPPOINT ["/bin/ping", "-c", "3"]
CMD ["localhost"]

执行时,可以在docker run后指定新参数覆盖CMD参数:

1
docker run -it myubuntu:v1.0 www.baidu.com

docker network

docker 服务默认会创建一个docker0网桥(其上有一个docker0内部接口),该网桥网络的名称为docker0,它在内核层联通了其他的物理或虚拟网卡,这就将所有容器和本地主机都放到同一个物理网络。docker默认指定docker0接口的IP地址和子网掩码,让主机和容器之间可以通过网桥相互图像。

docker network 分为 bridge, host, none, container 和自建网络。

  • bridge: 为每一个容器分配、设置IP等,并将容器连接到一个docker0。即虚拟网桥模式,默认为该模式docker run ubuntu。网桥docker0创建一对对等虚拟设备接口,一个叫veth(宿主机docker0),一个叫eth0(容器内),两者一一匹配。整个宿主机的网桥模式都是docker0,类似一个交换机有一堆接口,每个接口都叫做veth,在本地主机和容器内创建一个虚拟接口,并让他们彼此联通,这样一对接口叫做veth pari.
  • host: 容器将不会虚拟出自己的网卡,配置自己的IP等,而是直接使用宿主机的IP和端口与外界通信,不在需要额外进行NAT转换。与宿主机公用一个network namespace,容器不会虚拟出自己的网卡:docker run --network host ubuntu
  • none: 容器有独立的 Network namespace,但并没有对其进行任何网络设置,如分配veth pair和网桥连接、IP等,不常用:docker run --network none ubuntu
  • container: 新创建的容器不会创建自己的网卡和配置自己的IP,而是和一个指定的容器共享IP、端口范围等:docker run --network container:容器名或者容器ID。当指定的共享容器关闭时,当前的容器网络也会断开,只会存在本地回环127.0.0.1。
  • 自建网络:docker network create mybridge,使用自建网络:docker run --network mybridge ubuntu,使用自建网络的好处是除了使用可变的IP互相PING通或连接通外,还可以使用容器名进行PING通或连接上。当容器IP发生改变不会影响使用容器名PING通。建议使用这种方式。

使用bridge模式当容器关闭后重启可能会导致容器IP发生改变,原IP会分配给关闭期间新创建的容器。需要注意。

容器导出和导入镜像

本节演示从拉取基镜像,然后增加个人内容,从容器导出镜像文件(Export a container’s filesystem as a tar archive,不建议这样导出镜像)。把导出的镜像文件拷贝到新的电脑上,然后再导入使用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
# 拉取最新 Ubuntu 镜像
docker pull ubuntu:latest

# 查看镜像
docker images
#REPOSITORY TAG IMAGE ID CREATED SIZE
#ubuntu latest df5de72bdb3b 10 days ago 77.8MB

# 运行镜像 ubuntu
docker run -itd --name='ubuntu-base' df5de72bdb3b /bin/bash

# 查看运行的镜像
docker ps
#CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
#d090dfda0f04 df5d "/bin/bash" 4 seconds ago Up 2 seconds ubuntu-base

# 进入镜像并进行操作
docker exec -it d090 /bin/bash

# 导出容器到压缩文件中
docker export d090dfda0f04 > myubuntu.tar
# or
docker export -o myubuntu.tar d090dfda0f04
# 等价于
docker container export -o myubuntu.tar d090dfda0f04

# 把压缩文件导入成新镜像
# myubuntu.tar 为压缩文件
# my/ubuntu 为导入后的镜像仓库源REPOSITORY
# v1 为导入后的镜像标签tag
cat myubuntu.tar | docker import - my/ubuntu:v1
# or
docker import myubuntu.tar my/ubuntu:v1
# 等价于
docker image import myubuntu.tar my/ubuntu:v1

# 查看导入的镜像
docker images
#REPOSITORY TAG IMAGE ID CREATED SIZE
#my/ubuntu v1 6c5e97787cfc 3 seconds ago 77.8MB

# 运行新镜像
docker run -itd --name='ubuntu-personal' 6c5e97787cfc /bin/bash

# 查看运行的容器
docker ps
#CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
#cf6f1be73bb1 6c5e97787cfc "/bin/bash" About a minute ago Up About a minute ubuntu-personal
#d090dfda0f04 df5d "/bin/bash" 10 minutes ago Up 10 minutes ubuntu-base

上面的方法其实是将容器的文件系统导出为 tar 存档,也可以先把容器打标签成本地镜像,然后用下面的本地镜像导出和导入镜像到其他主机使用。容器打标签成镜像方法如下:

1
2
3
# 把容器转存镜像
# -a 提交的镜像作者;-m 说明文字
docker commit -a "jinzhongxu" -m "my new image" {CONTAINER ID} {IMAGE NAME}:{TAG}

镜像导出和导入镜像

同容器导出和导入镜像,主要区别是,使用的命令从 export/import 分别变为 save/load.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
# 查看镜像
docker images
#REPOSITORY TAG IMAGE ID CREATED SIZE
#person/ubuntu v1 e42208a4c3ce 13 minutes ago 77.8MB

# 导出镜像
docker save -o person.tar e42208a4c3ce
# or
docker save > person.tar e42208a4c3ce
# 等价于
docker image save -o person.tar e42208a4c3ce

# 再新的服务器上导入镜像压缩文件
docker load -i person.tar
# 等价于
docker image load -i person.tar
#b4b3a9fb5a5c: Loading layer [==================================================>] 80.36MB/80.36MB
#Loaded image ID: sha256:e42208a4c3ce49fdba41f504990f85554429a20a03d69a188d8efe3aef0d97b4

# 查看镜像
docker images
#REPOSITORY TAG IMAGE ID CREATED SIZE
#<none> <none> e42208a4c3ce 16 minutes ago 77.8MB

# 给镜像打标签
docker tag e42208a4c3ce person/ubuntu:v1

# 再次查看镜像
docker images
#REPOSITORY TAG IMAGE ID CREATED SIZE
#person/ubuntu v1 e42208a4c3ce 17 minutes ago 77.8MB

镜像和容器导入导出的区别

  1. 镜像导入是一个复制的过程,容器导入是将当前容器变成一个新的镜像;
  2. docker save 命令保存的是镜像(image),docker export 命令保存的是容器(container);
  3. export 命令导出的 tar 文件略小于 save 命令导出的;
  4. 因为 export 导出的是容器,export 导出的文件在 import 导入时,无法保留镜像所有的历史(即每一层 layer 信息),不能进行回滚操作。而 save 是根据镜像来的,所以导入时可以完整保留下每一层 layer 信息;
  5. docker load 不能对导入的镜像重命名,而 docker import 导入可以为镜像指定新名称。

注意:<font color=red> 测试发现 docker 20.10.9 运行 ubuntu22.04 出现异常。</font>
问题描述:
外网 docker 20.10.18 基于 ubuntu22.04 安装 基于 miniconda 的 python 环境,并安装支持 GPU 的 torch 和 torchvision,测试能够在 python 中调用服务器 GPU。导出该镜像并刻录到内网,内网部署 docker 20.10.9,加载镜像后能够启动,但 python 无法正常使用,出现 ipython (Original error was: PyCapsule_Import could not import module “datetime” 和 could not start threads 错误)和 numpy (import numpy 出现 OpenBLAS blas_thread_init: pthread_create failed for thread 1 of 999: Operation not permitted OpenBLAS blas_thread_init: RLIMIT_NPROC -1 current, -1 max)。但把镜像拷贝到其他一台能够连接外网的服务器(安装有 docker 20.10.11)上时,能够正常运行。
问题排查:
经查发现是 ubuntu21.10 和 fedora35 开始使用glibc2.34甚至更高的版本。在glibc2.34 版本里面,开始使用一个名为 clone3 的系统调用。通常情况下,容器里面所有的系统调用都会被 docker 捕获,然后 docker 决定如何处理它们。如果 docker 中没有为特定系统调用指定策略,则默认的策略会通知容器这边”Permission Denied”。但是,如果 Glibc 收到此错误,它不会回退。它仅在收到响应“此系统调用不可用”时才执行此操作。
解决方法:

  1. 运行容器的时候,加上这个参数来绕过docker系统调用限制

    1
    2
    3
    4
    --security-opt seccomp=unconfined

    # 例子
    docker run -itd --name gputest --gpus all -p 9090:80 -v /workspace:/data --security-opt seccomp=unconfined ubuntu22-gpu:v1 /bin/bash

    这能够使得上面的 python 运行但会有很大的问题,一个是你的容器将变得不安全,另一个是这些参数在构建镜像的时候是不可用的。

  2. 将 docker 升级到 20.10.11 以上的版本

  3. 不要使用基础镜像 ubuntu22.04,而是使用 ubuntu18.04

容器端口映射

有时候在我们运行的容器中有一些服务需要外部网络(docker 容器外)访问,此时就需要在我们从镜像运行容器时指定好本机端口和容器端口的映射,方便在本机直接访问容器中的服务。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 把容器的80端口映射到主机的9000端口,默认是“0.0.0.0”,即所有能访问本机的设备都可以访问
docker run -itd --name "ubuntu-python" -p 9000:80 {IMAGE ID}

# 把容器的80端口映射到主机的9000端口,设置为“127.0.0.1”,即只有本机可以访问
docker run -itd --name "ubuntu-python" -p 127.0.0.1:9000:80 {IMAGE ID}

# 把容器的80端口映射到主机的随机端口
docker run -itd --name "ubuntu-python" -p 127.0.0.1::80 {IMAGE ID}
# 可以通过如下命令查看映射的本机端口
docker ps
docker port {CONTAINER ID}

# 如果有多个端口映射
# 把容器的80端口映射到主机的9000端口,把容器的8080端口映射到主机的9090端口
docker run -itd --name "ubuntu-python" -p 9000:80 -p 9090:8080 {IMAGE ID}

给运行的容器新添加端口映射等其他涉及到非 CPU、内存的操作

对于运行的容器,更改CPU和内存、设置随docker自动开启等都可以使用 docker update 来完成,但是其他涉及到修改容器的端口、挂载点、GPU等需要停止docker服务,然后修改对应容易的配置文件才能完成。关于更新运行容器的CPU和内存等方法可以查看帮助:

1
docker update --help

目前 docker 没有直接为运行的容器动态增加端口映射的方法。建议从镜像运行容器时考虑清楚哪些端口需要进行映射。如果真的需要为运行的容器再增加新端口映射,可以考虑如下方法:

重新运行容器

最简单的方法就是直接重新再运行容器。如果已经运行的容器个人增加了一些不方便拷贝出来的内容,那么可以先将该容器 commit 成镜像,然后再从新镜像运行容器,增加新端口映射。
容器 commit 镜像的方法:

1
2
3
# 把容器转存镜像
# -a 提交的镜像作者;-m 说明文字;cv-ubuntu 镜像名;v1 镜像标签
docker commit -a "jinzhongxu" -m "my new cv image" {CONTAINER ID} cv-ubuntu:v1

修改容器配置文件

如果运行的容器体量太多(如几十G),上面的方法就显得有些不太合适,这里提供一种方法就是修改运行容器的配置文件。

值得一提的是,当对容器的 CPU 和内存修改,可以直接使用命令 docker update --help。除此之外,如容器修改挂载点、修改端口映射等需要修改配置文件。查看配置文件前需要先查看 docker 容器存放的位置:docker info | grep "Root" 和需要修改的容器 ID,使用 docker ps 查看。

修改容器配置文件前,需要先暂时关闭 docker 服务:

1
sudo systemctl stop docker

然后修改配置文件 hostcongfig.jsonconfig.v2.json(如果该文件中有记录)。

查看容器配置信息

1
docker inspect {CONTAINER ID}

进入配置文件目录,修改配置文件

1
2
3
4
5
6
7
# 以 Ubuntu 为例
cd /var/lib/docker/containers

# 然后进入以容器id开头的长名目录
# 修改里面的 PortBindings
vim hostconfig.json
vim config.v2.json

修改成功后,然后重启 docker 服务。

挂载宿主机目录到容器

有时候需要容器能够访问宿主机的某些文件或文件夹,此时,我们可以从镜像创建容器时通过设置挂载点的方式,将宿主机的目录挂载到容器的某个目录,使得容器内部就可以访问宿主机的文件夹。注意,与上面的 cp 命令的区别。

1
2
3
4
5
6
7
8
# 把宿主机的目录 /home/jinzhongxu/Documents 挂载到容器的 /data 目录
# 注意镜像中如果没有挂载点目录 /data,则会自动创建
# ubuntu:latest 为镜像名,也可以用镜像 ID
# -v 参数将宿主机的 /home/jinzhongxu/Documents 挂载到容器的 /data
docker run -itd --name 'ubuntu-test' -v /home/jinzhongxu/Documents:/data ubuntu:latest /bin/bash

# 默认挂载的路径权限是读写 rw,可在挂载时自定义权限,如只读 ro
docker run -itd --name 'ubuntu-test' -v /home/jinzhongxu/Documents:/data:ro ubuntu:latest /bin/bash

有时候,我们需要创建一个新容器,挂载信息同已经创建的一个容器,那么我们可以使用如下方法:

1
2
3
4
# 使用同上面的容器 'ubuntu-test' 相同的挂载信息
# 将宿主机的 /home/jinzhongxu/Documents 挂载到容器的 /data
# ubuntu:latest 为镜像名,也可以用镜像 ID
docker run -itd --name 'ubuntu-test2' --volumes-from ubuntu-test ubuntu:latest /bin/bash

此时,创建的新容器 ‘ubuntu-test2’ 同旧容器 ‘ubuntu-test’ 具有相同的挂载信息。

容器 SSH 访问宿主机

从容器内部通过SSH访问宿主机能够为容器和宿主机提供更好的交互方式。此时,我们只需要知道宿主机的 docker0 IP,以及宿主机用户名和密码即可。

查看宿主机 docker0 IP 方法是,在宿主机上执行 ifconfig 命令,查看 docker0 对应的 IP,一般为 172.17.0.1;

通过如下方式访问宿主机:

1
2
3
4
# 我这里用户名是 jinzhongxu
ssh jinzhongxu@172.17.0.1
# 然后输入用户名 jinzhongxu 在宿主机上的密码即可
# 注意,需要宿主机打开密码认证登录(/etc/ssh/sshd_config, PasswordAuthentication yes)

本地私有仓库 docker hub

有时候使用 Docker Hub 这样的公共仓库可能不方便,如内网环境下。此时,用户可以创建一个本地仓库供私人使用。

docker-registry 是官方提供的工具,可以用于构建私有的镜像仓库。本文内容基于 docker-registry v2.x 版本。

安装 docker-registry

如果在能联网的服务器上:

1
2
3
4
5
# 自动从官网下载 registry 镜像并允许
# 把本地 5000 映射到容器 5000 端口
# 会自动创建本地目录 /opt/data/registry,以后本地镜像 push 到该目录
# --restart=always 表示重启电脑时,自动运行该容器
docker run -d -p 5000:5000 --restart=always -v /opt/data/registry:/var/lib/registry --name registry registry

如果想在内网服务器使用,需要现在能够连接互联网的机器上下载 registry 镜像,然后把该镜像导出,刻录到内网,然后在内网服务器上加载镜像并打标签为 registry:latest(以个人喜好,我这里以此名和标签做演示)。然后使用下面的方式运行。

1
docker run -d --restart=always --name registry -p 5000:5000 -v /opt/data/registry:/var/lib/registry registry:latest

此时,registry 容器在运行,并监控 5000 端口。

在私有仓库上传、搜索、下载镜像

创建好私有仓库之后,就可以使用 docker tag 来标记一个镜像,然后推送它到仓库。例如私有仓库地址为 127.0.0.1:5000,把镜像打标签,以 127.0.0.1:5000 开头:

1
docker tag ubuntu:latest 127.0.0.1:5000/ubuntu18-gpu:v1

提交镜像

1
docker push 127.0.0.1:5000/ubuntu18-gpu:v1

搜索私有仓库中的镜像

1
curl 127.0.0.1:5000/v2/_catalog

结果大概这样

1
{"repositories":["ubuntu18-gpu"]}

从仓库中获取指定镜像的标签

1
curl 127.0.0.1:5000/v2/ubuntu18-gpu/tags/list

结果大概这样

1
{"name":"ubuntu18-gpu","tags":["v1"]}

从私有仓库下载镜像

1
docker pull 127.0.0.1:5000/ubuntu18-gpu:v1

配置非 https 仓库地址

如果你不想使用 127.0.0.1:5000 作为仓库地址,比如想让本网段的其他主机也能把镜像推送到私有仓库。你就得把例如 192.168.199.100:5000 这样的内网地址作为私有仓库地址,这时你会发现无法成功推送镜像。
这是因为 Docker 默认不允许非 HTTPS 方式推送镜像。我们可以通过 Docker 的配置选项来取消这个限制,或者查看下一节配置能够通过 HTTPS 访问的私有仓库。
Ubuntu 16.04+, Debian 8+, centos 7
对于使用 systemd 的系统,请在 /etc/docker/daemon.json 中写入如下内容(如果文件不存在请新建该文件):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
{
"registry-mirrors": ["https://docker.mirrors.ustc.edu.cn",
"https://registry.docker-cn.com",
"http://hub-mirror.c.163.com",
"https://mirror.baidubce.com"],
"insecure-registries": ["192.168.199.100:5000"],
"debug": true,
"experimental": false,
"runtimes": {
"nvidia": {
"path": "nvidia-container-runtime",
"runtimeArgs": []
}
}
}

注意:

  1. 该文件必须符合 json 规范,否则 Docker 将不能启动
  2. <font color=red>需要重启 docker 才能生效 </font>
  3. 上面我增加了容器调用宿主机服务器 GPU 的运行时,根据个人情况添加

其他

指定容器 IP 与宿主机同网段

1
2
3
4
5
6
7
8
9
10
11
# 查看网卡是否支持 macvlan
sudo modeprobe macvlan
sudo lsmod | grep macvlan

# 创建网络
docker network create -d macvlan --subnet=10.2.28.0/24 --gateway=10.2.28.1 -o parent=eno1 mymacvlan

# 指定 IP 创建容器
docker run -itd --name lan-desktop --net=mymacvlan --ip=10.2.28.18 ubuntu:18.04 /bin/bash

# 如果网卡没有开启混合模式,那么宿主机和容器无法直接 PING 通。但和其他 IP 可以互相 PING 通。

容器中访问宿主机服务

要求: Docker 版本高于 v20.10 (2020年12月4日更新)

命令行模式下:

1
--add-host=host.docker.internal:host-gateway

Docker Compose 模式下:

1
2
extra_hosts:
- "host.docker.internal:host-gateway"

而在 container 内,可请求 host.docker.internal:PORT,来获取宿主机上提供的服务。

容器中安装 opencv-python

Ubuntu18.04 容器中安装 opencv-python 需要安装一些依赖包:

1
2
apt update
apt install ffmpeg libsm6 libxext6 -y

容器中运行 GUI 界面在宿主机显示

DISPLAY 环境变量格式如下 host:NumA.NumB,其中 host 指 Xserver 所在的主机主机名或者 ip 地址,图形将显示在这一机器上,可以是启动了图形界面的 Linux/Unix 机器,也可以是安装了 Exceed、X-Deep/32 等 Windows 平台运行的 Xserver 的 Windows 机器。如果 Host 为空,则表示 Xserver 运行于本机,并且图形程序(Xclient)使用 unix socket 方式连接到 Xserver,而不是 TCP 方式。使用 TCP方式连接时,NumA 为连接的端口减去 6000 后的值,如果 NumA 为 0,则表示连接到 6000 端口;使用 unix socket 方式连接时则表示连接的 unix socket 的路径,如果为 0,则表示连接到 /tmp/.X11-unix/X0,NumB 则几乎总是0。

以 deeplabcut[gui] 镜像为例,先拉取镜像:

1
docker pull deeplabcut/deeplabcut:2.2.1.1-gui-cuda11.0.3-runtime-ubuntu18.04

然后设置宿主机运行远程连接 X server

1
xhost +

启动镜像,注意把本地 DISPLAY 设置上

1
2
3
4
5
docker run -itd --name deeplabcut --gpus=all -v /tmp/.X11-unix:/tmp/.X11-unix -e DISPLAY=unix$DISPLAY deeplabcut/deeplabcut:2.2.1.1-gui-cuda11.0.3-runtime-ubuntu18.04 /bin/bash

# 如果是远程访问 docker 容器,那么可以设置当前客户端电脑的 IP,如 10.2.28.12
# 或者运行容器时不设置,运行容器后再设置,进入容器后,export DISPLAY=10.2.28.12:0.0
docker run -itd --name deeplabcut --gpus=all -e DISPLAY=10.2.28.12:0.0 deeplabcut/deeplabcut:2.2.1.1-gui-cuda11.0.3-runtime-ubuntu18.04 /bin/bash

进入容器启动 GUI 服务

1
python -m deeplabcut

镜像包依赖问题可能需要把 python 包 wxpython 更改为 4.0.7:

1
pip install wxPython==4.0.7.post2

或者直接在容器中安装桌面环境,然后开启远程访问,如 VNC, XRDP 等,远程访问 docker 容器桌面环境,类似于一台正常的远程桌面服务器一样。但是安装桌面环境对于不同的基镜像方法不同,安装的包会有些差别。但是使用起来会更全面。

另外,有时候除了显示容器里GUI程序界面在宿主机,还需要与宿主机程序通信等,可以采用如下方式:

1
2
3
DIR=$(pwd)

xhost + && docker run --gpus all --env NVIDIA_DISABLE_REQUIRE=1 -it --network=host --name bundlesdf --cap-add=SYS_PTRACE --security-opt seccomp=unconfined -v /home:/home -v /tmp:/tmp -v /mnt:/mnt -v $DIR:$DIR --ipc=host -e DISPLAY=${DISPLAY} -e GIT_INDEX_FILE nvcr.io/nvidia/bundlesdf:latest bash

–cap-add 参数解决权限问题(无法使用gdb调试、无法date -s修改时间)

–security-opt seccomp=unconfined 选项,它可以取消 Seccomp (Seccomp,即安全计算模式,是 Linux 内核的一部分,用于限制进程可以使用的系统调用。通过禁止或限制对某些系统调用的访问,Seccomp 可以有效减少攻击者能够利用的攻击面。Docker 在默认情况下启用了 Seccomp,并提供了一个默认的配置文件,该文件白名单了大约300个系统调用,其他的则被禁止。这意味着在 Docker 容器中运行的进程只能访问这些已经过滤的系统调用。)的限制,使容器内的进程可以访问所有系统调用。虽然提供了灵活性,但也带来了一些系统风险。禁用 Seccomp 意味着容器内的进程可以访问系统调用,包括那些可能被利用来执行恶意操作的系统调用。

–env NVIDIA_DISABLE_REQUIRE=1 当容器内的 cuda 版本比较宿主机上的驱动版本高时,可能容器里的程序会出现报错。设置改环境变量,让容器内的 cuda 切换为宿主机上的。

–ipc=host 容器共享宿主机的 IPC(inter-process communication) 命名空间,让容器内进程与宿主机的进程进行通信。

容器中运行 pytorch 多 GPU 模型

在容器中运行 pytorch 多 GPU 模型可能会出现如下错误:

ERROR: Unexpected bus error encountered in worker. This might be caused by insufficient shared memory (shm). Dataloader中的num_workers设置与docker的shared memory相关问题

这是因为 docker 容器内 shm_size 默认大小 64M 太小导致的问题,有两个解决思路:

  1. 在 Dataloader 中将 num_worker 设置为 0,只需在代码中修改比较简单,缺点是训练过程变慢,特别是对较大数据例如视频图像
  2. 改变容器中 shared_memory 大小,如下:
    1
    docker run -itd --name mmdet-dlc-multi-gpus --gpus '"device=1,2"' -p 33336:80 --shm-size 8G mmdet-dlc:v1.2 /bin/bash

容器内存和 CPU 限制

有一次在容器中运行一个程序直接爆满宿主机内存,导致服务器无法使用。此时,可以尝试更新容器的内存限制,然后再运行程序,方法如下:

1
2
3
# mmdet-dlc-tencent 为容器名
# 此代码设置容器可使用内存为 40 GB
docker update --memory 40g --memory-swap 42g mmdet-dlc-tencent

上面命令设置后,容器就只能使用宿主机内存 40000 MB,但可以使用所有的交换分区。同时,当程序遇到内存不够时会出现 OOM(Out Of Memory)异常退出,解决方法是,在创建容器时,设置如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
# 设置 oom-kill-disable 能够避免内存不够时异常退出
# memory-swap 一般不设置或设置为0,表示当内存不够时使用所有交换分区。或设置为比 memory 大,多出的为使用的交换分区大小。即其包含了使用的内存和交换分区两者
docker run --cpus="3" --oom-kill-disable --memory="40000m" --memory-swap="50g" --name mmdet-dlc-tencent mmdet-dlc:v2.0 bash
# 或者使用--cpuset-cpus指定使用那几个cpu,该编号对应htop命令列出的CPU编号顺序(不过是从1开始,docker中从0开始)
docker run --cpuset-cpus="0,1,2" --oom-kill-disable --memory="40000m" --memory-swap="50g" --name mmdet-dlc-tencent mmdet-dlc:v2.0 bash
# 或用 0-2 表示 0,1,2
docker run --cpuset-cpus="0-2" --oom-kill-disable --memory="40g" --memory-swap="50g" --name mmdet-dlc-tencent mmdet-dlc:v2.0 bash

# docker-compose v2 配置 CPU 和内存限制
version: '2'
services:
colab-hub:
image: colab-hub:v2.0
container_name: colab-huber
user: 'root'
cpuset: '0-59'
mem_limit: '50g'
memswap_limit: '52g'
oom_kill_disable: true
expose:
- '8000'
ports:
- '38000:8000'
volumes:
- '/disk0:/disk0'
- '/disk1:/disk1'
restart: 'on-failure'
environment:
- TZ=Asia/Shanghai
- LANG="C.UTF-8"
shm_size: '8g'
deploy:
resources:
reservations:
devices:
- driver: 'nvidia'
#device_ids: ['0', '1', '2', '3', '4', '5', '6', '7']
count: 'all'
capabilities: ['gpu']
entrypoint: ['/root/start_jupyterhub.sh']
volumes:
colab-hub_data:
driver: local

networks:
default:
external: true
name: colab # docker network create colab


# cpus 表示限制容器只使用 3 个 逻辑CPU
# 可进入容器后,使用如下命令查看 CPU 情况,如下命令尝试使用 5 个CPU,但是容器创建时限制 3 个
stress --cpu 5

# 或者通过编写 python 多进程程序进行压力测试,代码如下,在 docker 中运行该 Python 程序,在容器外查看使用的 CPU 情况
# Python 程序参加下方 Python 代码

# 可在容器外使用如下命令查看容器资源(包括 CPU、内存、网络、磁盘IO 等)使用情况
docker stats

# 或使用 htop 查看哪些 CPU 被使用以及使用情况
htop

用于测试 docker 中 CPU 分配情况的程序 Python 程序(cpus-limit-test.py):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
#!/usr/bin/env python

from multiprocessing import Pool
import time
import argparse


def t():
parser = argparse.ArgumentParser(description="python argparser")
parser.add_argument('-c', "--cpus", type=int, default=5)
return parser


def loop(i):
while True:
pass

if __name__ == "__main__":
# 这里可以设置大于创建容器时指定的CPU个数
parser = t()
args = parser.parse_args()
CPUs = args.cpus
p = Pool(CPUs)
for i in range(CPUs):
p.apply_async(loop, args=(i,))

p.close()
p.join()

对容器进行内存现在时有时候会出现错误提示:No swap limit support,此时内存限制是无效的。可以尝试通过如下方法解决:

1
2
3
4
5
6
7
8
9
10
11
sudo vim /etc/default/grub

# 增加如下内容
GRUB_CMDLINE_LINUX="cgroup_enable=memory swapaccount=1"
# 如果本身的 GRUB_CMDLINE_LINUX 后的双引号里有内容,请在后面增加空格后再增加上面的内容 " cgroup_enable=memory swapaccount=1"。

# 然后需要更新引导
sudo update-grub

# 最后重启生效
sudo shutdown -r now

mac docker 无法启动

mac 中 docker 无法启动,提示:

com.docker.backend cannot start Exit code 101

可尝试如下方法解决:

1
2
pkill Docker  
killall Docker

然后在重启 docker

mac docker GUI

以 ubuntu:22.04 为例,在容器中安装VNC桌面,并在mac宿主机上访问。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
docker pull ubuntu:22.04

docker run -itd --name vnc-gui -p 8311:5907 ubuntu:22.04 /bin/bash
docker exec -it vnc-gui /bin/bash

# 在docker中执行
apt update
apt install xorg openbox xserver-xorg-core
apt install tigervnc-standalone-server tigervnc-xorg-extension
apt install xfce4 xfce4-goodies
apt update && sudo apt install locales
locale-gen en_US en_US.UTF-8
update-locale LC_ALL=en_US.UTF-8 LANG=en_US.UTF-8
export LANG=en_US.UTF-8
echo "export LANG=en_US.UTF-8" >> /etc/profile
apt install ttf-wqy-zenhei

# 设置密码
vncpasswd

# 启动测试
vncserver -localhost no
vncserver -list
vncserver -kill :1

# 启动指定端口,如5907
vncserver -localhost no :7

在Mac上安装tigervnc或其他VNC桌面程序,访问:localhost::8311,输入vncpasswd设置的密码,即可访问。进入后,可以设置默认程序。

安装firefox,建议从firefox官网下载firefox完整程序,手动解压到指定文件夹,设置默认程序时找到解压的firefox文件夹里的firefox可执行文件。

nvidia-smi 在 docker 中看不到进程号

当我们使用 --gpus all 等启动一个想要在 docker 容器中使用 GPU 时,使用命令 nvidia-smi 无法看到当前使用 GPU 的进程 ID,it is related to PID namespaces, the driver is not aware of the PID namespace and thus nvidia-smi in the container doesn’t see any process running. github-nvidia-docker issue. 一种解决方法是启动容器时,增加上一个参数:

1
--pid=host

或者,使用 python 包 py3nvml,方法如下:

1
2
3
4
5
6
# install 
pip install py3nvml

# usage
py3nvml
py3nvml -l 5

同时,py3nvml 作为 python 的一个包,可以在 python 代码中使用来获取 gpu 的实时信息:

1
2
3
import py3nvml

py3nvml.get_gree_gpus()

迁移 docker 镜像

默认情况下安装的 docker 会自动把镜像文件存储到 /var/lib/docker 目录下,当我们创建的镜像较多较大时,会导致磁盘空间不足等问题,此时,我们可以尝试将镜像存储路径迁移到其他挂载盘:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 停止 docker 进程
sudo service docker stop

# 把 /var/lib/docker 移动到其他磁盘,如 /disk2
# 这里 /disk2 是我新挂载的大容量磁盘
sudo chmod 775 /disk2
sudo mv /var/lib/docker /disk2

# 更改配置文件信息
vim /etc/docker/daemon.json
# 增加如下内容
"data-root": "/disk2/docker",

# 重启docker
sudo service docker start

docker 磁盘清理

在长时间使用后,docker 镜像会累积,占用大量的磁盘空间。

docker 磁盘分析

使用如下命令可查看 docker 使用空间情况:

1
2
3
docker system df
# 详细信息
docker system df -v

docker 磁盘清理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
# 清理所有停止的容器、不使用的网络、所有悬空的镜像
# - all stopped containers
# - all networks not used by at least one container
# - all dangling images
# - all dangling build cache
docker system prune

# 彻底清理。同时清理未使用的镜像。慎用
docker system prune -a

# 清理所有悬空的镜像
docker image prune

# 同时清理未使用的镜像
docker image prune -a

# 清理停止运行的容器
docker container prune

# 清理未使用的卷宗volume
docker volume prune

# 查看帮助
docker system prune --help

容器中使用 systemctl

当在容器中使用 systemctl 时有时会出现如下错误:

1
2
3
Failed to connect to bus: No such file or directory
# or
Failed to get D-Bus connection: Operation not permitted

此时,可以尝试如下方法:

1
2
3
4
# 拉取 centos8
docker pull centos:latest
# 启动时指定 --privileged 和默认运行 /usr/sbin/init 为 pid 1
docker run -itd --privileged --name nginx -p 9980:80 centos:latest /usr/sbin/init

进入容器后,使用命令查看是否可使用 systemctl

1
systemctl status

centos8 yum update

如果出现 centos8 中无法 yum update,可使用如下方法:

1
2
sed -i 's/mirrorlist/#mirrorlist/g' /etc/yum.repos.d/CentOS-*
sed -i 's|#baseurl=http://mirror.centos.org|baseurl=http://vault.centos.org|g' /etc/yum.repos.d/CentOS-*

centos8 中安装 ssh

1
2
3
yum install openssh-clients openssh-server -y
systemctl start sshd
systemctl enable sshd

centos8 中安装 nginx

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
yum install gcc wget epel-release -y
yum install vim htop make net-tools which -y
yum install -y pcre pcre-devel
yum install -y zlib zlib-devel
wget https://nginx.org/download/nginx-1.22.1.tar.gz
tar -xvzf nginx-1.22.1.tar.gz
cd nginx-1.22.1
./configure --prefix=/usr/local/nginx
make -j 4 # 指定使用 cpu 个数,加速
make install

cat > /usr/lib/systemd/system/nginx.service <<-"EOF"
[Unit]
Description=nginx - web server
After=network.target remote-fs.target nss-lookup.target

[Service]
Type=forking
PIDFile=/usr/local/nginx/logs/nginx.pid
ExecStartPre=/usr/local/nginx/sbin/nginx -t -c /usr/local/nginx/conf/nginx.conf
ExecStart=/usr/local/nginx/sbin/nginx -c /usr/local/nginx/conf/nginx.conf
ExecReload=/usr/local/nginx/sbin/nginx -s reload
ExecStop=/usr/local/nginx/sbin/nginx -s stop
ExecQuit=/usr/local/nginx/sbin/nginx -s quit
PrivateTmp=true

[Install]
WantedBy=multi-user.target
EOF

systemctl daemon-reload
systemctl start nginx
systemctl enable nginx

no gsettings

当在容器中运行图像界面打开模型交互式界面时出现:

No such file or directory: ‘gsettings’

解决方法:

1
2
apt update 
apt install libglib2.0-bin

docker pull proxy

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# 配置
sudo mkdir -p /etc/systemd/system/docker.service.d

sudo cat > /etc/systemd/system/docker.service.d/http-proxy.conf <<-"EOF"
[Service]
Environment="HTTP_PROXY=http://127.0.0.1:8118"
Environment="HTTPS_PROXY=http://127.0.0.1:8118"
Environment="NO_PROXY=localhost"
EOF

# 使生效
sudo systemctl daemon-reload

# 查看
sudo systemctl show --property=Environment docker

参考链接

  1. docker国内镜像源
  2. Docker 入门教程
  3. Docker 微服务教程
  4. Docker:docker镜像与容器的导入和导出
  5. runoob.com docker-tutorial
  6. 外部访问容器
  7. 添加和修改docker容器端口映射的方法
  8. Docker和宿主机之间共享文件
  9. docker 挂载宿主机文件目录
  10. docker容器挂载host宿主机的本地目录,docker容器与宿主机之间互相拷贝文件
  11. Docker Compose
  12. docker使用GPU总结
  13. nathzi1505/install-docker.sh
  14. Docker运行ubuntu22.04出现异常
  15. 私有仓库
  16. 解决docker警告WARNING: No swap limit support
  17. 关于export DISPLAY=:0.0
  18. Docker 网络模型之 macvlan 详解,图解,实验完整
  19. docker容器内访问宿主机host服务