前言

之前发过一篇有关Docker命令的文章,但感觉太水了,也不能系统地学习Docker这门技术,并且之前也只是会用,自己并不会构建Docker的镜像,并且很多命令行的操作并不习惯,所以现在重新系统的学习一下Docker,从使用到构建一个自己的Docker镜像

这篇文章并不是0基础学习Docker的文章,建议有过使用NAS上的Docker经验再看,这样能更快理解在做什么

安装Docker

Docker主要是Linux中使用,我接触Docker是因为NAS,大多是有图形化界面,结果发现自己并不熟悉在命令行配置Docker,所以这次我会使用Ubuntu重新学习使用Docker,细教程可以看Docker官方文档,这里不再赘述

安装好后可以通过下面的命令测试一下Docker

1
docker -v

配置镜像加速

如果使用的是非大陆地区的VPS,那么下面的步骤可以跳过了,换源后可能导致速度下降!!!

1
2
3
4
5
6
7
8
sudo mkdir -p /etc/docker
sudo tee /etc/docker/deamon.json <<- 'EOF'
{
"registry-mirrors": ["https://xxx.com"]
}
EOF
sudo systmctl demon-reload
sudo systemctl restart docker

其中https://xxx.com需要换成镜像地址,我收集了几个放在下面

镜像源 地址
Docker官方镜像源 https://registry.docker-cn.com
中国科学技术大学 https://docker.mirrors.ustc.edu.cn
腾讯镜像源 https://mirror.ccs.tencentyun.com
网易镜像源 http://hub-mirror.c.163.com

使用Docker

查看帮助文档

这个是最基础的命令,以后可以快速回忆起命令
查看docker所有命令的帮助文档

1
docker --help

查看pull命令的帮助文档

1
docker pull --help

类似的还可以查看psrun等命令的具体稳定
但这里的说明往往比较简略,需要详细的说明可以在官方文档中查看,本文只会介绍一血常用的命令

搜索命令

通过这个命令可以快速的找一些别人公开到镜像仓库的镜像,例如我需要一个mysql数据库的镜像

1
2
docker search mysql
# docker pull <关键词>

当然,这种在命令行之间查找的方式并不方便,因此也可以直接通过网页查询https://hub.docker.com

下载镜像

找到了需要的镜像,那么就要下载下来,就需要用到docker pull这个命令:

1
docker pull <镜像仓库地址>/<镜像名>:<标签>

有两点需要注意:

  • 镜像仓库地址可以不指定,若不指定镜像仓库地址,则默认从Docker官方镜像仓库中查询
  • 镜像标签可以不指定,若不指定镜像标签,则默认使用标签latest
    假设我需要从官方仓库中下载一个mysql版本为5.7镜像:
1
docker pull mysql:5.7

列出镜像信息

如果需要看当前的机器上下载了哪些镜像,可以通过下面的命令:

1
docker images

可以看到所有镜像的5条信息:镜像名、镜像标签、镜像ID、镜像创建时间、镜像大小
如果需要列出所有镜像(默认隐藏的镜像),可以使用参数-a(–all):

1
dock images -a

如果需要列出所有镜像名以busy开头并且标签以libc结尾的镜像可以使用参数-f(–filter):

1
docker -a -f=reference='busy*:*libc'

列出容器信息

默认情况下列出的是正在运行的容器的信息,若容器已经停止,则不会列出:

1
docker ps

如果需要列出所有容器,可以使用参数-a

1
docker ps -a

如果要列出所有推出的容器也可以使用过滤器-f

1
docker ps -f "status=exited"

创建并运行命令

1
docker run <可选项> <容器名> <命令> <命令参数>
  • 可选项:主要有-d-p-v-e--restart这几个选项
  • 容器名:若没有标签,则默认使用的标签为latest,若本机没有目标镜像则会自动下载
  • 命令:容器创建后需要执行的命令
  • 命令参数:上面执行命令的参数
    例如,我需要运行一个版本为5.7的mysql镜像:
1
docker run mysql:5.7

上面提供了一个最简的运行容器的方式,接下来将对参数具体说明

容器运行方式

默认运行

docker run默认的运行方式是由镜像中设置的,如果需要自定义,可以设置<可选项>的参数

后台运行

<可选项>中添加一个参数-d
这里提供一个后台运行mysql镜像容器的例子:

1
docker run -d mysql

交互式运行

这种运行方式主要是在调试时使用
<可选项>中添加一个参数it

  • -i:以交互式运行容器,通常与-t同时使用
  • -t:启动容器后为容器分配一个命令行,通常与-i同时使用
    一般进入容器后需要使用bash作为命令解析器,所以使用交互式的运行方式会导致配置镜像时设置的CMD命令失效
    这里还是提供一个以交互式运行的例子:
1
docker run -it mysql:6.1 bash

