Docker 入门笔记
简单学习一下 Docker。
基本概念
Docker 是一种轻量级的容器化技术,与传统虚拟机相比,它更加高效,能够快速部署和运行应用程序。传统虚拟机需要完整的操作系统,每个虚拟机通常占用多个 GB,而 Docker 容器共享宿主机内核,镜像通常只有 MB 级别,占用更少的资源,启动速度更快。
首先需要区分 Docker 的两个核心概念:
- Docker Image(镜像):Docker 镜像是一个只读的模板(实质是宿主系统中的一个文件),包含了运行应用程序所需的所有文件和环境(包括代码、运行时、依赖项、系统工具等)。镜像可以被用来创建容器,基于一个镜像可以创建多个容器。
- Docker Container(容器):容器是基于镜像创建的运行实例(实质是宿主系统中的一个进程),它提供了一个隔离的轻量级 Linux 环境,可以在其中独立运行应用程序。 容器可以被创建、启动、暂停、停止、删除等,多个容器可以在同一个主机上运行,并且它们彼此之间是相互隔离的。容器在运行过程中产生的更改通常不会影响外部系统(除非使用专门的方式,包括数据卷和绑定宿主目录等),更不会影响镜像文件。
镜像的构造过程实际上是分层进行的,每一层都是将前一层视作只读基础来进一步构建的,这样可以最大程度地实现复用,减少镜像的体积。
Docker 的基本架构如下图所示:
具体来说:
Docker Client(客户端):Docker 客户端是用户与 Docker 交互的主要方式,通常指
docker
命令行工具(CLI)。用户通过 Docker CLI 发送命令(如docker run
、docker pull
),然后这些命令会传递给 Docker 守护进程进行执行。Docker Daemon(守护进程):Docker Daemon (
dockerd
) 是运行在 Docker Host 上的后台守护进程,负责管理容器的生命周期。它接收来自 Docker Client 的请求,处理容器的创建、运行、停止等操作,并与 Docker Registry 交互以下载或上传镜像。Docker Host(Docker 主机、宿主机):Docker Host 是运行 Docker Daemon 的物理机或虚拟机,它提供了运行 Docker 容器的环境(容器实际运行在 Docker Host 中)。它通常就是本地计算机,但是也可以是云服务器,Docker 提供了远程管理的支持。
Docker Registry(注册服务器):Docker Registry 用于存储和分发 Docker 镜像。官方的 Docker Hub 是一个公共的 Docker Registry,用户可以从 Docker Hub 拉取镜像,也可以将自己的镜像上传到其中。一个 Docker Registry 通常包含大量仓库(Repository);每个仓库可以包含多个 标签(Tag);每个标签直接对应一个镜像。
一个仓库通常代表一个预设的系统或软件环境,可能会同时提供不同版本的镜像,使用标签进行区分,
可以通过 <仓库名>:<标签>
的格式来指定,如果不提供标签,将使用 latest
作为默认标签,表示最新的一个版本。
以 Ubuntu 镜像为例,ubuntu
是仓库的名字,仓库中包含有不同的版本标签,如,16.04, 18.04, 可以通过
ubuntu:18.04
来具体指定所需的镜像,缺省时,ubuntu
将视为
ubuntu:latest
。 仓库名还可能以两段式路径形式出现,比如
jwilder/nginx-proxy
,前者代表 Docker Registr
中的用户名,后者则是对应的软件名,不含用户名的仓库应该是注册服务器直接提供支持的。
虽然容器模拟了一个轻量级的Linux环境,在其中可以同时运行多个应用,但是通常并不推荐,推荐的做法是一个容器只运行一个进程,将不同的应用分散在不同容器中,例如一个 web 应用可能通过 web应用 + 数据库 + 缓存 这三个容器实现。
安装与配置
这里只考虑纯 Linux 环境,不考虑 Windows 的 WSL2,Linux 系统为 Ubuntu。
注意:Docker 的旧版本曾经被称为 docker.io,docker-engine 或
docker,这些名称很混乱,主要是早期有一个其它程序也叫docker,后来主动改名了。
如果已安装这些旧版本,需要使用下面的命令卸载它们 1
sudo apt-get remove docker docker-engine docker.io containerd runc
接下来需要安装的是 Docker Engine-Community 软件包 docker-ce,即社区版,与之对应的是 docker-ee,即商业版。
安装 Docker
记录一下在Ubuntu安装Docker的步骤:(参考Ubuntu 安装 Docker)
1 | # 更新软件包索引 |
Docker 服务
Docker
需要一个守护进程,安装之后似乎就是启动的,并且已经被设置为开机自启动,可以使用下面的命令查看或设置
1
2
3
4
5
6
7
8
9
10
11# 查看 Docker 服务是否开机自动启动
sudo systemctl is-enabled docker
# 设置 Docker 服务开机自动启动
sudo systemctl enable docker
# 查看 Docker 服务状态
sudo systemctl status docker
# 启动 Docker 服务
sudo systemctl start docker
Docker 用户组
默认情况下,docker 命令会使用 Unix socket 与 Docker 引擎通讯,但是只有 root 用户和 docker 组的用户才可以访问 Docker 引擎的 Unix socket,这导致普通用户使用很多 docker 命令的权限不够。出于安全考虑,推荐的做法是将需要使用 docker 的用户加入 docker 用户组。
创建 docker 组以及添加用户的命令如下(需要sudo权限或者使用root)
1
2
3
4
5
6
7
8# 建立 docker 组(有时安装docker时会自动创建(
sudo groupadd docker
# 将当前用户追加到 docker 组(用户通常自动属于与用户名同名的组)
sudo usermod -aG docker $USER
# 将alice追加到 docker 组
sudo usermod -aG docker alice
被修改的用户可能需要重新登录,以确保修改生效。
实践中可能需要下面这些命令进行辅助 1
2
3
4
5# 测试 docker 组是否存在
getent group docker
# 检查当前用户属于哪些组
groups $USER
进行上述操作之后,一般的docker命令就不再需要使用sudo了,下文中的命令也不再使用sudo。
docker 镜像是共享的,docker 用户组的用户可以看到所有的镜像,并不局限于自己创建的镜像,但是容器并不是共享的,只有创建者的用户才能看到和操作自己创建的容器。
测试
Docker 提供了一个最简单的 hello-world 镜像用来测试 docker
是否安装成功。 1
docker run hello-world
此命令的含义为启动 hello-world 镜像,由于本地没有,会自动开始下载,并基于此镜像运行容器,然后打印 hello-world 并自动退出容器,顺利完成即表示 Docker 安装成功。
由于不可抗力的原因,在获取镜像时会遇到各种网络问题,上述测试没有成功,选择手动从指定镜像下载镜像,命令修改为
1
2
3
4
5# 下载镜像
docker pull docker-0.unsee.tech/hello-world
# 运行容器
docker run docker-0.unsee.tech/hello-world
这里我们顺便下载一个基础的ubuntu镜像进行后续的操作 1
docker pull docker-0.unsee.tech/ubuntu
这个镜像里面几乎啥也没有,包括vim
,ping
,ssh
等基础工具都没有,在镜像任何实际操作之前需要apt-get update
更新包索引,然后下载必要的工具。
考虑到网络原因,下文中的命令所使用的ubuntu
都需要替换为docker-0.unsee.tech/ubuntu
。
关于网络问题的更多处理办法参考目前国内可用Docker镜像源汇总。
镜像操作
查找和下载镜像
可以在注册服务器中搜索相关镜像 1
2
3docker search mysql
docker search docker-0.unsee.tech/mysql
可以通过pull命令下载镜像 1
docker pull [IMAGE_NAME]:[TAG]
标签在缺省时视作latest
。
查看本地镜像
下面的命令可以查看当前本地的镜像列表 1
docker image list
返回的表格字段说明:
REPOSITORY
: 镜像来自于哪个仓库;TAG
: 镜像的标签;IMAGE ID
: 镜像的唯一标识 ID, 如果看到两个 ID 完全相同,那么实际上它们指向的是同一个镜像,只是标签名称不同罢了;CREATED
: 镜像创建时间;SIZE
: 镜像的大小。
下面这些命令都是一样的,都是 Docker 提供的一组别名 1
2
3docker image list
docker image ls
docker images
导入和导出镜像
可以将本地的镜像以压缩包形式导出,例如 1
docker save -o redis.tar redis:latest
与之配套的是从压缩包中导入镜像,例如 1
docker load -i redis.tar
删除镜像
下面的命令可以删除镜像 1
2
3
4docker image rm [IMAGE_NAME]:[TAG]
# or
docker rmi [IMAGE_NAME]:[TAG]
如果本地存在一个镜像的多个版本(即多个标签),必须指定删除的镜像标签。
也可以直接使用 IMAGE_ID 来删除镜像,例如 1
docker rmi ee7cbd482336
在使用 Docker
一段时间后,系统一般都会残存一些临时的、没有被使用的镜像文件(可能是已删除的镜像所使用的中间层等),可以通过以下命令进行清理
1
docker image prune
容器操作
对容器的操作通常需要指定容器ID或者名称:
- 容器 ID 是一个很长的随机字符串,在创建时自动生成,实际使用只需要提供足够唯一确定的前缀即可;
- 容器名称可以在创建时通过
--name
指定,否则会提供一个随机名称,例如sweet_carson
。(还可以指定--hostname
,否则默认使用容器 ID)
启动容器
Docker 启动容器通常有两种情况:
- 基于镜像新建一个容器并启动;
- 将处于终止状态 (exited)的容器重新启动。
我们先关注第一种情况。
启动容器的命令形式如下 1
docker run [IMAGE]:[TAG] [OPTIONS]
通常需要加上不同的选项来实现不同的启动方式。
例如下面这个命令会启动一个 ubuntu 镜像,直接执行命令
/bin/echo "hello,docker"
然后终止 1
docker run ubuntu:latest /bin/echo "hello,docker"
需要注意的是,一旦命令执行完毕,这个容器就会自动终止。
如果希望以交互的方式运行容器,可以参考如下命令:(默认以root用户进入,位于根目录/
下)
1
docker run -it ubuntu:latest /bin/bash
其中使用的选项为:
-t
: 让 Docker 分配一个伪终端(pseudo-tty)并绑定到容器的标准输入;-i
: 让容器的标准输入保持打开。
实践中我们更需要让容器在后台持续运行来提供服务,此时需要加入-d
选项,例如
1
docker run -d ubuntu:latest /bin/sh -c "while true; do echo hello world; sleep 1; done"
这里的效果是让容器在后台保持运行,每隔一秒打印出hello world
。
命令执行成功后会返回一个容器 ID。
可以使用下面的命令查看在后台运行的容器的日志 1
docker container logs [container ID or NAMES]
查看容器
下面的命令可以列出当前正在运行的容器 1
docker container list
加上-a
选项可以列出所有容器,包括当前正在运行的和已经停止的容器
1
docker container list -a
可以筛选最新创建的几个容器,例如 1
docker container list -a -n=2
下面这些命令都是一样的,都是 Docker 提供的一组别名 1
2
3
4docker container list
docker container ls
docker container ps
docker ps
停止容器
对于交互式运行的容器,直接输入exit或者Ctrl+D都可以退出容器。(ctrl+p,ctrl+q 则可以退出处于交互式的容器,但不会关闭容器)
对于在后台运行的容器,可以使用下面的命令停止 1
2
3docker container stop [container ID or NAMES]
# 可省略关键字 container
docker stop [container ID or NAMES]
除了stop
,还可以使用kill
强制关闭容器
1
2
3docker container kill [container ID or NAMES]
# 可省略关键字 container
docker kill [container ID or NAMES]
重启容器
下面的命令可以重启终止状态的容器 1
2
3docker container restart [container ID or NAMES]
# 可省略关键字 container
docker restart [container ID or NAMES]
进入容器
使用 docker exec
命令可以进入一个正在后台运行的容器,例如 1
docker exec -it [container ID or NAMES] /bin/bash
这里必须提供两个位置参数,通常是容器ID和一个shell。
启动的终端可以通过 exit
或 Ctrl+D
退出,这并不会导致容器终止。
除此之外,还有一个过时的 docker attach
命令也可以达到类似效果,但是不再推荐,因为它的退出会导致容器终止。
导入和导出容器
使用 docker export
命令可以导出容器为压缩包,其中包含了容器中文件系统的完整拷贝,例如
1
docker export 9e8d11aeef0c > redis.tar
使用 docker import
命令可以将快照导入为镜像,例如
1
cat redis.tar | docker import - test/redis:v1.0
除此之外还有
docker commit
命令,也可以基于容器当前状态创建镜像,但是与这里的做法不同,它可以保存原始镜像的元数据。
删除容器
使用下面的命令可以删除指定的已经停止的容器 1
2
3docker container rm [container ID or NAMES]
# 可省略关键字 container
docker rm [container ID or NAMES]
添加-f
选项还可以强制删除一个正在运行的容器。
使用下面的命令可以一次性删除所有已经停止的容器 1
docker container prune
数据管理与数据卷
Docker 镜像由多个文件系统(只读层)叠加而成,在容器启动时,Docker 会加载只读镜像层,在镜像栈顶部添加一个读写层。 如果在运行的容器中修改了某个已存在的文件,那么该文件将会从下面的只读层复制到上面的读写层,同时,该文件在只读层中仍然存在,并且保持未修改的原始状态。 当我们删除 Docker 容器,并通过镜像重新启动容器时,之前的更改的文件将会丢失,也就是说在容器中的数据修改默认是会丢失的。
如果我们希望将数据持久化,可以使用数据卷:数据卷是一个可供一个或多个容器使用的特殊目录,用于持久化数据以及共享容器间的数据,它以正常的文件或目录的形式存在于宿主机上。数据卷的生命周期独立于容器的生命周期,删除容器时,它使用的数据卷并不会被同时删除。
使用下面的命令可以查看当前的所有数据卷 1
docker volume ls
创建数据卷的命令如下 1
docker volume create test-vol
以这种方式创建的数据卷默认位于宿主文件系统的
/var/lib/docker/volumes
目录下。
与Docker相关的本地资源都存放在
/var/lib/docker/
目录下,例如其中的子目录containers
存放容器信息。
使用下面的命令可以查看指定数据卷的详细信息 1
docker volume inspect test-vol
下面提供一个挂载数据卷的示例,运行一个 Nginx
容器,并通过--mount
选项挂载该数据卷
1 | docker run -d -it \ |
参数说明:
-d -it
: 后台运行容器,开启一个终端;--name=test-nginx
: 指定容器名为test-nginx
;-p 8011:80
: 将容器的 80 端口挂载到宿主机的 8011 端口;(还有一个类似的-P
选项,代表随机端口)--mount
:source=test-vol
:指定数据卷名称为test-vol
;target=/usr/share/nginx/html
: 值得挂载到容器中的/usr/share/nginx/html
目录。
挂载成功后,不论是修改 /var/lib/docker/volumes
下的数据,还是进入到容器中修改 /usr/share/nginx/html
下的数据,都会自动同步修改,类似前端开发中双向绑定的作用。
挂载的目录对于容器来说默认是允许读写的,但是也可以加入选项
readonly
使其对于容器内的文件系统变为只读状态。
除了专门的数据卷,还可以直接指定挂载一个本地主机的目录到容器中去
1
2
3
4
5docker run -d -it \
--name=test-nginx \
-p 8011:80 \
--mount type=bind,source=/src/webapp,target=/usr/share/nginx/html \
nginx:1.13.12
这里需要指定type=bind
,前文中数据卷的做法相当于type=volume
,只是被默认省略了。
除了--mount
选项,还有一个更早提供的-v
选项,示例如下
1
2
3
4
5docker run -d -it \
--name=test-nginx \
-p 8011:80 \
-v test-vol:/usr/share/nginx/html \
nginx:1.13.12
使用-v
或--mount
存在一点区别:如果宿主机中没有指定文件,-v
会自动创建指定文件,--mount
则会报错找不到指定文件。因此从防止误操作的角度,更建议使用--mount
。
使用下面的命令可以删除数据卷 1
docker volume rm test-vol
如果需要在删除容器的同时移除数据卷,可以在删除时加上-v
选项
1
docker rm -v ...
注意:docker 本身并不支持在容器运行状态下的数据卷热插拔。
Dockerfile
Dockerfile
是一个特殊的文本文件(文件名即Dockerfile
),里面写着构建
Docker 镜像的指令。 它通过描述式的语句告诉 Docker
如何一步步打包应用程序和运行环境。
不讨论详细的语法细节,这里只提供一个使用 Docker 打包 Nginx
服务器的例子。
1 | # 指定基础镜像 |
注:
- 由于docker镜像是分层构建的,这里的每一个实际操作都会产生一个新的层,将主要的步骤分层可以在构建相似镜像时充分利用这里的中间层,从而提高构建速度;
- 这里的
EXPOSE
只是提示性质的,并不代表容器实际暴露的端口; - 这里还可以指定
CMD
字段,它表示容器启动时的默认命令,没有时需要在启动命令中指定,否则会启动一个默认的shell(例如/bin/bash
)。
使用下面的命令构建镜像 1
docker build -t my-nginx:1.0 .
在构建时会自动使用当前目录下的 Dockerfile 文件,也可以通过
-f
参数指定 Dockerfile 文件的路径。
运行容器 1
docker run -d -p 8080:80 my-nginx:1.0
访问 http://localhost:8080
即可看到 Nginx
提供的页面。
补充
联网问题
从容器内部访问外部网络默认应该是允许的,如果遇到容器内部联网问题,比如apt update
失败,可以尝试在启动时加上--net=host
选项,此时容器会直接使用宿主机的网络,不需要端口映射。
必须要说明的是,ufw对docker转发端口不起作用,因为 Docker 为了支持容器的网络配置,绕过了 UFW 直接修改了 iptables,所以一个容器可以绑定一个端口。 这就意味着,所有设置的 UFW 规则都将在 Docker 容器中失效。
可以使用下面的做法禁止 docker 自动修改 iptables,参考 Docker UFW
失效 1
2sudo vim /etc/docker/daemon.json
# 如果文件不存在,直接新建
添加如下配置 1
2
3{
"iptables": false
}
然后重启docker服务 1
sudo systemctl restart docker
但是这带来的后果是在docker容器中的联网可能存在问题,使用--nat=host
直接使用宿主机网络是最简单的做法。
其它更复杂的网络配置这里不做讨论。
时间问题
在容器中默认可能是UTC标准时间,可以使用下面的命令检查时区
1
date +"%Z %z"
可以用下面的命令修改为北京时间 1
ln -sf /usr/share/zoneinfo/Asia/Shanghai /etc/localtime
ssh登陆容器
下面记录一下如何让一个简单的 ubuntu 容器支持从外部ssh登陆。
首先准备好端口映射,例如在启动将22号端口映射到宿主机的2222号端口。
1
docker run -dit -p 2222:22 ubuntu:latest
这里端口映射对于bridge网络模式有效,对于host网络模式没有意义。在host网络模式下,还需要在容器内部修改sshd监听端口。
然后重新进入docker容器进行后续操作。 1
docker exec -it ubuntu:latest /bin/bash
我们需要安装一些必要的基础软件 1
2
3
4
5apt-get update
apt-get install vim
apt-get install openssh-server openssh-client
启动sshd守护进程 1
/etc/init.d/ssh start
查看进程是否启动 1
ps -e | grep sshd
如果使用--net=host
,这里很可能启动失败,因为22号端口已经被宿主机的sshd服务占用了,需要修改容器中sshd服务监听的端口。(ssh远程登陆时,通过-p
指定端口)
编辑sshd的配置文件 1
vim /etc/ssh/sshd_config
将PermitRootLogin prohibit-password
修改为PermitRootLogin yes
允许root用户远程登陆。
重启 ssh 服务使修改生效 1
service ssh restart
在本地直接进入docker容器默认就是环境中的root用户,不需要密码也没有密码。为了允许从外部ssh登陆,必须给root用户设置密码
1
passwd root
实际上,即使这里设置了root密码,通过docker exec
重新进入容器仍然是不需要密码的。
配置完成之后,通过ssh即可实现远程访问,并且这不是登陆宿主机再进入容器,而是直接登陆docker容器。
在容器中使用 GPU
在容器中通常无法直接使用宿主机的GPU,需要进行一些额外配置,这部分操作主要参考(解决方案)docker could not select device driver |Docker 无法成功分配或访问GPU资源。
首先检查本地的Nvidia驱动,CUDA版本等,选择拉取NVIDIA提供的对应版本的基础镜像
1
docker pull nvcr.io/nvidia/cuda:12.4.0-runtime-ubuntu20.04
然后安装nvidia-docker2
,这里需要先添加仓库,更新软件源索引
1
2
3
4
5
6
7
8
9
10# 设置仓库和GPG密钥
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-get update
# 安装 nvidia-docker2
sudo apt-get install -y nvidia-docker2
为了使修改生效,需要重启docker服务 1
sudo systemctl restart docker
可以启动这个基础镜像,启动时加上--gpus all
选项
1
docker run --gpus all -it nvcr.io/nvidia/cuda:12.4.0-runtime-ubuntu20.04
可以在容器中使用nvidia-smi
进行测试,输出正常即代表容器可以使用宿主机的GPU资源。