docker基础

docker

一、Docker概述

1、基本介绍

Docker 是一个开源的应用容器引擎,基于 Go 语言 并遵从 Apache2.0 协议开源。

Docker 可以让开发者打包他们的应用以及依赖包到一个轻量级、可移植的容器中,然后发布到任何流行的 Linux 机器上,也可以实现虚拟化。

容器是完全使用沙箱机制,相互之间不会有任何接口(类似 iPhone 的 app),更重要的是容器性能开销极低。

Docker 从 17.03 版本之后分为 CE(Community Edition: 社区版) 和 EE(Enterprise Edition: 企业版),我们用社区版就可以了。官网:https://docs.docker.com/

2、应用场景

  • Web 应用的自动化打包和发布。
  • 自动化测试和持续集成、发布。
  • 在服务型环境中部署和调整数据库或其他的后台应用。
  • 从头编译或者扩展现有的 OpenShift 或 Cloud Foundry 平台来搭建自己的 PaaS 环境。

3、Docker的优势

Docker 是一个用于开发,交付和运行应用程序的开放平台。Docker 使您能够将应用程序与基础架构分开,从而可以快速交付软件。借助 Docker,您可以与管理应用程序相同的方式来管理基础架构。通过利用 Docker 的方法来快速交付,测试和部署代码,您可以大大减少编写代码和在生产环境中运行代码之间的延迟。

1、快速,一致地交付您的应用程序。

Docker 允许开发人员使用您提供的应用程序或服务的本地容器在标准化环境中工作,从而简化了开发的生命周期。

容器非常适合持续集成和持续交付(CI / CD)工作流程,请考虑以下示例方案:

您的开发人员在本地编写代码,并使用 Docker 容器与同事共享他们的工作。

他们使用 Docker 将其应用程序推送到测试环境中,并执行自动或手动测试。

当开发人员发现错误时,他们可以在开发环境中对其进行修复,然后将其重新部署到测试环境中,以进行测试和验证。

测试完成后,将修补程序推送给生产环境,就像将更新的镜像推送到生产环境一样简单。

2、响应式部署和扩展

Docker 是基于容器的平台,允许高度可移植的工作负载。Docker 容器可以在开发人员的本机上,数据中心的物理或虚拟机上,云服务上或混合环境中运行。

Docker 的可移植性和轻量级的特性,还可以使您轻松地完成动态管理的工作负担,并根据业务需求指示,实时扩展或拆除应用程序和服务。

3、在同一硬件上运行更多工作负载

Docker 轻巧快速。它为基于虚拟机管理程序的虚拟机提供了可行、经济、高效的替代方案,因此您可以利用更多的计算能力来实现业务目标。Docker 非常适合于高密度环境以及中小型部署,而您可以用更少的资源做更多的事情。

二、虚拟化技术和容器化技术

虚拟化技术特点

  • 资源占用多
  • 冗余步骤多
  • 启动慢

容器化技术

容器化技术不是模拟一个完整的系统

Docker和虚拟机的不同

  1. 传统的虚拟机,先模拟出硬件,然后安装系统,最后在系统上安装软件运行
  2. Docker容器的内容直接运行在宿主机上,没有自己的内核,也不用虚拟出硬件
  3. 每个容器都是互相隔离的,每个容器都有自己的文件系统,互不影响

容器化带来的好处

  1. 引用更快速的交付和部署
  2. 更便捷的升级和扩缩容
  3. 更简单的系统运维
  4. 更高效地利用计算资源

三、Docker的组成

Docker组成

说明:

镜像(Image)

Docker镜像就类似于一个模板,可以通过这个模板创建容器服务,一个镜像可以创建多个容器服务,最终服务或者项目运行就是在容器中

容器(Container)

Docker利用容器技术,运行一个或者一组容器,通过镜像来创建

包含启动,停止,删除等基本命令

可以把容器理解为一个小型的简易的linux系统

仓库(Repository)

仓库就是存放镜像的地方

仓库分为共有仓库和私有仓库

四、Docker的安装

1、卸载旧版本

yum remove docker \
docker-client \
docker-client-latest \
docker-common \
docker-latest \
docker-latest-logrotate \
docker-logrotate \
docker-engine

2、设置存储库

yum install -y yum-utils
yum-config-manager \
    --add-repo \
    https://download.docker.com/linux/centos/docker-ce.repo

3、安装Docker

yum install -y docker-ce docker-ce-cli containerd.io

4、启动Docker

systemctl start docker

5、测试Docker

docker version
docker run hello-world
Unable to find image 'hello-world:latest' locally #未找到镜像
latest: Pulling from library/hello-world #前往远程仓库拉取
2db29710123e: Pull complete 
Digest: sha256:bfea6278a0a267fad2634554f4f0c6f31981eea41c553fdf5a83e95a41d40c38
Status: Downloaded newer image for hello-world:latest

Hello from Docker! #运行镜像
This message shows that your installation appears to be working correctly.

6、查看镜像

docker images

7、卸载Docker

yum remove docker-ce docker-ce-cli containerd.io
rm -rf /var/lib/docker
rm -rf /var/lib/containerd

五、Docker容器运行流程

启动一个容器,Docker的运行流程如下图:

docker运行流程图

六、底层原理

Docker是一个Client-Server结构的系统,Docker的守护进程运行在主机上,通过Socker从客户端访问!Docker Server接收到Docker-Client的指令,就会执行这个指令!

docker结构

Docker为什么比VM Ware快?

  1. Docker比虚拟机更少的抽象层
  2. docker利用宿主机的内核,VM需要的是Guest OS

docker与Vm对比

Docker新建一个容器的时候,不需要像虚拟机一样重新加载一个操作系统内核,直接利用宿主机的操作系统,而虚拟机是需要加载Guest OS。Docker和VM的对比如下:

docker与vm性能

七、Docker常用命令

1、基础命令

docker version          #查看docker的版本信息
docker info             #查看docker的系统信息,包括镜像和容器的数量
docker [commnd] --help       #帮助命令(可查看可选的参数)

2、镜像命令

docker images:查看本机所有镜像

[root@iZwz99sm8v95sckz8bd2c4Z ~]# docker images
REPOSITORY    TAG       IMAGE ID       CREATED         SIZE
hello-world   latest    bf756fb1ae65   11 months ago   13.3kB

#解释:
1.REPOSITORY  #镜像的仓库源

2.TAG  #镜像的标签

3.IMAGE ID #镜像的id

4.CREATED #镜像的创建时间

5.SIZE #镜像的大小


# 可选参数
-a/--all #列出所有镜像

-f/--filter filter #过滤

-q/--quiet #只显示镜像的id

docker search:搜索镜像

[root@iZwz99sm8v95sckz8bd2c4Z ~]# docker search mysql
NAME                              DESCRIPTION                                     STARS     OFFICIAL   AUTOMATED
mysql                             MySQL is a widely used, open-source relation…   10308     [OK]
mariadb                           MariaDB is a community-developed fork of MyS…   3819      [OK]
mysql/mysql-server                Optimized MySQL Server Docker images. Create…   754                  [OK]
percona                           Percona Server is a fork of the MySQL relati…   517       [OK]
centos/mysql-57-centos7           MySQL 5.7 SQL database server                   86
mysql/mysql-cluster               Experimental MySQL Cluster Docker images. Cr…   79
centurylink/mysql                 Image containing mysql. Optimized to be link…   60                   [OK]


#可选参数

-f, --filter filter   #根据提供的条件过滤输出
    --format string   #使用Go模板进行打印搜索
    --limit int       #最大搜索结果数(默认值25)
    --no-trunc        #不要截断输出
      
      
#搜索收藏数大于3000的镜像
[root@iZwz99sm8v95sckz8bd2c4Z ~]# docker search mysql --filter=STARS=3000
NAME      DESCRIPTION                                     STARS     OFFICIAL   AUTOMATED
mysql     MySQL is a widely used, open-source relation…   10308     [OK]
mariadb   MariaDB is a community-developed fordockerk of MyS…   3819      [OK]

docker pull 镜像名[:tag]:下载镜像

[root@iZwz99sm8v95sckz8bd2c4Z ~]# docker pull mysql
Using default tag: latest            #如果不写tag默认就是latest
latest: Pulling from library/mysql
6ec7b7d162b2: Pull complete          #分层下载,docker image的核心-联合文件系统
fedd960d3481: Pull complete
7ab947313861: Pull complete
64f92f19e638: Pull complete
3e80b17bff96: Pull complete
014e976799f9: Pull complete
59ae84fee1b3: Pull complete
ffe10de703ea: Pull complete
657af6d90c83: Pull complete
98bfb480322c: Pull complete
6aa3859c4789: Pull complete
1ed875d851ef: Pull complete
Digest: sha256:78800e6d3f1b230e35275145e657b82c3fb02a27b2d8e76aac2f5e90c1c30873 #签名
Status: Downloaded newer image for mysql:latest
docker.io/library/mysql:latest  #下载来源的真实地址  

#以下两个命令相同
docker pull mysql
docker pull docker.io/library/mysql:latest

docker rmi:删除镜像

#1.删除指定的镜像id
[root@iZwz99sm8v95sckz8bd2c4Z ~]# docker rmi -f  镜像id
#2.删除多个镜像id
[root@iZwz99sm8v95sckz8bd2c4Z ~]# docker rmi -f  镜像id 镜像id 镜像id
#3.删除全部的镜像id
[root@iZwz99sm8v95sckz8bd2c4Z ~]# docker rmi -f  $(docker images -aq)