对外发布端口

<可选项>中添加一个参数-p(–port):

1
docker run -p <宿主机端口>:<容器端口> <镜像名>:<镜像标签>

假设需要将nginx的80、443端口分别映射到主机的88、8443端口:

1
docker run -p 88:80 -p 8443:443 nginx:latest

在有需要的情况下可以声明使用tcp还是udp协议,若不声明,默认使用tcp

1
docker run -p 88:80/tcp -p 88:80/udp nginx:latest

映射数据卷

<可选项>中添加一个参数-v(–volume):

1
docker run -v <宿主机路径>:<容器路径>:<读写权限> <镜像名>:<镜像标签>

读写权限为可选项:

  • ro(read-only):容器对文件只读不可写
  • rw(read-write):容器对文件既可读也可写
  • 默认:当不配置权限是默认为rw
    和端口类似,同一个容器也可以映射多个数据卷

设置环境变量

<可选项>中添加一个参数-e(–enviroment):

1
docker run -e <环境变量名名>=<环境变量值> <镜像名>:<镜像标签>

和端口、数据卷类似,同一个容器也可以配置多个环境变量

设置容器名

<可选项>中添加一个参数--name

1
docker run --name <容器名> <镜像名>:<镜像标签>

设置容器重启策略

<可选项>中添加一个参数--restart

1
docker run --restart <重启策略> <镜像名>:<镜像标签>

重启策略主要有以下几种:

  • no:容器退出时不会自动重启
  • always:容器总是在退出后自动重启
  • on-failure[:max-retries]:容器仅在非正常退出时重启,可以指定最大重试次数
  • unless-stopped:容器会在推出后自动重启,除非手动停止了容器
  • 默认策略:no

进入容器内部执行命令

有时候我们需要进入容器内部执行一些命令检查一下运行状态,需要进入容器内部,就需要这个命令:

1
docker exec <可选项> <容器ID或容器名> <命令> <命令参数>

假设我们需要查看容器名为my-nginx的文件就可以使用下面这段命令:

1
docker exec my-nginx ls -a

这种方式每次只能使用一个命令,比较繁琐,还有一种“进入”容器”内部的方式,类似于创建容器时的以交互式运行的方法,这里需要增加一个参数-it

1
docker exec -it my-nginx bash

此时想要“从容器出来”就可以直接输入:

1
exit

与直接以交互式方式运行容器不同的是,这样退出容器后,该容器依旧在后台运行

查看容器日志

有时候我们需要查看程序运行的日志,一般容器是以后台方式运行的,想要查看日志,除了直接进入容器内部查看,还可以使用下面的命令:

1
docker logs <可选项> <容器ID或容器名>

如果需要持续查看日志可以增加一个参数-f(–follow):

1
docker logs -f my-nginx

如果只想查看最新的20条日志,可以增加一个参数-n(–tail):

1
docker logs -n 20 my-nginx

容器和宿主机之间的文件拷贝

把容器中的文件拷贝到宿主机中

1
docker cp <容器ID或容器名>:<容器中的文件路径> <宿主机目标地址路径>

把宿主机中的文件拷贝到容器中

1
docker cp <宿主机的文件路径> <容器ID或容器名>:<容器中的目标路径>

停止容器

1
docker stop <可选项> <容器ID或容器名...>

可以同时填写多个容器同时停止这些容器

继续运行容器

1
docker start <可选项> <容器ID或容器名>

docker startdocker run的区别在于前者是运行一个已创建但停止的容器,后者是创建一个新的容器并运行

删除容器

1
docker rm <可选项> <容器ID或容器名...>

假设需要删除一个容器ID为111222两个容器

1
docker rm 111 222

需要注意的是,这种方式并不能删除正在运行的容器,可以先停止容器或使用参数-f(–force)强制删除容器

1
docker rm -f my-nginx

如果想要删除所有容器,需要提前了解一下Linux中$()命令替换的知识,我们可以通过docker ps -aq获取所有容器的容器ID,需要删除一个容器就是docker rm <容器ID>,所以删除所有容器就是

1
docker rm -f $(docker ps -aq)

如果想要删除所有状态为退出的容器也是类似的:

1
docker rm $(docker ps -q -f "status=exited")

Docker网络

虽然默认情况下容器和容器可以进行网络通信,但每次创建容器都是Docker给容器分配的IP地址,这使得我们使用起来不太方便。这种情况我们可以通过创建一个自定义网络来解决这些问题,把需要互相联通的容器加入同一个网络中,这样容器之间可以通过容器名代替IP地址进行连接访问(其实就是Docker会自动把容器名作为一个域名去解析成对应的IP地址)

创建网络

1
docker network create <网络名>

创建完之后可以通过下面这个命令查看目前有几个Docker网络

1
docker network ls

加入网络

创建容器时加入

<可选项>中添加一个参数--network

