Docker 是一个开源的应用容器引擎,让开发者可以打包他们的应用以及依赖包到一个可移植的镜像中,然后发布到任何流行的 Linux或Windows操作系统的机器上,也可以实现虚拟化。容器是完全使用沙箱机制,相互之间不会有任何接口。
简单来说,Docker可以简化开发环境配置;方便不同系统(Windows/Mac/Linux),不同环境(生产环境,预发环境和开发环境)的程序部署迁移。
Docker 和传统虚拟化方式的区别:
Docker 镜像 是一个特殊的文件系统,除了提供容器运行时所需的程序、库、资源、配置等文件外,还包含了一些为运行时准备的一些配置参数(如匿名卷、环境变量、用户等)。镜像不包含任何动态数据,其内容在构建之后也不会被改变。
可以理解为软件安装包,可以方便的进行传播和安装。
镜像(Image
)和容器(Container
)的关系,就像是面向对象程序设计中的 类
和 实例
一样,镜像是静态的定义,容器是镜像运行时的实体。容器可以被创建、启动、停止、删除、暂停等。
容器的实质是进程。容器内的进程是运行在一个隔离的环境里,使用起来,就好像是在一个独立于宿主的系统下操作一样。这种特性使得容器封装的应用比直接在宿主运行更加安全。
可以理解为软件安装后的状态,每个软件运行环境都是独立的、隔离的,称之为容器。
镜像构建完成后,可以很容易的在当前宿主机上运行,但是,如果需要在其它服务器上使用这个镜像,我们就需要一个集中的存储、分发镜像的服务,Docker Registry就是这样的服务。
一个Docker Registry中可以包含多个仓库Repository
);每个仓库可以包含多个标签(Tag
);每个标签对应一个镜像。
通常,一个仓库会包含同一个软件不同版本的镜像,而标签就常用于对应该软件的各个版本。我们可以通过 <仓库名>:<标签>
的格式来指定具体是这个软件哪个版本的镜像。如果不给出标签,将以 latest
作为默认标签。
仓库名经常以两段式路径形式出现,比如 kint216/flask
,前者往往意味着 Docker Registry 多用户环境下的用户名,后者则往往是对应的软件名。但这并非绝对,取决于所使用的具体 Docker Registry 的软件或服务。
因为Docker的默认安装位置在C盘,而我的C盘容量越来越小,故采取了以下的方式更改Docker的安装位置。
# Docker的安装位置:
# C:\Program Files\Docker
# C:\ProgramData\Docker
# C:\ProgramData\DockerDesktop
# C:\Users\你的用户名\AppData\Local\Docker
# C:\Users\你的用户名\AppData\Roaming\Docker
在有管理员权限的cmd
中执行以下命令:
mklink /j "C:\Program Files\Docker" "自定义的位置"
"自定义的位置“必须在执行命令前手动建立。
执行完上面的命令后直接安装docker就行了。该命令会在“默认的位置”那创建一个指向“自定义位置”的快捷方式(和自己添加的快捷方式不同)。如果想撤回这个命令的话,在”默认的位置“那把快捷方式删了就行。
下载地址:https://hub.docker.com/editions/community/docker-ce-desktop-windows
配置下载完成后需要重启电脑。
启动Docker时报错,需要手动更新WSL 2内核。
镜像加速器 | 镜像加速器地址 |
---|---|
Docker 中国官方镜像 | https://registry.docker-cn.com |
DaoCloud 镜像站 | http://f1361db2.m.daocloud.io |
Azure 中国镜像 | https://dockerhub.azk8s.cn |
科大镜像站 | https://docker.mirrors.ustc.edu.cn |
阿里云 | https:// |
七牛云 | https://reg-mirror.qiniu.com |
网易云 | https://hub-mirror.c.163.com |
腾讯云 | https://mirror.ccs.tencentyun.com |
# 加入的代码如下
"registry-mirrors": [
"https://registry.docker-cn.com",
"https://docker.mirrors.ustc.edu.cn",
"https://mirror.ccs.tencentyun.com"
],
Docker Hub 上有大量的高质量的镜像可以用,从 Docker镜像仓库获取镜像的命令是docker pull
。其命令格式为:
$ docker pull [选项] [Docker Registry地址[:端口号]/]仓库名[:标签]
具体的选项可以通过 docker pull --help
命令看到,这里我们说一下镜像名称的格式。
<域名/IP>[:端口号]
。默认地址是 Docker Hub(docker.io
)。<用户名>/<软件名>
。对于 Docker Hub,如果不给出用户名,则默认为 library
,也就是官方镜像。$ docker pull ubuntu:18.04
18.04: Pulling from library/ubuntu
284055322776: Pull complete
Digest: sha256:0fedbd5bd9fb72089c7bbca476949e10593cebed9b1fb9edf5b79dbbacddd7d6
Status: Downloaded newer image for ubuntu:18.04
docker.io/library/ubuntu:18.04
上面的命令中没有给出 Docker 镜像仓库地址,因此将会从 Docker Hub (docker.io
)获取镜像。而镜像名称是 ubuntu:18.04
,因此将会获取官方镜像 library/ubuntu
仓库中标签为 18.04
的镜像。docker pull
命令的输出结果最后一行给出了镜像的完整名称,即: docker.io/library/ubuntu:18.04
。
列出已经下载的镜像,可以使用 docker image ls
命令。
$ docker image ls
REPOSITORY TAG IMAGE ID CREATED SIZE
docker/getting-started latest 26d80cd96d69 2 months ago 28.5MB
ubuntu 18.04 5a214d77f5d7 4 months ago 63.1MB
ubuntu bionic 5a214d77f5d7 4 months ago 63.1MB
ccr.ccs.tencentyun.com/dockerpracticesig/docker_practice latest a54473bbb25a 7 months ago 47.3MB
列表包含了 仓库名
、标签
、镜像 ID
、创建时间
以及 所占用的空间
。
镜像 ID则是镜像的唯一标识,一个镜像可以对应多个标签。因此,在上面的例子中,我们可以看到 ubuntu:18.04
和 ubuntu:bionic
拥有相同的 ID,因为它们对应的是同一个镜像。
不加任何参数的情况下,docker image ls
会列出所有顶层镜像,但是有时候我们只希望列出部分镜像。docker image ls
有好几个参数可以帮助做到这个事情。
$ docker image ls ubuntu
REPOSITORY TAG IMAGE ID CREATED SIZE
ubuntu 18.04 5a214d77f5d7 4 months ago 63.1MB
ubuntu bionic 5a214d77f5d7 4 months ago 63.1MB
$ docker image ls ubuntu:18.04
REPOSITORY TAG IMAGE ID CREATED SIZE
ubuntu 18.04 5a214d77f5d7 4 months ago 63.1MB
$ docker image ls ubuntu:bionic
REPOSITORY TAG IMAGE ID CREATED SIZE
ubuntu bionic 5a214d77f5d7 4 months ago 63.1MB
删除本地的镜像,可以使用 docker image rm
命令,其格式为:
$ docker image rm [选项] <镜像1> [<镜像2> ...]
其中,<镜像>
可以是 镜像短 ID
、镜像长 ID
、镜像名
。
比如我们有这么一些镜像:
$ docker image ls
REPOSITORY TAG IMAGE ID CREATED SIZE
docker/getting-started latest 26d80cd96d69 2 months ago 28.5MB
ubuntu 18.04 5a214d77f5d7 4 months ago 63.1MB
ubuntu bionic 5a214d77f5d7 4 months ago 63.1MB
ccr.ccs.tencentyun.com/dockerpracticesig/docker_practice latest a54473bbb25a 7 months ago 47.3MB
我们可以用镜像的完整 ID,也称为 长 ID
,来删除镜像,但更多的时候是用 短 ID
来删除镜像。docker image ls
默认列出的就已经是短 ID 了,一般取前3个字符以上,只要足够区分于别的镜像就可以了。
比如这里,如果我们要删除 docker/getting-started
镜像,可以执行:
$ docker image rm 26d
Untagged: docker/getting-started:latest
Untagged: docker/getting-started@sha256:86093b75a06bf74e3d2125edb77689c8eecf8ed0cb3946573a24a6f71e88cf80
Deleted: sha256:26d80cd96d6905cdc4f95c8e4cac9e1dce43cc9d81054fff28371728435891a8
Deleted: sha256:417302e3ee3f952e11b2c25c5295ac437e9eceb97b5d50683ab9717688c2c47a
Deleted: sha256:8dfa110a9ba2693c02cc4311773b7a7ebdd02a8be91d089cd4cfc4a7ca51a75c
Deleted: sha256:57dba6dab88290f4e9b3125f9e6b3b088626af1fe3a93d47056c030ac7d51c1c
Deleted: sha256:67c225b96afdfdb9c2b8f6f7284d771fe20ff5ebabc65a088afeca1fbafadbee
Deleted: sha256:d8242f1082c90da0b3819a2737bc23215237e2440857a0c5ec0904abc81e81b0
Deleted: sha256:329df243003cb0259caf109da224f24391d0a4e3fe419fde5a6f092ee27424fe
Deleted: sha256:bdb2ce57de00bca016d44a1def44f2a281dfe027bbc4975d65c8ff4cf04d63fb
Deleted: sha256:1a058d5342cc722ad5439cacae4b2b4eedde51d8fe8800fcf28444302355c16d
我们也可以用镜像名
,也就是 <仓库名>:<标签>
,来删除镜像。
$ docker image rm ubuntu:18.04
Untagged: ubuntu:18.04
如果观察上面这几个命令的运行输出信息的话,你会注意到删除行为分为两类,一类是 Untagged
,另一类是 Deleted
。我们之前介绍过,镜像的唯一标识是其 ID 和摘要,而一个镜像可以有多个标签。
因此当我们使用上面命令删除镜像的时候,实际上是在要求删除某个标签的镜像,这就是 Untagged
的信息。因为一个镜像可以对应多个标签,因此当我们删除了所指定的标签后,可能还有别的标签指向了这个镜像,如果是这种情况,那么 Delete
行为就不会发生。所以在执行docker image rm ubuntu:18.04
指令时,只是仅取消了标签名18.04
,因为该ubuntu镜像还有其他的标签名bionic
当该镜像所有的标签都被取消了,该镜像很可能会失去了存在的意义,因此会触发删除行为。镜像是多层存储结构,因此在删除的时候也是从上层向基础层方向依次进行判断删除。镜像的多层结构让镜像复用变得非常容易,因此很有可能某个其它镜像正依赖于当前镜像的某一层。这种情况,依旧不会触发删除该层的行为。直到没有任何层依赖当前层时,才会真实的删除当前层。
除了镜像依赖以外,还需要注意的是容器对镜像的依赖。如果有用这个镜像启动的容器存在(即使容器没有运行),那么同样不可以删除这个镜像。如果这些容器是不需要的,应该先将它们删除,然后再来删除镜像。
容器是独立运行的一个或一组应用,以及它们的运行态环境。
启动容器有两种方式,一种是基于镜像新建一个容器并启动,另外一个是将在终止状态(exited
)的容器重新启动。
有了镜像后,我们就能够以这个镜像为基础启动并运行一个容器。使用的命令主要为 docker run
。
例如,下面的命令则启动一个 bash 终端,允许用户进行交互。
$ docker run -it --rm ubuntu:18.04 bash
root@10781f4737ca:/# cat /etc/os-release
NAME="Ubuntu"
VERSION="18.04.6 LTS (Bionic Beaver)"
ID=ubuntu
ID_LIKE=debian
PRETTY_NAME="Ubuntu 18.04.6 LTS"
VERSION_ID="18.04"
HOME_URL="https://www.ubuntu.com/"
SUPPORT_URL="https://help.ubuntu.com/"
BUG_REPORT_URL="https://bugs.launchpad.net/ubuntu/"
PRIVACY_POLICY_URL="https://www.ubuntu.com/legal/terms-and-policies/privacy-policy"VERSION_CODENAME=bionic
UBUNTU_CODENAME=bionic
root@10781f4737ca:/# exit
exit
-it
:这是两个参数,一个是 -i
交互式操作,一个是 -t
终端,即交互式终端。--rm
:这个参数是说容器退出后随之将其删除。ubuntu:18.04
:这是指用 ubuntu:18.04
镜像为基础来启动容器。bash
:放在镜像名后的是命令,这里我们希望有个交互式 Shell,因此用的是 bash
。进入容器后,我们可以在 Shell 下操作,执行任何所需的命令。这里,我们执行了 cat /etc/os-release
,这是 Linux 常用的查看当前系统版本的命令,从返回的结果可以看到容器内是 Ubuntu 18.04.1 LTS
系统。最后我们通过 exit
退出了这个容器。
当利用 docker run
来创建容器时,Docker 在后台运行的标准操作包括:
registry
下载可以利用 docker container start
命令,直接将一个已经终止(exited
)的容器启动运行。
更多的时候,需要让 Docker 在后台运行而不是直接把执行命令的结果输出。此时,可以通过添加 -d
参数来实现。
如果不使用 -d
参数运行容器。
$ docker run ubuntu:18.04 /bin/sh -c "while true; do echo hello world; sleep 1; done"
hello world
hello world
hello world
hello world
从上面的例子中可以看到,容器会把输出的结果打印出来。
如果使用了 -d
参数运行容器。
$ docker run -d ubuntu:18.04 /bin/sh -c "while true; do echo hello world; sleep 1; done"
9822df018d4e99c8057af442a90f5b18dd4e73ea40627602bba4312f38364300
此时容器会在后台运行并不会把输出的结果打印(输出结果可以用 docker logs
查看)。
使用 -d
参数启动后会返回一个唯一的 id,也可以通过 docker container ls
命令来查看容器信息。
要获取容器的输出信息,可以通过 docker container logs
命令。
$ docker container logs [container ID or NAMES]
hello world
hello world
hello world
. . .
在docker desktop中也可以查看容器的logs。
可以使用 docker container stop
来终止一个运行中的容器。此外,当 Docker 容器中指定的应用终结时,容器也自动终止。
$ docker container stop [container ID]
处于终止状态的容器,可以通过 docker container start
命令来重新启动。
此外,docker container restart
命令会将一个运行态的容器终止,然后再重新启动它。
在使用 -d
参数时,容器启动后会进入后台。某些时候需要进入容器进行操作,可以使用 docker attach
命令或 docker exec
命令。
# 启动容器
$ docker run -dit ubuntu
862613a2556eb1dcd3aefcd954ee0eda25ee71d21baba0f6ed4fb364b05cd680
# 查看容器状态
$ docker container ls
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
862613a2556e ubuntu "bash" 3 seconds ago Up 2 seconds infallible_cori
# 根据container ID进入某一容器
$ docker attach 862
# 退出
root@862613a2556e:/# exit
exit
# 查看结束attach指令后的容器状态
$ docker container ls
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
如果exit,会导致容器的停止。
# 启动容器
$ docker run -dit ubuntu
6f242cd3ca38dbc32e85d55821ed23336a2b935fa19a8aa97d42df054723477d
# 查看容器状态
$ docker container ls
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
6f242cd3ca38 ubuntu "bash" 4 seconds ago Up 3 seconds naughty_williamson
# 根据container ID进入某一容器
$ docker exec -it 6f24 bash
# 查看当前目录
root@6f242cd3ca38:/# ls
bin boot dev etc home lib lib32 lib64 libx32 media mnt opt proc root run sbin srv sys tmp usr var
# 退出
root@6f242cd3ca38:/# exit
exit
# 查看结束exec指令后的容器状态
$ docker container ls
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
6f242cd3ca38 ubuntu "bash" 5 minutes ago Up 5 minutes naughty_williamson
如果exit,不会导致容器的停止。
推荐使用 docker exec
指令。
可以使用docker container rm
来删除一个处于终止状态
的容器。
$ docker container rm [container ID]
如果要删除一个运行中的容器,可以添加 -f
参数。Docker 会发送 SIGKILL
信号给容器。
用 docker container ls -a
命令可以查看所有已经创建的包括终止状态的容器,如果数量太多要一个个删除可能会很麻烦,用下面的命令可以清理掉所有处于终止状态的容器。
$ docker container prune
镜像是容器的基础,每次执行 docker run
的时候都会指定哪个镜像作为容器运行的基础。在之前的例子中,我们所使用的都是来自于 Docker Hub 的镜像。直接使用这些镜像是可以满足一定的需求,而当这些镜像无法直接满足需求时,我们就需要定制这些镜像。
下面通过基于Flask框架的一个demo做演示。
文件目录结构
demo/
app.py
requirements.txt
Dockerfile
后端代码如下,打开网页显示Hello, World!
:
from flask import Flask
app = Flask(__name__)
@app.route("/")
def hello_world():
return "Hello, World!"
if __name__ == '__main__':
app.run(host='0.0.0.0')
requirements.txt内容如下:
flask
Dockerfile
内容如下:
# FROM 指定基础镜像
FROM python:3.8-slim-buster
# 指定Docker命令的工作路径,如果路径不存在,Docker会自动创建
WORKDIR /app
# 将所有程序拷贝到Docker镜像中
# COPY <本地路径> <目标路径>
# 第一个'.'代表程序根目录下的所有文件;第二个'.'代表Docker镜像中的路径,即当前的工作路径'/app/'
COPY . .
# 创建镜像,安装环境,RUN指令是在创建镜像时使用
RUN pip install -r requirements.txt
# CMD指令用来指定当Docker容器运行起来执行的命令
CMD ["python","app.py"]
在Dockerfile
文件目录下输入指令,-t
参数是指定镜像名称,.
告诉docker在该目录下寻找Dockerfile
文件
$ docker build -t demo .
docker按照我们编写的指令逐步执行,如下图。
然后我们通过docker run
指令来启动一个容器。-p
是将容器上的某一个端口映射到本地主机上,这样我们才能从主机上访问容器中的Web服务,前面的8080是本地主机上的端口,后面的5000是容器上的端口;-d
让容器在后台运行,这样输出不会直接显示在控制台;--name
参数指定容器的名称,如果未设置Docker会随机生成一个容器名称。
$ docker run -p 8080:5000 -d --name web demo
此时在浏览器打开localhost:8080
,就可以在网页中看到Hello World!
注意:Flask后端的代码主机号要写成0.0.0.0
,否则容器启动后在本机上打开报错。
数据卷是一个可供一个或多个容器使用的特殊目录,有以下特性:
# 创建一个数据卷
$ docker volume create 数据卷名字
# 查看所有的数据卷
$ docker volume ls
# 在主机里使用以下命令可以查看指定数据卷的信息
$ docker volume inspect 数据卷名字
# 删除数据卷
$ docker volume rm my-vol
# 删除不使用的数据卷
$ docker volume prune
下面演示启动一个挂载数据卷的容器:
# 创建数据卷
$ docker volume create test
test
# 查看数据卷
$ docker volume ls
DRIVER VOLUME NAME
local test
# 用ubuntu镜像创建一个名为test的容器,且将新创建的数据卷挂载到/volume目录下,如果没有该目录Docker会自动创建
$ docker run -dit --name test -v test:/volume ubuntu
57cc84da548f5f60da942e77cd5bf3ec9be5baff31adb3dfa21178bf1a5587cc
# 进入容器,创建测试文件
$ docker exec -it 57cc bash
root@57cc84da548f:/# cd volume
root@57cc84da548f:/volume# touch test.txt
此时可以看到数据卷中也出现了测试文件。
在本地创建一个挂载目录,这里我用的目录是D:\Volume
。
挂载一个本地主机目录作为数据卷,命令如下:
# 用ubuntu镜像创建一个名为test的容器,且将本地目录挂载到/volume目录下,如果没有该目录Docker会自动创建
$ docker run -dit --name test01 -v /d/Volume:/volume ubuntu
为了能够在容器中修改文件,我在容器里安装vim
,用到指令如下:
# 更新apt源
apt-get update
# 安装vim
apt-get install -y vim
在容器中创建文件:
$ docker exec -it test01 bash
root@7c6623ff53d2:/# cd volume
# 创建测试文件,内容为“Container test”
root@7c6623ff53d2:/volume# vim test.txt
本地文件夹中也会同步更新。
在很多情况下,我们需要多个容器相互配合来完成某项任务。例如要实现一个 Web 项目,除了 Web 服务容器本身,往往还需要再加上后端的数据库服务容器,甚至还包括负载均衡容器等。Compose
恰好满足了这样的需求。它允许用户通过一个单独的 docker-compose.yml
模板文件(YAML 格式)来定义一组相关联的应用容器为一个项目(project)。
这里我使用一个简单的个人博客系统作为演示,包括Flask后端和MySQL数据库两部分。
仓库地址:https://github.com/LenkyAndrews/notebook-docker
文件目录如下:
notebook/
docker-compose.yml
flask/
...
dockerfile
mysql/
notebook.sql
dockerfile
后端flask文件夹中的dockerfile如下:
FROM python:3.8-slim-buster
WORKDIR /app
COPY . .
RUN pip install -r requirements.txt -i https://mirrors.aliyun.com/pypi/simple/
CMD ["python","app.py"]
数据库mysql文件夹中的dockerfile如下:
FROM mysql
COPY ./notebook.sql /docker-entrypoint-initdb.d
notebook.sql
是数据库初始化文件,它会把flask应用需要用到的库和表创建出来。在mysql官方镜像中提供了容器启动时自动执行/docker-entrypoint-initdb.d
文件夹下的脚本的功能(包括shell脚本和sql脚本) 。因此我们只要把初始化文件在镜像启动时复制到/docker-entrypoint-initdb.d
文件夹就可以了。
要把项目依赖的多个服务集合到一起,我们需要编写一个docker-compose.yml
文件,描述依赖哪些服务。
# 版本号
version: '3'
services:
# 定义数据库容器
mysql:
# 执行数据库文件夹下的dockerfile
build: ./mysql
# 设置主机和容器的端口映射
ports:
- "3307:3306"
environment:
# 设置root用户密码
- MYSQL_ROOT_PASSWORD=123456
# 定义后端容器
flask:
# 执行后端文件夹下的dockerfile
build: ./flask
# 设置主机和容器的端口映射
ports:
- "5000:5000"
完成以上步骤后,在docker-compose.yml
文件所在目录,执行命令:
$ docker-compose up -d
# 后端报错提示
pymysql.err.OperationalError: (2003, "Can't connect to MySQL server on '0.0.0.0' ([Errno 111] Connection refused)")
网页报错提示如下:
解决方案:
在github
和stack overflow
上查询类似的问题后,发现是我的host和port写错了。host应该写成数据库服务的名称,即docker-compose.yml
中的mysql
;因为我将容器的端口3306
映射到本地主机的端口3307
,之前我一直写的是3307
端口没成功,这两个容器是处在同一个网络中的,所以改成3306
端口后就可以相互通信了。
# 数据库连接代码
class MysqlUtil():
def __init__(self):
host = 'mysql' # 主机名
port = 3306 # 端口号
user = 'root' # 数据库用户名
password = '123456' # 数据库密码
database = 'notebook' # 数据库名称
self.db = pymysql.connect(host=host, port=port, user=user, password=password, db=database) # 建立连接
self.cursor = self.db.cursor(cursor=pymysql.cursors.DictCursor) # 设置游标,并将游标设置为字典类型
解决方法:
安装cryptography
包,或者设置加密方式(在docker-compose.yml
或手动修改)。
在Docker Hub上注册账号
创建一个镜像库
$ docker login -u username
$ docker tag notebook_flask:latest username/notebook:v1
$ docker push username/notebook:v1
$ docker run -dp 8080:5000 --name hub_notebook username/notebook:v1
# 拉取镜像
$ docker pull mysql
# 运行一个MySQL容器
# -p 3306:3306 :映射容器服务的3306端口到宿主机的3307端口,外部主机可以直接通过宿主机ip:3307访问到MySQL的服务。
# MYSQL_ROOT_PASSWORD=123456:设置MySQL服务root用户的密码。
$ docker run -itd --name mysql-test -p 3307:3306 -e MYSQL_ROOT_PASSWORD=123456 mysql
在本地主机连接mysql
出现sqlyog:2058的错误。意思是客户端不支持caching_sha2_password的加密方式。
解决步骤:
$ docker exec -it mysql-test bash
mysql -uroot -p
select user,host,plugin from mysql.user where user = 'root';
ALTER USER 'root'@'localhost' IDENTIFIED WITH mysql_native_password BY 'root密码';
ALTER USER 'root'@'%' IDENTIFIED WITH mysql_native_password BY 'root密码';
flush privileges;