3、容器命令

示例:先拉取一个centos镜像

docker pull centos

运行容器

docker run [可选参数] image

#参数说明
--name="名字"           #指定容器名字
-d                     #后台方式运行
-it                    #使用交互方式运行,进入容器查看内容
-p                     #指定容器的端口
    (
    -p #ip:主机端口:容器端口  配置主机端口映射到容器端口
    -p #主机端口:容器端口,例:-p 3344:80,将容器中的80端口映射到宿主机中的3344端口
    -p #容器端口
    )
-P                     #随机指定端口(大写的P)

运行并进入容器

[root@iZwz99sm8v95sckz8bd2c4Z ~]# docker run -it centos /bin/bash
[root@bd1b8900c547 /]# ls      
bin  dev  etc  home  lib  lib64  lost+found  media  mnt  opt  proc  root  run  sbin  srv  sys  tmp  usr  var

退出容器

exit #停止并退出容器(后台方式运行则仅退出)
Ctrl+P+Q #不停止容器退出

列出运行过的容器

#docker ps # 列出当前正在运行的容器

-a   # 列出所有容器的运行记录
-n=? # 显示最近创建的n个容器
-q   # 只显示容器的编号

[root@iZwz99sm8v95sckz8bd2c4Z ~]# docker ps
CONTAINER ID   IMAGE     COMMAND   CREATED   STATUS    PORTS     NAMES

[root@iZwz99sm8v95sckz8bd2c4Z ~]# docker ps -a
CONTAINER ID   IMAGE          OMMAND       CREATED         STATUS                     PORTS     NAMES
bca129320bb5   centos         "/bin/bash"   4 minutes ago   Exited (0) 3 minutes ago             optimistic_shtern
bd1b8900c547   centos         "/bin/bash"   6 minutes ago   Exited (0) 5 minutes ago             cool_tesla
cf6adbf1b506   bf756fb1ae65   "/hello"      5 hours ago     Exited (0) 5 hours ago               optimistic_darwin

删除容器

docker rm 容器id                  #删除指定的容器,不能删除正在运行的容器,强制删除使用 rm -f
docker rm -f $(docker ps -aq)    #删除所有的容器
docker ps -a -q|xargs docker rm  #删除所有的容器

启动和停止容器

docker start 容器id          #启动容器
docker restart 容器id        #重启容器
docker stop 容器id           #停止当前运行的容器
docker kill 容器id           #强制停止当前容器

4、其他命令

后台启动容器

[root@VM-20-6-centos ~]# docker run -d mysql
2b9ade9604f03f9d556cd0f0be6e256455cae7fc4c180c771436d5579958e6c8
[root@VM-20-6-centos ~]# docker ps 
CONTAINER ID   IMAGE     COMMAND   CREATED   STATUS    PORTS     NAMES
[root@VM-20-6-centos ~]# 
#问题:后台启动了,但是查看正在运行的容器发现没有
#原因:容器使用后台运行,就必须要一个前台进程,docker发现没有,就会停止;即容器启动后,发现自己没有提供服务,就停止了

查看日志

[root@iZwz99sm8v95sckz8bd2c4Z ~]# docker logs --help

Options:
      --details        Show extra details provided to logs
  -f, --follow         Follow log output
      --since string   Show logs since timestamp (e.g. 2013-01-02T13:23:37Z) or relative (e.g. 42m for 42 minutes)
  -n, --tail string    Number of lines to show from the end of the logs (default "all")
  -t, --timestamps     Show timestamps
      --until string   Show logs before a timestamp (e.g. 2013-01-02T13:23:37Z) or relative (e.g. 42m for 42 minutes)

常用:
docker logs -tf 容器id
docker logs --tail number 容器id #num为要显示的日志条数


#docker容器后台运行,必须要有一个前台的进程,否则会自动停止
#编写shell脚本循环执行,使得centos容器保持运行状态
[root@iZwz99sm8v95sckz8bd2c4Z ~]# docker run -d centos /bin/sh -c "while true;do echo hi;sleep 5;done"
c703b5b1911ff84d584390263a35707b6024816e1f46542b61918a6327a570dc
[root@iZwz99sm8v95sckz8bd2c4Z ~]# docker ps
CONTAINER ID   IMAGE     COMMAND                  CREATED          STATUS          PORTS     NAMES
c703b5b1911f   centos    "/bin/sh -c 'while t…"   13 seconds ago   Up 10 seconds             pedantic_banach
[root@iZwz99sm8v95sckz8bd2c4Z ~]# docker logs -tf --tail 10 c703b5b1911f
2020-12-27T03:34:07.255599560Z hi
2020-12-27T03:34:12.257641517Z hi
2020-12-27T03:34:17.259706294Z hi
2020-12-27T03:34:22.261693707Z hi
2020-12-27T03:34:27.262609289Z hi
2020-12-27T03:34:32.267862677Z hi
2020-12-27T03:34:37.270382873Z hi
2020-12-27T03:34:42.272414182Z hi
2020-12-27T03:34:47.274823243Z hi
2020-12-27T03:34:52.277419274Z hi

查看容器中的进程信息

[root@iZwz99sm8v95sckz8bd2c4Z ~]# docker top c703b5b1911f
UID                 PID                 PPID                C                   STIME               TTY                 TIME                CMD
root                11156               11135               0                   11:31               ?                   00:00:00            /bin/sh -c while true;do echo hi;sleep 5;done
root                11886               11156               0                   11:43               ?                   00:00:00            /usr/bin/coreutils --coreutils-prog-shebang=sleep /usr/bin/sleep 5

查看容器元数据

[root@iZwz99sm8v95sckz8bd2c4Z ~]# docker inspect 容器id

进入正在运行的容器

  • 因为通常我们的容器都是使用后台方式来运行的,有时需要进入容器修改配置
  • docker exec 进入容器后开启一个新的终端,可以在里面操作
  • docker attach 进入容器正在执行的终端,不会启动新的进程
#方式一
[root@iZwz99sm8v95sckz8bd2c4Z ~]# docker exec -it c703b5b1911f /bin/bash
[root@c703b5b1911f /]# ls
bin  dev  etc  home  lib  lib64  lost+found  media  mnt  opt  proc  root  run  sbin  srv  sys  tmp  usr  var
[root@c703b5b1911f /]# ps -ef      
UID        PID  PPID  C STIME TTY          TIME CMD
root         1     0  0 03:31 ?        00:00:00 /bin/sh -c while true;do echo hi;sleep 5;done
root       279     0  0 03:54 pts/0    00:00:00 /bin/bash
root       315     1  0 03:56 ?        00:00:00 /usr/bin/coreutils --coreutils-prog-shebang=sleep /usr/bin/sleep 5
root       316   279  0 03:56 pts/0    00:00:00 ps -ef

#方式二
[root@iZwz99sm8v95sckz8bd2c4Z ~]# docker attach c703b5b1911f

拷贝文件

#拷贝容器的文件到主机中
docker cp 容器id:容器内路径  目的主机路径

#拷贝宿主机的文件到容器中
docker cp 目的主机路径 容器id:容器内路径

5、命令合集

attach    Attach to a running container  #当前shell下attach连接指定运行镜像
build     Build an image from a Dockerfile  #通过Dockerfile定制镜像
commit    Create a new image from a container's changes  #提交当前容器为新的镜像
cp    Copy files/folders from a container to a HOSTDIR or to STDOUT  #从容器中拷贝指定文件或者目录到宿主机中
create    Create a new container  #创建一个新的容器,同run 但不启动容器
diff    Inspect changes on a container's filesystem  #查看docker容器变化
events    Get real time events from the server#从docker服务获取容器实时事件
exec    Run a command in a running container#在已存在的容器上运行命令
export    Export a container's filesystem as a tar archive  #导出容器的内容流作为一个tar归档文件(对应import)
history    Show the history of an image  #展示一个镜像形成历史
rename    Rename a container  #重命名容器
restart    Restart a running container  #重启运行的容器
rm    Remove one or more containers  #移除一个或者多个容器
rmi    Remove one or more images  #移除一个或多个镜像(无容器使用该镜像才可以删除,否则需要删除相关容器才可以继续或者-f强制删除)
run    Run a command in a new container  #创建一个新的容器并运行一个命令
save    Save an image(s) to a tar archive#保存一个镜像为一个tar包(对应load)
search    Search the Docker Hub for images  #在docker
hub中搜索镜像
start    Start one or more stopped containers#启动容器
stats    Display a live stream of container(s) resource usage statistics  #统计容器使用资源
stop    Stop a running container  #停止容器
tag         Tag an image into a repository  #给源中镜像打标签
top       Display the running processes of a container #查看容器中运行的进程信息
unpause    Unpause all processes within a container  #取消暂停容器
version    Show the Docker version information#查看容器版本号
wait         Block until a container stops, then print its exit code  #截取容器停止时的退出状态值

八、利用容器部署Nginx

拉取镜像

docker pull nginx

创建容器

#将容器中的端口80映射到宿主机中的端口8080 
docker run -d --name nginx01 -p 8080:80 nginx

运行测试

#或者直接访问:http://外网ip:8080
curl localhost:8080

九、图形化管理工具Portaniner安装

Portaniner是Docker的图形化管理工具,类似的工具还有Rancher(CI/CD再用)

  • 安装运行:
docker run -d -p 8088:9000 --restart=always -v /var/run/docker.sock:/var/run/docker.sock --privileged=true portainer/portainer
  • 设置账号密码
  • 选择local

十、Docker镜像

1、什么是镜像

镜像是一种轻量级、可执行的独立软件包,用来打包软件运行环境和基于运行环境开发的软件,它包含运行某个软件所需要的所有内容,包括代码,运行时(一个程序在运行或者在被执行的依赖)、库,环境变量和配置文件。

2、镜像加载原理

Docker的镜像实际上由一层一层的文件系统组成,这种层级的文件系统是UnionFS联合文件系统。

UnionFS联合文件系统

UnionFS (联合文件系统) : Union文件系统( UnionFS)是一种分层、轻量级并且高性能的文件系统,它支持对文件系统的修改作为一次提交来一层层的叠加,同时可以将不同目录挂载到同一个虚拟文件系统下(unite several directories into a single virtual filesystem)。Union文件系统是Docker镜像的基础。镜像可以通过分层来进行继承,基于基础镜像(没有父镜像),可以制作各种具体的应用镜像。

特性: 一次同时加载多个文件系统,但从外面看起来,只能看到一个文件系统,联合加载会把各层文件系统叠加起来,这样最终的文件系统会包含所有底层的文件和目录。

例如:有镜像1和镜像2,镜像1下载了centos内核,镜像2也需要centos内核,那此时镜像2就不用再下载centos内核,因为可以与镜像1的centos内核共用。

Docker镜像加载原理

docker的镜像实际上是由一层一层的文件系统组成,即UnionFS。

bootfs(boot file system)主要包含bootloader和kernel,bootloader主要是引导加载kernel,Linux刚启动时会加载bootfs文件系统,在Docker镜像的最底层是bootfs。这一层与我们典型的Linux/Unix系统是一样的,包含boot加载器和内核。当boot加载完成之后整个内核就都在内存中了,此时内存的使用权已由bootfs转交给内核,此时系统也会卸载bootfs。

rootfs (root file system),在bootfs之上。包含的就是典型Linux系统中的/dev,/proc,/bin,/etc等标准目录和文件。rootfs就是各种不同的操作系统发行版,比如Ubuntu,Centos等等。

平常的centos系统至少都几个g,为什么docker中的镜像才200多兆?

对于一个精简的OS,rootfs可以很小,只需要包含最基本的命令、工具和程序库就可以了。因为底层直接用宿主机的kernel,自己只需要提供rootfs就可以了。由此可见对于不同的linux发行版,bootfs基本是一致的,rootfs会有差别,因此不同的发行版可以共用bootfs。

3、分层理解

我们可以去下载一个镜像,查看日志输入,发现是分层下载的。

思考:为什么Docker镜像要采用这种结构呢?

最大的好处,我觉得莫过于是资源共享了!比如有多个镜像都从相同的Base镜像构建而来,那么宿主机只需在磁盘上保留一份base镜像,同时内存中也只需要加载一份base镜像,这样就可以为所有的容器服务了,而且镜像的每一层都可以被共享。

查看镜像分层的方式可以通过docker image inspect命令:

[root@VM-20-6-centos ~]# docker image inspect nginx:latest
[
    {
        "Id": "sha256:f2f70adc5d89aa922836e9cc6801980a12a7ff9012446cc6edf52ef8798a67bd",
        "RepoTags": [
            "nginx:latest"
        ],
        "RepoDigests": [
            "nginx@sha256:4ed64c2e0857ad21c38b98345ebb5edb01791a0a10b0e9e3d9ddde185cdbd31a"
        ],
        "Parent": "",
        "Comment": "",
        "Created": "2022-03-17T18:37:34.337367188Z",
        "Container": "9c0bfa83eb335b15e72e05cfa7222a68aee957bd04506b752307fdeeae7a6822",
        "ContainerConfig": {
            "Hostname": "9c0bfa83eb33",
            "Domainname": "",
            "User": "",
            "AttachStdin": false,
            "AttachStdout": false,
            "AttachStderr": false,
            "ExposedPorts": {
                "80/tcp": {}
            },
            "Tty": false,
            "OpenStdin": false,
            "StdinOnce": false,
            "Env": [
                "PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin",
                "NGINX_VERSION=1.21.6",
                "NJS_VERSION=0.7.2",
                "PKG_RELEASE=1~bullseye"
            ],
            "Cmd": [
                "/bin/sh",
                "-c",
                "#(nop) ",
                "CMD [\"nginx\" \"-g\" \"daemon off;\"]"
            ],
            "Image": "sha256:9c088c153d0263afd5e854438887f0eb7ea145b0578e3b850013c6a3078c27e4",
            "Volumes": null,
            "WorkingDir": "",
            "Entrypoint": [
                "/docker-entrypoint.sh"
            ],
            "OnBuild": null,
            "Labels": {
                "maintainer": "NGINX Docker Maintainers <docker-maint@nginx.com>"
            },
            "StopSignal": "SIGQUIT"
        },
        "DockerVersion": "20.10.12",
        "Author": "",
        "Config": {
            "Hostname": "",
            "Domainname": "",
            "User": "",
            "AttachStdin": false,
            "AttachStdout": false,
            "AttachStderr": false,
            "ExposedPorts": {
                "80/tcp": {}
            },
            "Tty": false,
            "OpenStdin": false,
            "StdinOnce": false,
            "Env": [
                "PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin",
                "NGINX_VERSION=1.21.6",
                "NJS_VERSION=0.7.2",
                "PKG_RELEASE=1~bullseye"
            ],
            "Cmd": [
                "nginx",
                "-g",
                "daemon off;"
            ],
            "Image": "sha256:9c088c153d0263afd5e854438887f0eb7ea145b0578e3b850013c6a3078c27e4",
            "Volumes": null,
            "WorkingDir": "",
            "Entrypoint": [
                "/docker-entrypoint.sh"
            ],
            "OnBuild": null,
            "Labels": {
                "maintainer": "NGINX Docker Maintainers <docker-maint@nginx.com>"
            },
            "StopSignal": "SIGQUIT"
        },
        "Architecture": "amd64",
        "Os": "linux",
        "Size": 141523072,
        "VirtualSize": 141523072,
        "GraphDriver": {
            "Data": {
                "LowerDir": "/var/lib/docker/overlay2/11042f23b32cacbae4c32f35a4dcb1e42902ed0e41707ad7fd1e32aca079de3d/diff:/var/lib/docker/overlay2/d494434e4bbd0bef1c2b8f6683760faa77d7668839125ee218e7ed8ff521062b/diff:/var/lib/docker/overlay2/9a2355a0ad67923ec7a8b543351bf278879a3b0902c2f4d94f1ae6429277b6b7/diff:/var/lib/docker/overlay2/a837b231718a84f25469112d850a494f0b09bac764135e5990ca0ef5b576088f/diff:/var/lib/docker/overlay2/40e739161ac41a0160c2a48010da59fc71b7f8ffd87f750beda5371b6e41d946/diff",
                "MergedDir": "/var/lib/docker/overlay2/e19485706300126dfff36beb3f0313564f0f4f1e478d27c8ead6f24f08840212/merged",
                "UpperDir": "/var/lib/docker/overlay2/e19485706300126dfff36beb3f0313564f0f4f1e478d27c8ead6f24f08840212/diff",
                "WorkDir": "/var/lib/docker/overlay2/e19485706300126dfff36beb3f0313564f0f4f1e478d27c8ead6f24f08840212/work"
            },
            "Name": "overlay2"
        },
        "RootFS": {
            "Type": "layers",
            "Layers": [
                "sha256:3a626bb08c24b5cc968d312bf5694aa87b6d9961c5f182c6bc138d8ca8ac13ee",
                "sha256:30c00b5281a1b19edcf44ab863eaf3ba4918b43c201fa778c0784c69882c8de5",
                "sha256:8b8ecda1d12d64d07bcf75282912d952666c4ca21b1d26874ba9cf1605b24f18",
                "sha256:2793e885dc34c929ce7875b84fee7940584689617c73347aef78d803b6cdd0b3",
                "sha256:d00147ef6763226b12c4de69c36000eb57718799411d6af91ab5761ddf77d761",
                "sha256:24037b645d66baa74b756af08594d33a72f5d5427e9032460d229bf120a8ff3e"
            ]
        },
        "Metadata": {
            "LastTagTime": "0001-01-01T00:00:00Z"
        }
    }
]

这里指示了分层信息:

"RootFS": {
            "Type": "layers",
            "Layers": [
                "sha256:3a626bb08c24b5cc968d312bf5694aa87b6d9961c5f182c6bc138d8ca8ac13ee",
                "sha256:30c00b5281a1b19edcf44ab863eaf3ba4918b43c201fa778c0784c69882c8de5",
                "sha256:8b8ecda1d12d64d07bcf75282912d952666c4ca21b1d26874ba9cf1605b24f18",
                "sha256:2793e885dc34c929ce7875b84fee7940584689617c73347aef78d803b6cdd0b3",
                "sha256:d00147ef6763226b12c4de69c36000eb57718799411d6af91ab5761ddf77d761",
                "sha256:24037b645d66baa74b756af08594d33a72f5d5427e9032460d229bf120a8ff3e"
            ]
        },

理解:

所有的Docker镜像都起始于一个基础镜像层,当进行修改或增加新的内容时,就会在当前镜像层之上创建新的镜像层。