1
docker run --network <网络名> <镜像名>:<镜像标签>

将已创建的容器加入网络

1
docker network connect <可选项> <网络名> <容器ID或镜像名>

删除网络

1
docker network rm <网络名>

DockerFile

前面主要学习了如何使用别人已经构建好的Docker镜像,这一节主要讲解如何自己构建一个Docker镜像。可以将Docker理解成一个虚拟机,而这个虚拟机是通过DockerFile构建的,容器启动后会按照DockerFile的设置运行相关程序

基本语法

  • 不区分大小写,但是习惯大写
  • 基本以FROM指令开头
  • #表示注释

快速入门

这里先构建一个最简单的Helloworld镜像,其目的是输出字符串Hello World,在Linux中想要在终端中打印某个字符可以用echo

  1. 先创建一个Dokckerfile文件
1
vim Dockerfile
  1. 编写Dockerfile
1
2
3
4
FROM ubuntu:22.04
# 将Ubuntu:22.04作为基础镜像
CMD echo "Hello World"
# 运行echo命令
  1. 编译镜像
1
docker build -t helloworld:1.0 -f Dockerfile .
  • 参数-t代表tag标签,后面接着就是docker名和标签
  • 参数-f(file)后面接着是dockerfile的名称,最开始我们创建了一个叫Dockerfile的文件,所以我们这里就写Dockerfile
  • 后面是Dockerfile的路径,因为我们是在当前目录下创建的,所以写.
    稍等片刻,一个叫helloworld:1.0的镜像就会出现,可以通过docker image -a显示本机所有镜像,然后可以尝试运行一下自己构建的docker:
1
docker run helloworld:1.0

不出意外的话,终端就会输出对应字符
但使用docker ps应该找不到刚刚启动的容器,因为容器已经退出了,这里简单讲一下容器的生命周期:docker容器运行必须有一个前台进程,如果没有前台进程执行,容器就认为空闲,就会自行退出,刚刚启动的容器在运完echo “Hello World”后前台已经没有进程了,就自行退出了

常用指令

FROM

  • 说明:指定基础镜像,基于哪个镜像做变更
  • 用法:FROM <镜像名>:<镜像标签>
  • 作用时机:构建镜像时

CMD

  • 说明:定义容器运行时的默认命令,可以在使用docker run的时候覆盖掉CMD中定义的命令
  • 用法:CMD [“<命令>”,“<参数1>”,“<参数2>”,...](命令和参数作为json数组的元素去书写)或CMD <命令> <参数1> <参数2> ...(前者形式不能解析环境变量,后者可以);一个Dockerfile中有多个CMD,那么只有最后一个生效
  • 作用时机:容器运行时
    以下面这个基础的镜像作为例子:
1
2
FROM ubuntu:22.04
CMD echo "Hello World"

在启动容器时定义运行命令

1
docker run helloworld:1.0 echo “Hello Docker”

则会输出Hello Docker并且不会输出Hello World

ENV

  • 说明:在容器运行时通过修改环境变量达到相应运行效果
  • 用法:ENV <变量名>="变量值"
  • 作用时机:构建镜像时
1
2
3
4
5
FROM ubuntu:22.04
ENV CONTENT="Hello World"
CMD echo $CONTENT
#CMD [“echo”,"$CONTENT"]不能这么写,这种格式无法解析环境变量
#CMD [“sh”,"-c","echo $CONTENT"]这么写可以

构建镜像后就可以通过改变环境变量来改变输出的值了

1
docker run -e CONTENT=Docker helloworld:1.0

这样就会输出Docker

WORKDIR

  • 说明:设置在容器中后的工作目录,若指定的路径未创建则会自动创建
  • 用法:WORKDIR <路径>(路径可以是绝对路径也可以是相对路径)
  • 作用时机:构建镜像时

RUN

  • 说明:在构建镜像时运行相应指令,与CMD差异在于作用时机不同
  • 用法:RUN <命令>
  • 作用时机:构建镜像时

ADD

  • 说明:将文件添加至镜像中,可以从本地路径添加,也可以从远端地址添加;如果是从本地文件中添加文件且若目标是tar、zip等压缩包则会自动解压
  • 用法:ADD <原路径> <镜像中目标路径>
  • 作用时机:构建镜像时

EXPOSE

  • 说明:声明暴露的端口(即使不声明,在启动容器时映射了相关端口也能正常访问)
  • 用法:EXPOSE <端口>/tcpEXPOSE <端口>/udp(在不声明的情况下默认是tcp)

COPY

  • 说明:类似于ADD,但只能从本地加载资源,并且不会自动解压压缩包
  • 用法:COPY <本地路径> <目标路径>

ENTRYPOINT

  • 说明:类似于CMD,但是在启动容器时并不会被覆盖
  • 用法:ENTRYPOINT [“<命令>”,“<参数1>”,“<参数2>”,...](命令和参数作为json数组的元素去书写)或ENTRYPOINT <命令> <参数1> <参数2> ...
  • 作用时机:容器启动时
    可以和CMD配合使用,以达到启动命令只有部分能被覆盖
1
2
ENTRYPOINT java -jar
CMD app1.jar

这样容器启动时就会默认运行java -jar app1.jar

1
docker run java_test:1.0 app2.jar

这样运行的就是java -jar app2.jar`

将镜像推送到镜像仓库

有时间我们自定义的镜像需要在多台设备上使用,这时我们就可以将构建的镜像推送到Docker Hub的镜像仓库中,需要在其他机器上部署时只需要pull下来即可

  1. 注册Docker Hub账号,这个在网页端进行,https://hub.docker.com
  2. 登陆账户:docker login,然后输入账户密码
  3. 构建镜像:docker build -t <镜像名>:<标签> -f <DockerFile文件> <DockerFile路径>
  4. 给镜像打标签:docker tag <镜像名>:<标签> <DockerHub用户名>/<镜像名>:<镜像标签>,这一步其实是修改镜像标签,让其符合DockerHub个人镜像规范
  5. 推送镜像:docker push <DockerHub用户名>/<镜像名>:<镜像标签>

DockerCompose

一般一个项目会有前端后端数据库等多个容器组成,并且要管理起网络、端口、数据卷和端口等信息,如果使用docker run一个一个启动容器也是可以的,但是启动起来本不是特别方便,因此可以使用docker compose一键部署多个容器

安装

当前版本的docker已经自带了DockerCompose,并不需要单独安装,并且命令已经由docker-compose变更为docker compose,可以通过下面命令查看版本

1
docker compose version

快速入门

编写docker-compose.yaml

1
2
3
4
5
services:
<容器1>:
image:<镜像1>
<容器2>:
image:<镜像2>

运行

1
docker compose up -d

这样两个容器就在后台运行了

停止

1
docker compose down

模版文件书写

基本元素

  • image:指定镜像
  • environment:指定环境变量
  • ports:指定发布端口
  • volumes:指定数据卷
  • command:覆盖容器启动后的默认指令
  • restart:指定重启策略
  • networks:指定docker网络

模版

其实看得懂DockerCompose就好,对着写几次就会了,这里提供一个有前后端服务的模版模版

DockerRun

先看看docker run

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#Docker网络
docker newtwork create blog_demo

#数据库
docker run -d -v mysql_data:/var/lib/mysql -p 3306:3306 -e MYSQL_ROOT_PASSWORD=root --restart always --name blog_mysql --network blog_demo mysql:5.7

#redis
docekr run -d -v redis_data:/data -p 6379:6379 --restart always --name blog_redis --network blog_demo redis:7.0 redis-server --appendonly yes

#前端
docker run -d -p 80:80 -v /usr/bog/blog-vue/dist:/usr/share/nginx/html --restart always --name blog_vue --network blog_demo nginx:1.21.5

#后端
docker run -d -p 3000:3000 --network blog_demo --name my_blog --restart always myblog:1.0

DockerCompose

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
63
64
65
66
67
#version: "3.8"
#新版本中版本不是必须,可以不用写
name: blog
#名称可写可不写,最好命命为项目相关的,比如说blog、git、clouddrive

services:
#数据库
blog_mysql:
image: mysql:5.7
volumes:
- ./mysql_data:/var/lib/mysql #这样会把数据放在DockerCompose目录的子目录中,方便整个项目的文件管理,并且只要迁移整个文件夹到另一个机器上就能全部启动
ports:
- 3306:3306 #其实可以不用映射端口到宿主机,因为后面是直接通过Docker网络访问
environment:
- MYSQL_ROOT_PASSWORD: root
restart: always
networks:
- blog_demo
#不推荐命名,直接通过DockerCompose的服务名就可以访问

#redis
blog_redis:
image: redis:7.0
volumes:
- ./redis_data:/data
ports:
- 6379:6379
restart: always
networks:
- blog_demo
commands: ['redis-server','--appendonly','yes']

#前端
blog_vue:
image: nginx:1.21.5
volumes:
- ./blog_vue/dist:/usr/share/nginx/html
ports:
- 80:80
restart: always
networks:
- blog_demo

#后端
my_blog:
image: myblog:1.0
ports:
- 3000:3000
restart: always
networks:
- blog_demo

#定义网络
networks:
blog_demo:
external: true
#表示使用已经存在的网络,而不是使用DockerCompose创建的


#定义数据卷
#这里我们都使用了相对路径,所以不用定义,如果严格按照DockerRun书写,这里就要填写实际路径
#volumes:
#mysql_data:
#external: true
#表示使用已经存在的目录
#redis_data:
#external: true