举一个简单的例子,假如基于Ubuntu Linux 16.04创建一个新的镜像,这就是新镜像的第一层;如果在该镜像中添加Python包,就会在基础镜像层之上创建第二个镜像层;如果继续添加一个安全补丁,就会创建第三个镜像层。该镜像当前已经包含3个镜像层,如下图所示(这只是一个于演示的很简单的例子 )。

docker分层理解1

在添加额外的镜像层的同时,镜像始终保持是当前所有镜像的组合,理解这一点非常重要。下图中举了一个简单的例子:第1层包含3个文件,第2层也包含3个文件,且他们之间没有冲突,组合之后镜像包含了来自两个镜像层的6个文件。

docker分层理解2

上图中的镜像层跟之前图中的略有区别,主要目的是便于展示文件。
下图中展示了一个稍微复杂的三层镜像,在外部看来整个镜像只有6个文件,这是因为最上层中的文件7是文件5的一个更新版本。

docker分层理解3

这种情况下,上层镜像层中的文件覆盖了底层镜像层中的文件。这样就使得文件的更新版本作为一个新镜像层添加到镜像当中。

Docker通过存储引擎(新版本采用快照机制)的方式来实现镜像层堆栈,并保证多镜像层对外展示为统一的文件系统。

Linux上可用的存储引擎有AUFS、Overlay2、Device Mapper、Btrfs 以及ZFS。顾名思义,每种存储引擎都基于Linux中对应的文件系统或者块设备技术,并且每种存储引擎都有其独有的性能特点。

Docker在Windows上仅支持windowsfilter一种存储引擎,该引擎基于NTFS文件系统之上实现了分层和CoW[1]。

下图展示了与系统显示相同的三层镜像。所有镜像层堆叠并合并,对外提供统一的视图。

特点

Docker镜像都是只读的,当容器启动时,一个新的可写层被加载到镜像的顶部

这一层就是我们通常说的容器层,容器之下的都叫镜像层,而我们每对容器进行的修改都会新加一层

4、commit镜像

#使用docker commit 命令提交容器成为一个新的版本

docker commit -m=“提交的描述信息”  -a="作者" 容器id 目标镜像名:[TAG]

由于默认的Tomcat镜像的webapps文件夹中没有任何内容,需要从webapps.dist中拷贝文件到webapps文件夹。下面自行制作镜像:就是从webapps.dist中拷贝文件到webapps文件夹下,并提交该镜像作为一个新的镜像。使得该镜像默认的webapps文件夹下就有文件。具体命令如下:

#启动Tomcat
docker run -it tomcat /bin/bash
#复制
cp -r webapps.dist/* webapps
#新建终端,查看正在运行的容器
docker ps
#提交容器成为镜像
docker commit -m="add webapps" -a="srx" 2a3bf3eaa2e4 mytomcat:1.0
#查看镜像
docker images
#运行mytomcat:1.0
docker run -it mytomcat:1.0 /bin/bash #进入可以看到webapps文件夹中有我们之前复制的文件

十一、容器数据卷

1、数据卷简介

Docker将运用与运行的环境打包形成容器运行, Docker容器产生的数据,如果不通过docker commit生成新的镜像,使得数据做为镜像的一部分保存下来, 那么当容器删除后,数据自然也就没有了。 为了能保存数据在Docker中我们使用卷。

卷就是目录或文件,存在于一个或多个容器中,由Docker挂载到容器,但卷不属于联合文件系统(Union FileSystem),因此能够绕过联合文件系统提供一些用于持续存储或共享数据的特性。

卷的设计目的就是数据的持久化,完全独立于容器的生存周期,因此Docker不会在容器删除时删除其挂载的数据卷。

数据卷的特点:

  • 数据卷可在容器之间共享或重用数据
  • 卷中的更改可以直接生效
  • 数据卷中的更改不会包含在镜像的更新中
  • 数据卷的生命周期一直持续到没有容器使用它为止

2、数据卷的简单使用

方式一:直接通过命令 -v

docker run -it -v 主机目录:容器目录

测试:

#终端一启动centos
docker run -it -v /home/test:/home centos /bin/bash
#终端二可看到创建的test文件夹
cd /home
ls
#终端一创建文件
touch test.sh
#终端二中可查看到文件test.sh
cd /home/test
ls
#终端二中创建文件
touch demo.sh
#终端一中可查看到文件
cd /home
ls

查看容器对应元数据docker inspect 容器id,可以在Mounts节点查看建立的数据卷信息:

"Mounts": [
            {
                "Type": "bind",
                "Source": "/home/test", #主机内的地址
                "Destination": "/home", #容器内的地址
                "Mode": "",
                "RW": true,
                "Propagation": "rprivate"
            }
        ],

即使容器停止运行或者容器删除,仍然可以实现数据同步,本地的数据卷不会丢失,因为他们是共享同一个地址的,所以不存在数据不同步和丢失的情况

3、MySQL建立数据卷

docker run -d -p 6603:3306 -v /home/mysql/conf:/etc/mysql/conf.d -v /home/mysql/data:/var/lib/mysql -e MYSQL_ROOT_PASSWORD=123456 --name mysql01 mysql:5.7

如果使用配置的密码连接mysql服务失败,原因很大可能是本机挂载的配置文件中已有文件,将容器中的配置给覆盖了,我们将相应的本机文件中的文件配置删除即可。

4、常用命令

  • 创建数据卷

    docker volume create 数据卷名称
    
  • 查看所有数据卷

    docker volume ls
    
  • 查看指定数据卷的信息

    docker volume inspect 数据卷名称
    
  • 删除数据卷

    docker volume rm 数据卷名称
    
  • 删除容器之时删除相关的卷

    docker rm -v ...
    
  • 删除无主的数据卷

    docker volume prune
    

    数据卷是被设计用来持久化数据的,它的生命周期独立于容器,Docker 不会在容器被删除后自动删除数据卷,并且也不存在垃圾回收这样的机制来处理没有任何容器引用的数据卷 。如果需要在删除容器的同时移除数据卷。可以在删除容器的时候使用 docker rm -v 这个命令。

5、具名挂载和匿名挂载

1.匿名挂载

匿名挂载就是在指定数据卷的时候,不指定容器路径对应的主机路径,这样对应映射的主机路径就是默认的路径/var/lib/docker/volumes/中自动生成一个随机命名的文件夹。

如下运行并匿名挂载Nginx容器:

docker run -d -P --name nginx01 -v /etc/nginx nginx

2.具名挂载

具名挂载,就是指定文件夹名称,区别于指定路径挂载,这里的指定文件夹名称是在Docker指定的默认数据卷路径下的。通过docker volume ls命令可以查看当前数据卷的目录情况。

如下运行并具名挂载Nginx容器:

docker run -d -P --name nginx02 -v juming-nginx:/etc/nginx nginx

查看指定的数据卷信息的命令:

docker volume inspect 数据卷名称

Docker所有的数据卷默认在/var/lib/docker/volumes/ 目录下

匿名挂载,具名挂载,指定路径挂载的命令区别如下:

  • 匿名挂载:-v 容器内路径
  • 具名挂载:-v 卷名:容器内路径
  • 指定路径挂载:-v /宿主机路径:容器内路径

指定数据卷映射的相关参数:

ro —— readonly 只读。设置了只读则只能操作宿主机的路径,不能操作容器中的对应路径。

rw ----- readwrite 可读可写

docker run -d -P --name nginx02 -v juming-nginx:/etc/nginx:ro nginx
docker run -d -P --name nginx02 -v juming-nginx:/etc/nginx:rw nginx

6、Dockerfile中设置数据卷

我们可以在Dockerfile中使用VOLUME指令来给镜像添加一个或多个数据卷。

下面使用Dockerfile构建一个新的镜像,dockerfile01文件的内容,匿名挂载了volume01和volume02两个目录:

FROM centos

VOLUME ["volume01","volume02"]

CMD echo "----end----"
CMD /bin/bash

执行并构建镜像:

[root@iZwz99sm8v95sckz8bd2c4Z docker-test-volume]# docker build -f /www/dockerfile -t mycentos:1.0 .
Sending build context to Docker daemon  2.048kB
Step 1/4 : FROM centos
 ---> 300e315adb2f
Step 2/4 : VOLUME ["volume01","volume02"]
 ---> Running in 215ef28fd5a6
Removing intermediate container 215ef28fd5a6
 ---> f506ddf133d2
Step 3/4 : CMD echo "----end----"
 ---> Running in 62a1c4b9dc7b
Removing intermediate container 62a1c4b9dc7b
 ---> bbea81a6e94e
Step 4/4 : CMD /bin/bash
 ---> Running in 245d239f3776
Removing intermediate container 245d239f3776
 ---> 1df90e6fd790
Successfully built 1df90e6fd790
Successfully tagged ethan/centos:1.0
[root@iZwz99sm8v95sckz8bd2c4Z docker-test-volume]# docker images
REPOSITORY            TAG       IMAGE ID       CREATED          SIZE
mycentos              1.0       1df90e6fd790   13 minutes ago   209MB
mytomcat              1.0       f189aac861de   25 hours ago     653MB
mysql                 5.7       f07dfa83b528   7 days ago       448MB
tomcat                latest    feba8d001e3f   11 days ago      649MB
nginx                 latest    ae2feff98a0c   13 days ago      133MB
centos                latest    300e315adb2f   3 weeks ago      209MB
portainer/portainer   latest    62771b0b9b09   5 months ago     79.1MB
elasticsearch         7.6.2     f29a1ee41030   9 months ago     791MB

完成镜像的生成后,启动自己生成的容器:

[root@73422f706266 /]# docker run -it 89aed38ec414 /bin/bash
[root@73422f706266 /]# ls
bin  etc   lib    lost+found  mnt  proc  run   srv  tmp  var       volume02
dev  home  lib64  media       opt  root  sbin  sys  usr  volume01 #可看到挂载的volume01、volume02

可以看到自动挂载的数据卷目录。下面查看对应宿主机的数据卷目录:

docker inspect 73422f706266

 "Mounts": [
            {
                "Type": "volume",
                "Name": "8280ed081572b9b3a0cc53871c9da128722aa47bdf02d6751306f15d3030928a",
                "Source": "/var/lib/docker/volumes/8280ed081572b9b3a0cc53871c9da128722aa47bdf02d6751306f15d3030928a/_data",
                "Destination": "volume01",
                "Driver": "local",
                "Mode": "",
                "RW": true,
                "Propagation": ""
            },
            {
                "Type": "volume",
                "Name": "80530e4b8ea2479e19febb6b17a548b8d8672a6e26c1052e772368b048d4ad43",
                "Source": "/var/lib/docker/volumes/80530e4b8ea2479e19febb6b17a548b8d8672a6e26c1052e772368b048d4ad43/_data",
                "Destination": "volume02",
                "Driver": "local",
                "Mode": "",
                "RW": true,
                "Propagation": ""
            }
        ],

可以看到Mounts下有宿主机的挂载目录。因为dockerfile中没有指定宿主机目录,所以属于匿名挂载,在/var/lib/docker/volumes/目录下生成了随机命名的路径。

7、容器数据卷

容器数据卷是指建立数据卷,来同步多个容器间的数据,实现容器间的数据同步

首先启动容器1,volume01、volume02为挂载目录。

[root@VM-20-6-centos www]# docker run -it --name docker01 mycentos:1.0
[root@cb2f66d0d356 /]# ls
bin  etc   lib    lost+found  mnt  proc  run   srv  tmp  var       volume02
dev  home  lib64  media       opt  root  sbin  sys  usr  volume01

然后启动容器2,通过参数--volumes-from,设置容器2和容器1建立数据卷挂载关系。

[root@VM-20-6-centos www]# docker run -it --name docker02 --volumes-from docker01 mycentos:1.0
[root@0a071281109e /]# ls
bin  etc   lib    lost+found  mnt  proc  run   srv  tmp  var       volume02
dev  home  lib64  media       opt  root  sbin  sys  usr  volume01

首先在容器2中的volume01中添加文件:

[root@0a071281109e /]# cd /volume01
[root@0a071281109e volume01]# ls
[root@0a071281109e volume01]# touch docker
[root@0a071281109e volume01]# ls
docker

然后就可以看到容器1的文件也会添加上了

下面同步两个MySQL的数据库和配置文件,与上面的操作相同,首先建立数据卷,然后给另一个MySQL容器建立容器数据卷挂载,示例:

[root@iZwz99sm8v95sckz8bd2c4Z home]# docker run -d -p 6603:3306 -v /home/mysql/conf:/etc/mysql/conf.d -v /home/mysql/data:/var/lib/mysql -e MYSQL_ROOT_PASSWORD=123456 --name mysql01 mysql:5.7
[root@iZwz99sm8v95sckz8bd2c4Z home]# docker run -d -p 6604:3306 -e MYSQL_ROOT_PASSWORD=123456 --name mysql02 --volumes-from mysql01 mysql:5.7

十二、Dockerfile

1、Dockerfile介绍

Dockerfile是用来构建Docker镜像的文本文件,也可以说是命令参数脚本。docker build命令用于从Dockerfile构建镜像。可以在docker build命令中使用-f标志指向文件系统中任何位置的Dockerfile。

Docker镜像发布的步骤:

  1. 编写一个dockerfile文件

  2. docker build 构建成为一个镜像

  3. docker run 镜像

  4. docker push 镜像(发布镜像到DockerHub、阿里云镜像仓库)

2、Dockerfile指令说明

指令说明
FROM指定基础镜像
MAINTAINER镜像是谁写的,姓名+邮箱
RUN镜像构建的时候需要运行的命令
ADD将本地文件添加到容器中,tar类型文件会自动解压(网络压缩资源不会被解压),可以访问网络资源,类似wget
WORKDIR镜像的工作目录
VOLUME挂载的目录
EXPOSE保留端口配置
CMD指定这个容器启动的时候要运行的命令(会被追加的命令替换,只有最后一个会生效)
EMTRYPOINT指定这个容器启动的时候要运行的命令,可以追加命令
ONBUILDONBUILD 是一个特殊的指令,它后面跟的是其它指令,比如 RUN, COPY 等,而这些指令,在当前镜像构建时并不会被执行。只有当以当前镜像为基础镜像,去构建下一级镜像的时候才会被执行
COPY功能类似ADD,但是不会自动解压文件,也不能访问网络资源
ENV构建的时候设置环境变量

Dockerfile 一般分为四部分:基础镜像信息、维护者信息、镜像操作指令和容器启动时执行指令,’#’ 为 Dockerfile 中的注释。

关于DockerFile文件的脚本注意点有:

  1. 每个保留关键字(指令)约定为大写字母

  2. 文件中的指令从上到下顺序执行,第一个指令必须是FROM

  3. # 号表示注释

  4. 每一个指令都会创建一个新的镜像层,并提交

3、制作CentOS镜像

下面通过编写Dockerfile文件来制作Centos镜像,并在官方镜像的基础上添加vim和net-tools工具。首先在/home/dockerfile目录下新建文件mydockerfile-centos。然后使用上述指令编写该文件。

FROM centos:7

ENV MYPATH /usr/local

WORKDIR $MYPATH

RUN yum -y install vim
RUN yum -y install net-tools

EXPOSE 80

CMD echo $MYPATH
CMD echo "---end---"
CMD /bin/bash

逐行解释该Dockerfile文件的指令:

  • FROM centos:7:该image文件继承官方的centos7,后面加冒号如centos:7,用于指定镜像的版本
  • ENV MYPATH /usr/local:设置环境变量MYPATH ,后面有用到,为一个键值对
  • WORKDIR $MYPATH:直接使用上面设置的环境变量,指定/usr/local为工作目录
  • RUN yum -y install vimRUN yum -y install net-tools:在/usr/local目录下,运行yum -y install vim和yum -y install net-tools命令安装工具,注意安装后的所有依赖和工具都会打包到image文件中
  • EXPOSE 80:将容器80端口暴露出来,允许外部连接这个端口
  • CMD:指定容器启动的时候运行命令

通过这个dockerfile构建镜像,构建镜像命令:

docker build -f /home/dockerfile/dockerfile -t mycentos:1.0 . #注意后面有个.

上面命令中,-t参数用来指定 image 文件的名字,后面还可以用冒号指定标签。如果不指定,默认的标签就是latest。最后的那个点表示这是在指定context路径,有人翻译为上下文路径(context)

build命令可以省略-f,但是规定dockerfile文件必须命名为Dockerfile,且执行该命令的路径必须在Dockerfile文件所在目录

下面执行build命令生成image文件,如果执行成功,可以通过docker images来查看新生成的镜像文件。

[root@VM-20-6-centos dockerfile]# docker build -f /home/dockerfile/dockerfile -t mycentos:1.0 .
Sending build context to Docker daemon  2.048kB
Step 1/9 : FROM centos:7
7: Pulling from library/centos
2d473b07cdd5: Pull complete 
Digest: sha256:c73f515d06b0fa07bb18d8202035e739a494ce760aa73129f60f4bf2bd22b407
Status: Downloaded newer image for centos:7
 ---> eeb6ee3f44bd
Step 2/9 : ENV MYPATH /usr/local
 ---> Running in cfa4ef3e8e2c
Removing intermediate container cfa4ef3e8e2c
 ---> c44817f29f28
Step 3/9 : WORKDIR $MYPATH
 ---> Running in 4c9e6d2d560d
Removing intermediate container 4c9e6d2d560d
 ---> fb5cff96b3d0
Step 4/9 : RUN yum -y install vim
 ---> Running in d72f9228f9ad
Removing intermediate container d72f9228f9ad
 ---> 700553ee3fb7
Step 5/9 : RUN yum -y install net-tools
 ---> Running in 221f2ef33ae4
Removing intermediate container 221f2ef33ae4
 ---> a51d78c1f517
Step 6/9 : EXPOSE 80
 ---> Running in b269f3d1dec8
Removing intermediate container b269f3d1dec8
 ---> a97cf4da3f78
Step 7/9 : CMD echo $MYPATH
 ---> Running in 9e30be0e229d
Removing intermediate container 9e30be0e229d
 ---> 8c07e3a50490
Step 8/9 : CMD echo "---end---"
 ---> Running in aa88c5a1db09
Removing intermediate container aa88c5a1db09
 ---> 29aac32b5519
Step 9/9 : CMD /bin/bash
 ---> Running in 0d30d7c67e1a
Removing intermediate container 0d30d7c67e1a
 ---> 232ef116fcaf
Successfully built 232ef116fcaf
Successfully tagged mycentos:1.0

下面生成容器,测试相关命令,查看默认工作目录是否设置成功,vim和net-tools工具是否下载成功。

[root@VM-20-6-centos dockerfile]# docker run -it mycentos:1.0
[root@ca8e324b7b58 local]# pwd
/usr/local #该目录为Dockerfile文件中的环境目录
[root@ca8e324b7b58 local]# ifconfig
eth0: flags=4163<UP,BROADCAST,RUNNING,MULTICAST>  mtu 1500 #ifconfig可以使用了
        inet 172.17.0.2  netmask 255.255.0.0  broadcast 172.17.255.255
        ether 02:42:ac:11:00:02  txqueuelen 0  (Ethernet)
        RX packets 8  bytes 656 (656.0 B)
        RX errors 0  dropped 0  overruns 0  frame 0
        TX packets 0  bytes 0 (0.0 B)
        TX errors 0  dropped 0 overruns 0  carrier 0  collisions 0

lo: flags=73<UP,LOOPBACK,RUNNING>  mtu 65536
        inet 127.0.0.1  netmask 255.0.0.0
        loop  txqueuelen 1000  (Local Loopback)
        RX packets 0  bytes 0 (0.0 B)
        RX errors 0  dropped 0  overruns 0  frame 0
        TX packets 0  bytes 0 (0.0 B)
        TX errors 0  dropped 0 overruns 0  carrier 0  collisions 0

[root@ca8e324b7b58 local]# vim test #vim命令也可以使用了

另外,我们通过docker history 容器id命令来查看镜像的构建步骤

4、CMD、RUN和EMTRYPOINT

RUN命令与CMD命令的区别在哪里?

简单说,RUN命令在 image 文件的构建阶段执行,执行结果都会打包进入 image 文件;CMD命令则是在容器启动后执行。另外,一个 Dockerfile 可以包含多个RUN命令,但是只能有一个CMD命令

注意,指定了CMD命令以后,docker container run命令就不能附加命令了(比如前面的/bin/bash),否则它会覆盖CMD命令。

CMD和ENTRYPOINT的区别在哪里?

  • CMD :指定容器启动的时候要运行的命令,只有最后一个会生效

  • ENTRYPOINT :指定容器启动的时候要运行的命令,命令可以追加

以下是CMD和ENTRYPOINT的区别

首先是使用CMD指令:

[root@iZwz99sm8v95sckz8bd2c4Z dockerfile]# cat dockerfile-cmd-test
FROM centos
CMD ["ls","-a"]
[root@iZwz99sm8v95sckz8bd2c4Z dockerfile]# docker build -f dockerfile-cmd-test -t cmdtest:1.0 .
Sending build context to Docker daemon  3.072kB
Step 1/2 : FROM centos
 ---> 300e315adb2f
Step 2/2 : CMD ["ls","-a"]
 ---> Running in 6d4d0112322f
Removing intermediate container 6d4d0112322f
 ---> b6ec5224d2ac
Successfully built b6ec5224d2ac
Successfully tagged cmdtest:1.0
#启动镜像,可正常执行CMD的命令
[root@iZwz99sm8v95sckz8bd2c4Z dockerfile]# docker run cmdtest:1.0
.
..
.dockerenv
bin
dev
etc
home
lib
lib64
lost+found
media
mnt
opt
proc
root
run
sbin
srv
sys
tmp
usr
var
#由于使用的是 CMD指令,命令无追加,-l取代了原本的ls -a,而-l命令不存在所以报错。
[root@iZwz99sm8v95sckz8bd2c4Z dockerfile]# docker run cmdtest:1.0 -l
docker: Error response from daemon: OCI runtime create failed: container_linux.go:370: starting container process caused: exec: "-l": executable file not found in $PATH: unknown.

下面使用ENTRYPOINT来构建一个镜像:

#1.修改dockerfile文件
[root@iZwz99sm8v95sckz8bd2c4Z dockerfile]# cat dockerfile-cmd-test
FROM centos
ENTRYPOINT ["ls","-a"]
#2.构建镜像
[root@iZwz99sm8v95sckz8bd2c4Z dockerfile]# docker build -f dockerfile-cmd-test -t cmdtest:2.0 .
Sending build context to Docker daemon  3.072kB
Step 1/2 : FROM centos
 ---> 300e315adb2f
Step 2/2 : ENTRYPOINT ["ls","-a"]
 ---> Running in 61389c0c1967
Removing intermediate container 61389c0c1967
 ---> ac7b7e83ff88
Successfully built ac7b7e83ff88
Successfully tagged cmdtest:2.0
#3.运行镜像
[root@iZwz99sm8v95sckz8bd2c4Z dockerfile]# docker run cmdtest:2.0
.
..
.dockerenv
bin
dev
etc
home
lib
lib64
lost+found
media
mnt
opt
proc
root
run
sbin
srv
sys
tmp
usr
var
#4.追加镜像再次运行,此时是可以运行的,不像CMD那样会报错
[root@iZwz99sm8v95sckz8bd2c4Z dockerfile]# docker run cmdtest:2.0 -l
total 56
drwxr-xr-x   1 root root 4096 Jan  1 03:55 .
drwxr-xr-x   1 root root 4096 Jan  1 03:55 ..
-rwxr-xr-x   1 root root    0 Jan  1 03:55 .dockerenv
lrwxrwxrwx   1 root root    7 Nov  3 15:22 bin -> usr/bin
drwxr-xr-x   5 root root  340 Jan  1 03:55 dev
drwxr-xr-x   1 root root 4096 Jan  1 03:55 etc
drwxr-xr-x   2 root root 4096 Nov  3 15:22 home
lrwxrwxrwx   1 root root    7 Nov  3 15:22 lib -> usr/lib
lrwxrwxrwx   1 root root    9 Nov  3 15:22 lib64 -> usr/lib64
drwx------   2 root root 4096 Dec  4 17:37 lost+found
drwxr-xr-x   2 root root 4096 Nov  3 15:22 media
drwxr-xr-x   2 root root 4096 Nov  3 15:22 mnt
drwxr-xr-x   2 root root 4096 Nov  3 15:22 opt
dr-xr-xr-x 106 root root    0 Jan  1 03:55 proc
dr-xr-x---   2 root root 4096 Dec  4 17:37 root
drwxr-xr-x  11 root root 4096 Dec  4 17:37 run
lrwxrwxrwx   1 root root    8 Nov  3 15:22 sbin -> usr/sbin
drwxr-xr-x   2 root root 4096 Nov  3 15:22 srv
dr-xr-xr-x  13 root root    0 Dec 29 15:41 sys
drwxrwxrwt   7 root root 4096 Dec  4 17:37 tmp
drwxr-xr-x  12 root root 4096 Dec  4 17:37 usr
drwxr-xr-x  20 root root 4096 Dec  4 17:37 var

5、制作Tomcat镜像

1.准备Tomcat

[root@VM-20-6-centos tomcat]# vim readme.md
[root@VM-20-6-centos tomcat]# ls
apache-tomcat-9.0.60.tar.gz  readme.md

2.编写Dockerfile

vim Dockerfile
FROM centos:7

COPY readme.md /usr/local/readme.md

#复制并解压
ADD apache-tomcat-9.0.60.tar.gz /usr/local/

#安装jdk
RUN yum install -y java-1.8.0-openjdk-devel.x86_64

#设置工作目录
ENV MYPATH /usr/local
WORKDIR $MYPATH

#配置Tomcat
ENV CATALINA_HOME /usr/local/apache-tomcat-9.0.60
ENV CATALINA_BASH /usr/local/apache-tomcat-9.0.60
ENV PATH $PATH:$CATALINA_HOME/lib:$CATALINA_HOME/bin

EXPOSE 8080

#创建日志输出
CMD /usr/local/apache-tomcat-9.0.60/bin/startup.sh && tail -F /usr/local/apache-tomcat-9.0.60/logs/catalina.out

3.构建镜像

#需要在Dockerfile所在目录下
docker build -t diytomcat:1.0 .

4.启动镜像

这里设置了数据卷,宿主机的/home/tomcat/test对应该容器的/usr/local/apache-tomcat-9.0.60/webapps/test。这样关于test项目的修复只需要在宿主机上修改就可以了,不需要进入到容器中修改。

docker run -d -p 9080:8080 --name diytomcat -v /home/tomcat/test:/usr/local/apache-tomcat-9.0.60/webapps/test diytomcat:1.0

5.测试挂载

此时访问http://ip:9080可以访问到Tomcat的默认页面

在宿主机/home/tomcat/test目录下,新建文件index.html,测试是否可以正常挂载

<!DOCTYPE html>
<html>
    <head>
         <meta charset="UTF-8"/>
        <title>这是个标题</title>
    </head>
    <body>
        <h1>这是一个一个简单的HTML</h1>
        <p>Hello World!</p>
    </body>
</html>

访问http://ip:9080/test,可访问到index.html

十三、Docker0网络

1、Docker的网络连通原理

输入ip addr,会输出以下信息:

1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN group default qlen 1000
    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
    inet 127.0.0.1/8 scope host lo
       valid_lft forever preferred_lft forever
    inet6 ::1/128 scope host 
       valid_lft forever preferred_lft forever
2: eth0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc mq state UP group default qlen 1000
    link/ether 52:54:00:75:17:13 brd ff:ff:ff:ff:ff:ff
    inet 10.0.20.6/22 brd 10.0.23.255 scope global eth0
       valid_lft forever preferred_lft forever
    inet6 fe80::5054:ff:fe75:1713/64 scope link 
       valid_lft forever preferred_lft forever
3: docker0: <NO-CARRIER,BROADCAST,MULTICAST,UP> mtu 1500 qdisc noqueue state DOWN group default 
    link/ether 02:42:30:83:ed:0d brd ff:ff:ff:ff:ff:ff
    inet 172.17.0.1/16 brd 172.17.255.255 scope global docker0
       valid_lft forever preferred_lft forever
    inet6 fe80::42:30ff:fe83:ed0d/64 scope link 
       valid_lft forever preferred_lft forever

其中lo为本机环回地址,eth0为内网地址,docker0为docker的地址

主机可以ping通容器,那容器间是否可以相互ping通呢?

先运行一个容器:docker run -d -P --name tomcat01 tomcat

查看容器内部地址: docker exec -it tomcat01 ip addr

1: 1o: <LOOPBACK,UP ,LOWER_ UP> mtu 65536 qdisc noqueue state UNKNOWN group default qlen 1000
link/loopback 00:00:00:00:00:00 brd 00:00:00 :00:00:00
inet 127.0.0.1/8 scope host 1o
valid_ .1ft forever preferred. _1ft forever
60: eth0@61: <BROADCAST, MULTICAST ,UP ,LOWER_ _UP> mtu 1500 qdisc noqueue state UP group default
link/ether 02 :42:ac:12:00:02 brd ff:ff:ff:ff:ff:ff link -netnsid 0
inet 172.18.0.2/16 brd 172.18. 255.255 scope global eth0
valid_ 1ft forever preferred_ 1ft forever

看到有个261: eth0@if262:

本机中再:ip addr

1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN group default qlen 1000
    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
    inet 127.0.0.1/8 scope host lo
       valid_lft forever preferred_lft forever
    inet6 ::1/128 scope host 
       valid_lft forever preferred_lft forever
2: eth0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc mq state UP group default qlen 1000
    link/ether 52:54:00:75:17:13 brd ff:ff:ff:ff:ff:ff
    inet 10.0.20.6/22 brd 10.0.23.255 scope global eth0
       valid_lft forever preferred_lft forever
    inet6 fe80::5054:ff:fe75:1713/64 scope link 
       valid_lft forever preferred_lft forever
3: docker0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP group default 
    link/ether 02:42:30:83:ed:0d brd ff:ff:ff:ff:ff:ff
    inet 172.17.0.1/16 brd 172.17.255.255 scope global docker0
       valid_lft forever preferred_lft forever
    inet6 fe80::42:30ff:fe83:ed0d/64 scope link 
       valid_lft forever preferred_lft forever
61: veth635863b@if60: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue master docker0 state UP group default 
    link/ether 1e:dc:84:c3:c8:e1 brd ff:ff:ff:ff:ff:ff link-netnsid 0
    inet6 fe80::1cdc:84ff:fec3:c8e1/64 scope link 
       valid_lft forever preferred_lft forever

可以看到有个61: veth635863b@if60

这个就是docker为容器创建的一个地址,通过这个地址,主机可以ping通容器,而容器间也可以相互ping通

2、Docker默认的网络模式

使用以下命令查看所有的Docker网络:

[root@VM-20-6-centos bbs]# docker network ls
NETWORK ID     NAME      DRIVER    SCOPE
09effc5a67d7   bridge    bridge    local
74feed8e6018   host      host      local
5a520470e49e   none      null      local

Docker默认提供了四个网络模式:

  • bridge:容器默认的网络是桥接模式(自己搭建的网络默认也是使用桥接模式,启动容器默认也是使用桥接模式)。此模式会为每一个容器分配、设置IP等,并将容器连接到一个docker0虚拟网桥,通过docker0网桥以及Iptables nat表配置与宿主机通信。

  • none:不配置网络,容器有独立的Network namespace,但并没有对其进行任何网络设置,如分配veth pair 和网桥连接,配置IP等。

  • host:容器和宿主机共享Network namespace。容器将不会虚拟出自己的网卡,配置自己的IP等,而是使用宿主机的IP和端口。

  • container:创建的容器不会创建自己的网卡,配置自己的IP容器网络连通。容器和另外一个容器共享Network namespace(共享IP、端口范围)。

容器默认使用bridge网络模式,我们使用该docker run --network=选项指定容器使用的网络:

  • host模式:使用 --net=host 指定。
  • none模式:使用 --net=none 指定。
  • bridge模式:使用 --net=bridge 指定,默认设置。
  • container模式:使用 --net=container:NAME_or_ID 指定。
1.host模式

Namespace的简要说明:

Docker使用了Linux的Namespaces技术来进行资源隔离,如PID Namespace隔离进程,Mount Namespace隔离文件系统,Network Namespace隔离网络等。

一个Network Namespace提供了一份独立的网络环境,包括网卡、路由、Iptable规则等都与其他的NetworkNamespace隔离。一个Docker容器一般会分配一个独立的Network Namespace。

如果启动容器的时候使用host模式,那么这个容器将不会获得一个独立的Network Namespace,而是和宿主机共用一个Network Namespace。容器将不会虚拟出自己的网卡,配置自己的IP等,而是使用宿主机的IP和端口。但是,容器的其他方面,如文件系统、进程列表等还是和宿主机隔离的。

使用host模式的容器可以直接使用宿主机的IP地址与外界通信,容器内部的服务端口也可以使用宿主机的端口,不需要进行NAT,host最大的优势就是网络性能比较好,但是docker host上已经使用的端口就不能再用了,网络的隔离性不好。

2.container模式

这个模式指定新创建的容器和已经存在的一个容器共享一个 Network Namespace,而不是和宿主机共享。新创建的容器不会创建自己的网卡,配置自己的 IP,而是和一个指定的容器共享 IP、端口范围等。同样,两个容器除了网络方面,其他的如文件系统、进程列表等还是隔离的。两个容器的进程可以通过 lo 网卡设备通信。

3.none模式

使用none模式,Docker容器拥有自己的Network Namespace,但是,并不为Docker容器进行任何网络配置。也就是说,这个Docker容器没有网卡、IP、路由等信息。需要我们自己为Docker容器添加网卡、配置IP等。

这种网络模式下容器只有lo回环网络,没有其他网卡。none模式可以在容器创建时通过–network=none来指定。这种类型的网络没有办法联网,封闭的网络能很好的保证容器的安全性。

4.bridge模式

当Docker进程启动时,会在主机上创建一个名为docker0的虚拟网桥,此主机上启动的Docker容器会连接到这个虚拟网桥上。虚拟网桥的工作方式和物理交换机类似,这样主机上的所有容器就通过交换机连在了一个二层网络中。

从docker0子网中分配一个IP给容器使用,并设置docker0的IP地址为容器的默认网关。在主机上创建一对虚拟网卡veth pair设备,Docker将veth pair设备的一端放在新创建的容器中,并命名为eth0(容器的网卡),另一端放在主机中,以vethxxx这样类似的名字命名,并将这个网络设备加入到docker0网桥中。可以通过brctl show命令查看。

bridge模式是docker的默认网络模式,不写–net参数,就是bridge模式。使用docker run -p时,docker实际是在iptables做了DNAT规则,实现端口转发功能。可以使用iptables -t nat -vnL查看。

3、自定义网络

因为docker0,默认情况下不能通过容器名进行访问。需要通过–link进行设置连接。这样的操作比较麻烦,更推荐的方式是自定义网络,容器都使用该自定义网络,就可以实现通过容器名来互相访问了。

查看network的相关命令:docker network --help

查看默认的网络bridge的详细信息:

[root@VM-20-6-centos bbs]# docker network inspect bridge
[
    {
        "Name": "bridge",
        "Id": "09effc5a67d7fdde17c50f3c3ab427cc59946fc389f48f4d8f44721a702fad0c",
        "Created": "2022-03-25T14:12:28.755675457+08:00",
        "Scope": "local",
        "Driver": "bridge",
        "EnableIPv6": false,
        "IPAM": {
            "Driver": "default",
            "Options": null,
            "Config": [
                {
                    "Subnet": "172.17.0.0/16"
                }
            ]
        },
        "Internal": false,
        "Attachable": false,
        "Ingress": false,
        "ConfigFrom": {
            "Network": ""
        },
        "ConfigOnly": false,
        "Containers": { #这里描述了所有容器的网络信息
            "c818daefe0b17c2a8f76cc7a63855c95bf0730800a365fe32764d339077f7f35": {
                "Name": "tomcat01",
                "EndpointID": "f722ccde62177d0769ab15990ec25dbe88d554c7dce240d27668de0d68288cc1",
                "MacAddress": "02:42:ac:11:00:02",
                "IPv4Address": "172.17.0.2/16",
                "IPv6Address": ""
            }
        },
        "Options": {
            "com.docker.network.bridge.default_bridge": "true",
            "com.docker.network.bridge.enable_icc": "true",
            "com.docker.network.bridge.enable_ip_masquerade": "true",
            "com.docker.network.bridge.host_binding_ipv4": "0.0.0.0",
            "com.docker.network.bridge.name": "docker0",
            "com.docker.network.driver.mtu": "1500"
        },
        "Labels": {}
    }
]

查看 network create命令的相关参数:docker network create --help

下面自定义一个网络

docker network create --driver bridge --subnet 192.168.0.0/16 --gateway 192.168.0.1 mynet

参数说明:

--driver bridge   #指定bridge驱动程序来管理网络
--subnet 192.168.0.0/16 #指定网段的CIDR格式的子网
--gateway 192.168.0.1 	#指定主子网的IPv4或IPv6网关

网络mynet创建成功后,查看网络信息:docker network inspect mynet

[root@VM-20-6-centos bbs]# docker network inspect mynet
[
    {
        "Name": "mynet",
        "Id": "3c53732f649da6544b227671838c93aba47cb58dae560c52e5bcd3243a50312a",
        "Created": "2022-03-30T16:46:54.10443745+08:00",
        "Scope": "local",
        "Driver": "bridge",
        "EnableIPv6": false,
        "IPAM": {
            "Driver": "default",
            "Options": {},
            "Config": [
                {
                    "Subnet": "192.168.0.0/16",
                    "Gateway": "192.168.0.1"
                }
            ]
        },
        "Internal": false,
        "Attachable": false,
        "Ingress": false,
        "ConfigFrom": {
            "Network": ""
        },
        "ConfigOnly": false,
        "Containers": {},
        "Options": {},
        "Labels": {}
    }
]

下面启动两个容器,指定使用该自定义网络mynet,测试处于自定义网络下的容器,是否可以直接通过容器名进行网络访问。

docker run -d -P --name tomcat-net-01 --net mynet tomcat 
docker run -d -P --name tomcat-net-02 --net mynet tomcat 
docker network inspect mynet
[root@VM-20-6-centos bbs]# docker network inspect mynet
[
    {
        "Name": "mynet",
        "Id": "3c53732f649da6544b227671838c93aba47cb58dae560c52e5bcd3243a50312a",
        "Created": "2022-03-30T16:46:54.10443745+08:00",
        "Scope": "local",
        "Driver": "bridge",
        "EnableIPv6": false,
        "IPAM": {
            "Driver": "default",
            "Options": {},
            "Config": [
                {
                    "Subnet": "192.168.0.0/16",
                    "Gateway": "192.168.0.1"
                }
            ]
        },
        "Internal": false,
        "Attachable": false,
        "Ingress": false,
        "ConfigFrom": {
            "Network": ""
        },
        "ConfigOnly": false,
        "Containers": { #可查看到自定义中容器的相关网络信息
            "224fe08cec38572c124138f7c4762a2ab3668c572d97aad5848b1c1203694c8a": {
                "Name": "tomcat-net-02",
                "EndpointID": "e20dccdbdce90e800209d77aacabd2624785ceb51f3256a181dffa44d296a58a",
                "MacAddress": "02:42:c0:a8:00:03",
                "IPv4Address": "192.168.0.3/16",
                "IPv6Address": ""
            },
            "ccea31548995c404553fa0467fa73b02c529578bd5d8414ce8a329f0a8efcad1": {
                "Name": "tomcat-net-01",
                "EndpointID": "b941dda9f3a7cb958fa0e2ba4dbde6444a71fff25fff762e9b93f1d70d00c0ac",
                "MacAddress": "02:42:c0:a8:00:02",
                "IPv4Address": "192.168.0.2/16",
                "IPv6Address": ""
            }
        },
        "Options": {},
        "Labels": {}
    }
]

下面通过容器名来测试容器tomcat-net-01 和容器tomcat-net-02之间是否能正常网络通信

docker exec -it tomcat-net-01 ping tomcat-net-02
docker exec -it tomcat-net-02 ping tomcat-net-01
docker exec -it tomcat-net-01 ping 192.168.0.1

可以发现,在我们的自定义网络下,容器之间既可以通过容器名也可以通过ip地址进行网络通信。 我们自定义的网络默认已经帮我们维护了容器间的网络通信问题,这是实现网络互联的推荐方式。

4、Docker容器网络之间的互联

没有设置的情况下,不同网络间的容器是无法进行网络连接的。如图,两个不同的网络docker0和自定义网络mynet的网络模型图:

Docker网络连通

在默认网络bridge下启动容器tomcat-01,尝试连接mynet网络下的tomcat-net-01容器。可以看到是无法网络连接的。不同Docker网络之间的容器需要连接的话需要把作为调用方的容器注册一个ip到被调用方所在的网络上。需要使用docker connect命令。

下面设置容器tomcat-01连接到mynet网络上。并查看mynet的网络详情,可以看到给容器tomcat-01分配了一个ip地址。

#docker network connect 自定义网络名 容器名
docker network connect mynet tomcat01
[root@VM-20-6-centos bbs]# docker network inspect mynet
[
    {
        "Name": "mynet",
        "Id": "3c53732f649da6544b227671838c93aba47cb58dae560c52e5bcd3243a50312a",
        "Created": "2022-03-30T16:46:54.10443745+08:00",
        "Scope": "local",
        "Driver": "bridge",
        "EnableIPv6": false,
        "IPAM": {
            "Driver": "default",
            "Options": {},
            "Config": [
                {
                    "Subnet": "192.168.0.0/16",
                    "Gateway": "192.168.0.1"
                }
            ]
        },
        "Internal": false,
        "Attachable": false,
        "Ingress": false,
        "ConfigFrom": {
            "Network": ""
        },
        "ConfigOnly": false,
        "Containers": { #可以看到Tomcat01容器的网络信息出现了
            "224fe08cec38572c124138f7c4762a2ab3668c572d97aad5848b1c1203694c8a": {
                "Name": "tomcat-net-02",
                "EndpointID": "e20dccdbdce90e800209d77aacabd2624785ceb51f3256a181dffa44d296a58a",
                "MacAddress": "02:42:c0:a8:00:03",
                "IPv4Address": "192.168.0.3/16",
                "IPv6Address": ""
            },
            "c818daefe0b17c2a8f76cc7a63855c95bf0730800a365fe32764d339077f7f35": {
                "Name": "tomcat01",
                "EndpointID": "06e33f9f68aafdaf3fbfd17fc7ef6c77b62668e83af53d2b7abf8067fff089d0",
                "MacAddress": "02:42:c0:a8:00:04",
                "IPv4Address": "192.168.0.4/16",
                "IPv6Address": ""
            },
            "ccea31548995c404553fa0467fa73b02c529578bd5d8414ce8a329f0a8efcad1": {
                "Name": "tomcat-net-01",
                "EndpointID": "b941dda9f3a7cb958fa0e2ba4dbde6444a71fff25fff762e9b93f1d70d00c0ac",
                "MacAddress": "02:42:c0:a8:00:02",
                "IPv4Address": "192.168.0.2/16",
                "IPv6Address": ""
            }
        },
        "Options": {},
        "Labels": {}
    }
]

设置完成后我们就可以实现不同网络之间的容器互联了。

5、创建redis集群

部署三主三从的Redis集群

1.创建自定义网络

docker network create redis --subnet 172.38.0.0/16

2.创建redis配置信息

#直接输入命令即可
for port in $(seq 1 6); \
do \
mkdir -p /mydata/redis/node-${port}/conf
touch /mydata/redis/node-${port}/conf/redis.conf
cat << EOF >/mydata/redis/node-${port}/conf/redis.conf
port 6379 
bind 0.0.0.0
cluster-enabled yes 
cluster-config-file nodes.conf
cluster-node-timeout 5000
cluster-announce-ip 172.38.0.1${port}
cluster-announce-port 6379
cluster-announce-bus-port 16379
appendonly yes
EOF
done

3.启动redis容器,并挂载

for port in $(seq 1 6); \
do
docker run -p 637${port}:6379 -p 1637${port}:16379 --name redis-${port} \
-v /mydata/redis/node-${port}/data:/data \
-v /mydata/redis/node-${port}/conf/redis.conf:/etc/redis/redis.conf \
-d --net redis --ip 172.38.0.1${port} redis:5.0.9-alpine3.11 redis-server /etc/redis/redis.conf; \
done

4.创建集群

#进入redis-1
docker exec -it redis-1 /bin/sh
#创建集群
redis-cli --cluster create 172.38.0.11:6379 172.38.0.12:6379 172.38.0.13:6379 172.38.0.14:6379 172.38.0.15:6379 172.38.0.16:6379 --cluster-replicas 1
#输入:yes

5.测试

#查看集群信息
redis-cli -c
cluster info
#查看节点信息
cluster nodes
#测试主从复制是否生效
#先设置一个key
127.0.0.1:6379> set k v
-> Redirected to slot [7629] located at 172.38.0.12:6379
OK
#可以看到是redis-2处理了
#新建一个会话,停止Redis-2容器服务
docker stop afe82eccc706
#此时重新连接Redis-cli客户端,再次获取k
127.0.0.1:6379> get k
-> Redirected to slot [7629] located at 172.38.0.16:6379
"v"
#可以看到是redis-6处理

十四、springboot打包成Docker镜像

1.编写springboot项目

2.将springboot项目打包成jar

3.编写Dockerfile

FROM openjdk:8-jdk-alpine
VOLUME /tmp
#修改时区
ENV TZ=Asia/Shanghai
RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone
ADD demo-0.0.1-SNAPSHOT.jar app.jar #demo-0.0.1-SNAPSHOT.jar为打包后的jar包名
EXPOSE 8080
ENTRYPOINT ["java","-jar","/app.jar"]

4.构建镜像: docker build -t demo .

5.运行镜像:docker run -d -p 8080:8080 demo

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值