这可能是最为详细的Docker入门吐血总结
<h3 id="docker是什么">Docker是什么?</h3><p>在计算机技术日新月异的今天, <code>Docker</code> 在国内发展的如火如荼,特别是在一线互联网公司, <code>Docker</code> 的使用是十分普遍的,甚至成为了一些企业面试的加分项,不信的话看看下面这张图。</p>
<p><img src="https://img2018.cnblogs.com/blog/1100338/201810/1100338-20181010205426451-1244210434.png" alt="" loading="lazy"></p>
<p>这是我在某招聘网站上看到的招聘 <strong>Java开发工程师</strong> 的招聘要求,其中有一条熟悉 <code>docker</code> 成为了你快速入职的加分项,由此可见熟悉 <code>docker</code> 在互联网公司的地位之重要。</p>
<p>当然对于我们 <strong>CTF选手</strong> 而言,熟悉 <code>docker</code> 可以快速搭建 <code>CTF环境</code> ,完美地还原比赛真实漏洞的场景,帮助我们快速提升自己。</p>
<p>市面上已经有很多优秀的教程,但是很多原理性的东西,笔者认为那些教程对初学者而言还是很难理解,感觉没有说清楚(笔者自己都觉得挺懵逼的),为了让初学者少走弯路,我将以我的学习经历以及作为一个 <strong>CTF选手</strong> 的角度,编写此套教程,来带大家去了解并熟练运用 <code>docker</code> ,祝愿各位读者朋友们学完此套教程后,在未来企业面试中能够多一项加分的筹码,能够帮助到大家,我觉得就很值了。</p>
<p>既然说了这么多, <code>docker</code> 到底是个什么东西呢?</p>
<p>我们在理解 <code>docker</code> 之前,首先我们得先区分清楚两个概念,<strong>容器</strong>和<strong>虚拟机</strong>。</p>
<p>可能很多读者朋友都用过虚拟机,而对容器这个概念比较的陌生。</p>
<p>我们用的传统虚拟机如 <code>VMware</code> , <code>VisualBox</code> 之类的需要模拟整台机器包括硬件,每台虚拟机都需要有自己的操作系统,虚拟机一旦被开启,预分配给它的资源将全部被占用。每一台虚拟机包括应用,必要的二进制和库,以及一个完整的用户操作系统。</p>
<p>而容器技术是和我们的宿主机共享硬件资源及操作系统,可以实现资源的动态分配。容器包含应用和其所有的依赖包,但是与其他容器共享内核。容器在宿主机操作系统中,在用户空间以分离的进程运行。</p>
<p>容器技术是实现操作系统虚拟化的一种途径,可以让您在资源受到隔离的进程中运行应用程序及其依赖关系。通过使用容器,我们可以轻松打包应用程序的代码、配置和依赖关系,将其变成容易使用的构建块,从而实现环境一致性、运营效率、开发人员生产力和版本控制等诸多目标。容器可以帮助保证应用程序快速、可靠、一致地部署,其间不受部署环境的影响。容器还赋予我们对资源更多的精细化控制能力,让我们的基础设施效率更高。通过下面这幅图我们可以很直观的反映出这两者的区别所在。</p>
<p><img src="https://img2018.cnblogs.com/blog/1100338/201810/1100338-20181010205426157-1788702025.png" alt="" loading="lazy"></p>
<p><strong>Docker 属于 Linux 容器的一种封装,提供简单易用的容器使用接口。</strong>它是目前最流行的 <code>Linux</code> 容器解决方案。</p>
<p>而 <code>Linux</code> 容器是 <code>Linux</code> 发展出了另一种虚拟化技术,简单来讲, <code>Linux</code> 容器不是模拟一个完整的操作系统,而是对进程进行隔离,相当于是在正常进程的外面套了一个保护层。对于容器里面的进程来说,它接触到的各种资源都是虚拟的,从而实现与底层系统的隔离。</p>
<p><code>Docker</code> 将应用程序与该程序的依赖,打包在一个文件里面。运行这个文件,就会生成一个虚拟容器。程序在这个虚拟容器里运行,就好像在真实的物理机上运行一样。有了 <code>Docker</code> ,就不用担心环境问题。</p>
<p>总体来说, <code>Docker</code>的接口相当简单,用户可以方便地创建和使用容器,把自己的应用放入容器。容器还可以进行版本管理、复制、分享、修改,就像管理普通的代码一样。</p>
<h3 id="docker的优势">Docker的优势</h3>
<p><strong>Docker相比于传统虚拟化方式具有更多的优势:</strong></p>
<ul>
<li><code>docker</code> 启动快速属于秒级别。虚拟机通常需要几分钟去启动</li>
<li><code>docker</code> 需要的资源更少, <code>docker</code> 在操作系统级别进行虚拟化, <code>docker</code> 容器和内核交互,几乎没有性能损耗,性能优于通过 <code>Hypervisor</code> 层与内核层的虚拟化</li>
<li><code>docker</code> 更轻量, <code>docker</code> 的架构可以共用一个内核与共享应用程序库,所占内存极小。同样的硬件环境, <code>Docker</code> 运行的镜像数远多于虚拟机数量,对系统的利用率非常高</li>
<li>与虚拟机相比, <code>docker</code> 隔离性更弱, <code>docker</code> 属于进程之间的隔离,虚拟机可实现系统级别隔离</li>
<li>安全性:<code>docker</code> 的安全性也更弱。 <code>Docker</code> 的租户 <code>root</code> 和宿主机 <code>root</code> 等同,一旦容器内的用户从普通用户权限提升为root权限,它就直接具备了宿主机的root权限,进而可进行无限制的操作。虚拟机租户 <code>root</code> 权限和宿主机的 <code>root</code> 虚拟机权限是分离的,并且虚拟机利用如 <code>Intel</code> 的 <code>VT-d</code> 和 <code>VT-x</code> 的 <code>ring-1</code> 硬件隔离技术,这种隔离技术可以防止虚拟机突破和彼此交互,而容器至今还没有任何形式的硬件隔离,这使得容器容易受到攻击</li>
<li>可管理性: <code>docker</code> 的集中化管理工具还不算成熟。各种虚拟化技术都有成熟的管理工具,例如 <code>VMware vCenter</code> 提供完备的虚拟机管理能力</li>
<li>高可用和可恢复性: <code>docker</code> 对业务的高可用支持是通过快速重新部署实现的。虚拟化具备负载均衡,高可用,容错,迁移和数据保护等经过生产实践检验的成熟保障机制, <code>VMware</code> 可承诺虚拟机 <code>99.999%</code> 高可用,保证业务连续性</li>
<li>快速创建、删除:虚拟化创建是分钟级别的, <code>Docker</code> 容器创建是秒级别的, <code>Docker</code> 的快速迭代性,决定了无论是开发、测试、部署都可以节约大量时间</li>
<li>交付、部署:虚拟机可以通过镜像实现环境交付的一致性,但镜像分发无法体系化。 <code>Docker</code> 在 <code>Dockerfile</code> 中记录了容器构建过程,可在集群中实现快速分发和快速部署</li>
</ul>
<p>我们可以从下面这张表格很清楚地看到容器相比于传统虚拟机的特性的优势所在:</p>
<table>
<thead>
<tr>
<th style="text-align: center">特性</th>
<th style="text-align: center">容器</th>
<th style="text-align: center">虚拟机</th>
</tr>
</thead>
<tbody>
<tr>
<td style="text-align: center">启动</td>
<td style="text-align: center">秒级</td>
<td style="text-align: center">分钟级</td>
</tr>
<tr>
<td style="text-align: center">硬盘使用</td>
<td style="text-align: center">一般为MB</td>
<td style="text-align: center">一般为GB</td>
</tr>
<tr>
<td style="text-align: center">性能</td>
<td style="text-align: center">接近原生</td>
<td style="text-align: center">弱于</td>
</tr>
<tr>
<td style="text-align: center">系统支持量</td>
<td style="text-align: center">单机支持上千个容器</td>
<td style="text-align: center">一般是几十个</td>
</tr>
</tbody>
</table>
<h3 id="docker的三个基本概念">Docker的三个基本概念</h3>
<p><img src="https://img2018.cnblogs.com/blog/1100338/201810/1100338-20181010205425908-509725301.jpg" alt="" loading="lazy"></p>
<p>从上图我们可以看到,<code>Docker</code> 中包括三个基本的概念:</p>
<ul>
<li><code>Image</code>(镜像)</li>
<li><code>Container</code>(容器)</li>
<li><code>Repository</code>(仓库)</li>
</ul>
<p>镜像是 <code>Docker</code> 运行容器的前提,仓库是存放镜像的场所,可见镜像更是 <code>Docker</code> 的核心。</p>
<h4 id="image-镜像">Image (镜像)</h4>
<p>那么镜像到底是什么呢?</p>
<p><code>Docker</code> 镜像可以看作是一个特殊的文件系统,除了提供容器运行时所需的程序、库、资源、配置等文件外,还包含了一些为运行时准备的一些配置参数(如匿名卷、环境变量、用户等)。镜像不包含任何动态数据,其内容在构建之后也不会被改变。</p>
<p>镜像<code>(Image)</code>就是一堆只读层<code>(read-only layer)</code>的统一视角,也许这个定义有些难以理解,下面的这张图能够帮助读者理解镜像的定义。</p>
<p><img src="https://img2018.cnblogs.com/blog/1100338/201810/1100338-20181010205425698-1711765011.png" alt="" loading="lazy"></p>
<p>从左边我们看到了多个只读层,它们重叠在一起。除了最下面一层,其它层都会有一个指针指向下一层。这些层是<code>Docker</code> 内部的实现细节,并且能够在主机的文件系统上访问到。统一文件系统 <code>(union file system)</code> 技术能够将不同的层整合成一个文件系统,为这些层提供了一个统一的视角,这样就隐藏了多层的存在,在用户的角度看来,只存在一个文件系统。我们可以在图片的右边看到这个视角的形式。</p>
<h4 id="container-容器">Container (容器)</h4>
<p>容器 <code>(container)</code> 的定义和镜像 <code>(image)</code> 几乎一模一样,也是一堆层的统一视角,唯一区别在于容器的最上面那一层是可读可写的。</p>
<p><img src="https://img2018.cnblogs.com/blog/1100338/201810/1100338-20181010205425262-960721404.png" alt="" loading="lazy"></p>
<p>由于容器的定义并没有提及是否要运行容器,所以实际上,容器 = 镜像 + 读写层。</p>
<h4 id="repository-仓库">Repository (仓库)</h4>
<p><code>Docker</code> 仓库是集中存放镜像文件的场所。镜像构建完成后,可以很容易的在当前宿主上运行,但是, 如果需要在其它服务器上使用这个镜像,我们就需要一个集中的存储、分发镜像的服务,<code>Docker Registry</code> (仓库注册服务器)就是这样的服务。有时候会把仓库 <code>(Repository)</code> 和仓库注册服务器 <code>(Registry)</code> 混为一谈,并不严格区分。<code>Docker</code> 仓库的概念跟 <code>Git</code> 类似,注册服务器可以理解为 <code>GitHub</code> 这样的托管服务。实际上,一个 <code>Docker Registry</code> 中可以包含多个仓库 <code>(Repository)</code> ,每个仓库可以包含多个标签 <code>(Tag)</code>,每个标签对应着一个镜像。所以说,镜像仓库是 <code>Docker</code> 用来集中存放镜像文件的地方类似于我们之前常用的代码仓库。</p>
<p>通常,<strong>一个仓库会包含同一个软件不同版本的镜像</strong>,而<strong>标签就常用于对应该软件的各个版本</strong> 。我们可以通过<code><仓库名>:<标签></code>的格式来指定具体是这个软件哪个版本的镜像。如果不给出标签,将以 <code>latest</code> 作为默认标签.。</p>
<p>仓库又可以分为两种形式:</p>
<ul>
<li><code>public</code>(公有仓库)</li>
<li><code>private</code>(私有仓库)</li>
</ul>
<p><code>Docker Registry</code> 公有仓库是开放给用户使用、允许用户管理镜像的 <code>Registry</code> 服务。一般这类公开服务允许用户免费上传、下载公开的镜像,并可能提供收费服务供用户管理私有镜像。</p>
<p>除了使用公开服务外,用户还可以在本地搭建私有 <code>Docker Registry</code> 。<code>Docker</code> 官方提供了 <code>Docker Registry</code> 镜像,可以直接使用做为私有 <code>Registry</code> 服务。当用户创建了自己的镜像之后就可以使用 <code>push</code> 命令将它上传到公有或者私有仓库,这样下次在另外一台机器上使用这个镜像时候,只需要从仓库上 <code>pull</code> 下来就可以了。</p>
<p>我们主要把 <code>Docker</code> 的一些常见概念如 <code>Image</code> , <code>Container</code> , <code>Repository</code> 做了详细的阐述,也从传统虚拟化方式的角度阐述了 <code>docker</code> 的优势,我们从下图可以直观地看到 <code>Docker</code> 的架构:</p>
<p><img src="https://img2018.cnblogs.com/blog/1100338/201810/1100338-20181011200344086-1510826338.png" alt="framework" loading="lazy"></p>
<p><code>Docker</code> 使用 <code>C/S</code> 结构,即<strong>客户端/服务器</strong>体系结构。 <code>Docker</code> 客户端与 <code>Docker</code> 服务器进行交互,Docker服务端负责构建、运行和分发 <code>Docker</code> 镜像。 <code>Docker</code> 客户端和服务端可以运行在一台机器上,也可以通过 <code>RESTful</code> 、 <code>stock</code> 或网络接口与远程 <code>Docker</code> 服务端进行通信。</p>
<p><img src="https://img2018.cnblogs.com/blog/1100338/201810/1100338-20181011200343656-1972949758.png" alt="docker-framework" loading="lazy"></p>
<p>这张图展示了 <code>Docker</code> 客户端、服务端和 <code>Docker</code> 仓库(即 <code>Docker Hub</code> 和 <code>Docker Cloud</code> ),默认情况下<code>Docker</code> 会在 <code>Docker</code> 中央仓库寻找镜像文件,这种利用仓库管理镜像的设计理念类似于 <code>Git</code> ,当然这个仓库是可以通过修改配置来指定的,甚至我们可以创建我们自己的私有仓库。</p>
<h3 id="docker的安装和使用">Docker的安装和使用</h3>
<p><code>Docker</code> 的安装和使用有一些前提条件,主要体现在体系架构和内核的支持上。对于体系架构,除了 <code>Docker</code> 一开始就支持的 <code>X86-64</code> ,其他体系架构的支持则一直在不断地完善和推进中。</p>
<p><code>Docker</code> 分为 <code>CE</code> 和 <code>EE</code> 两大版本。 <code>CE</code> 即社区版(免费,支持周期 <code>7</code> 个月), <code>EE</code> 即企业版,强调安全,付费使用,支持周期 <code>24</code> 个月。</p>
<p>我们在安装前可以参看官方文档获取最新的 <code>Docker</code> 支持情况,官方文档在这里:</p>
<pre><code class="language-reStructuredText">https://docs.docker.com/install/
</code></pre>
<p><code>Docker</code> 对于内核支持的功能,即内核的配置选项也有一定的要求(比如必须开启 <code>Cgroup</code> 和 <code>Namespace</code> 相关选项,以及其他的网络和存储驱动等), <code>Docker</code> 源码中提供了一个检测脚本来检测和指导内核的配置,脚本链接在这里:</p>
<pre><code class="language-reStructuredText">https://raw.githubusercontent.com/docker/docker/master/contrib/check-config.sh
</code></pre>
<p>在满足前提条件后,安装就变得非常的简单了。</p>
<p><code>Docker CE</code> 的安装请参考官方文档:</p>
<ul>
<li><code>MacOS</code>:https://docs.docker.com/docker-for-mac/install/</li>
<li><code>Windows</code>:https://docs.docker.com/docker-for-windows/install/</li>
<li><code>Ubuntu</code>:https://docs.docker.com/install/linux/docker-ce/ubuntu/</li>
<li><code>Debian</code>:https://docs.docker.com/install/linux/docker-ce/debian/</li>
<li><code>CentOS</code>:https://docs.docker.com/install/linux/docker-ce/centos/</li>
<li><code>Fedora</code>:https://docs.docker.com/install/linux/docker-ce/fedora/</li>
<li>其他 <code>Linux</code> 发行版:https://docs.docker.com/install/linux/docker-ce/binaries/</li>
</ul>
<p>这里我们以 <code>CentOS7</code> 作为本文的演示。</p>
<h4 id="环境准备">环境准备</h4>
<ul>
<li><strong>阿里云服务器(1核2G,1M带宽)</strong></li>
<li><strong>CentOS 7.4 64位</strong></li>
</ul>
<p>由于 <code>Docker-CE</code> 支持 <code>64</code> 位版本的 <code>CentOS7</code> ,并且要求内核版本不低于 <code>3.10</code></p>
<p>首先我们需要卸载掉旧版本的 <code>Docker</code></p>
<pre><code class="language-shell">$ sudo yum remove docker \
docker-client \
docker-client-latest \
docker-common \
docker-latest \
docker-latest-logrotate \
docker-logrotate \
docker-selinux \
docker-engine-selinux \
docker-engine
</code></pre>
<p><img src="https://img2018.cnblogs.com/blog/1100338/201810/1100338-20181011200343338-1510604822.png" alt="" loading="lazy"></p>
<p>我们执行以下安装命令去安装依赖包:</p>
<pre><code class="language-shell">$ sudo yum install -y yum-utils \
device-mapper-persistent-data \
lvm2
</code></pre>
<p>这里我事先已经安装过了,所以提示我已经安装了最新版本</p>
<p><img src="https://img2018.cnblogs.com/blog/1100338/201810/1100338-20181011200343082-1053047604.png" alt="" loading="lazy"></p>
<p><strong>安装Docker</strong></p>
<p><code>Docker</code> 软件包已经包括在默认的 <code>CentOS-Extras</code> 软件源里。因此想要安装 <code>docker</code>,只需要运行下面的 <code>yum</code> 命令</p>
<pre><code class="language-shell">$ sudo yum install docker
</code></pre>
<p>当然在测试或开发环境中 <code>Docker</code> 官方为了简化安装流程,提供了一套便捷的安装脚本,<code>CentOS</code> 系统上可以使用这套脚本安装:</p>
<pre><code class="language-shell">curl -fsSL get.docker.com -o get-docker.sh
sh get-docker.sh
</code></pre>
<p>具体可以参看 <code>docker-install</code> 的脚本:</p>
<pre><code class="language-reStructuredText">https://github.com/docker/docker-install
</code></pre>
<p>执行这个命令后,脚本就会自动的将一切准备工作做好,并且把 <code>Docker CE</code> 的 <code>Edge</code> 版本安装在系统中。</p>
<p>安装完成后,运行下面的命令,验证是否安装成功:</p>
<pre><code class="language-shell">docker version
or
docker info
</code></pre>
<p>返回docker的版本相关信息,证明 <code>docker</code> 安装成功</p>
<p><img src="https://img2018.cnblogs.com/blog/1100338/201810/1100338-20181011200342486-1791743202.png" alt="" loading="lazy"></p>
<p>启动Docker-CE</p>
<pre><code class="language-shell">$ sudo systemctl enable docker
$ sudo systemctl start docker
</code></pre>
<h4 id="docker的简单运用---hello-world">Docker的简单运用---Hello World</h4>
<p>由于服务器日常崩溃了, <code>docker</code> 出了点问题,所以以下案例的演示是基于 <code>Kali Linux</code> 环境下进行的。</p>
<p>我们通过最简单的 <code>image</code> 文件 <code>hello world</code>,感受一下 <code>Docker</code> 的魅力吧!</p>
<p>我们直接运行下面的命令,将名为 <code>hello-world</code> 的 <code>image</code> 文件从仓库抓取到本地。</p>
<pre><code class="language-shell">docker pull library/hello-world
</code></pre>
<p><code>docker pull images</code> 是抓取 <code>image</code> 文件, <code>library/hello-world</code> 是 <code>image</code> 文件在仓库里面的位置,其中 <code>library</code> 是 <code>image</code> 文件所在的组, <code>hello-world</code> 是 <code>image</code> 文件的名字。</p>
<p><img src="https://img2018.cnblogs.com/blog/1100338/201810/1100338-20181011200342100-457982713.png" alt="" loading="lazy"></p>
<p>抓取成功以后,就可以在本机看到这个 <code>image</code> 文件了。</p>
<pre><code class="language-shell">docker images
</code></pre>
<p>我们可以看到如下结果:</p>
<p><img src="https://img2018.cnblogs.com/blog/1100338/201810/1100338-20181011200341767-414070160.png" alt="" loading="lazy"></p>
<p>现在,我们可以运行 <code>hello-world</code> 这个 <code>image</code> 文件</p>
<pre><code>docker run hello-world
</code></pre>
<p>我们可以看到如下结果:</p>
<p><img src="https://img2018.cnblogs.com/blog/1100338/201810/1100338-20181011200341270-1701862326.png" alt="" loading="lazy"></p>
<p>输出这段提示以后,<code>hello world</code> 就会停止运行,容器自动终止。有些容器不会自动终止,因为提供的是服务,比如Mysql镜像等。</p>
<p>是不是很 <code>easy</code> 呢?我们从上面可以看出, <code>docker</code> 的功能是十分强大的,除此之外,我们还可以拉去一些 <code>Ubuntu</code> , <code>Apache</code> 等镜像,在未来的教程中我们将会一一提到。</p>
<p><code>Docker</code> 提供了一套简单实用的命令来创建和更新镜像,我们可以通过网络直接下载一个已经创建好了的应用镜像,并通过 <code>Docker RUN</code> 命令就可以直接使用。当镜像通过 <code>RUN</code> 命令运行成功后,这个运行的镜像就是一个 <code>Docker</code> 容器啦,容器可以理解为一个轻量级的沙箱, <code>Docker</code> 利用容器来运行和隔离应用,容器是可以被启动、停止、删除的,这并不会影响 <code>Docker</code> 镜像。</p>
<p>我们可以看看下面这幅图:</p>
<p><img src="https://img2018.cnblogs.com/blog/1100338/201810/1100338-20181011200340694-1202370283.png" alt="" loading="lazy"></p>
<p><code>Docker</code> 客户端是 <code>Docker</code> 用户与 <code>Docker</code> 交互的主要方式。当您使用 <code>docker</code> 命令行运行命令时, <code>Docker</code> 客户端将这些命令发送给服务器端,服务端将执行这些命令。 <code>docker</code> 命令使用 <code>docker API</code> 。 <code>Docker</code> 客户端可以与多个服务端进行通信。</p>
<p>我们将剖析一下 <code>Docker</code> 容器是如何工作的,学习好Docker容器工作的原理,我们就可以自己去管理我们的容器了。</p>
<h3 id="docker架构">Docker架构</h3>
<p>在上面的学习中,我们简单地讲解了Docker的基本架构。了解到了<code>Docker</code> 使用的是 <code>C/S</code> 结构,即<strong>客户端/服务器</strong>体系结构。明白了 <code>Docker</code> 客户端与 <code>Docker</code> 服务器进行交互时, <code>Docker</code> 服务端负责构建、运行和分发 <code>Docker</code> 镜像。 也知道了<code>Docker</code> 客户端和服务端可以运行在一台机器上,可以通过 <code>RESTful</code> 、 <code>stock</code> 或网络接口与远程 <code>Docker</code> 服务端进行通信。</p>
<p>我们从下图可以很直观的了解到Docker的架构:</p>
<p><img src="https://img2018.cnblogs.com/blog/1100338/201810/1100338-20181012214103608-969706945.png" alt="" loading="lazy"></p>
<p><code>Docker</code> 的核心组件包括:</p>
<ol>
<li><strong>Docker Client</strong></li>
<li><strong>Docker daemon</strong></li>
<li><strong>Docker Image</strong></li>
<li><strong>Docker Registry</strong></li>
<li><strong>Docker Container</strong></li>
</ol>
<p><code>Docker</code> 采用的是 <code>Client/Server</code> 架构。客户端向服务器发送请求,服务器负责构建、运行和分发容器。客户端和服务器可以运行在同一个 <code>Host</code> 上,客户端也可以通过 <code>socket</code> 或 <code>REST API</code> 与远程的服务器通信。可能很多朋友暂时不太理解一些东西,比如 <code>REST API</code> 是什么东西等,不过没关系,在后面的文章中会一一给大家讲解清楚。</p>
<h4 id="docker-client">Docker Client</h4>
<p><code>Docker Client</code> ,也称 <code>Docker</code> 客户端。它其实就是 <code>Docker</code> 提供命令行界面 <code>(CLI)</code> 工具,是许多 <code>Docker</code> 用户与 <code>Docker</code> 进行交互的主要方式。客户端可以构建,运行和停止应用程序,还可以远程与<code>Docker_Host</code>进行交互。最常用的 <code>Docker</code> 客户端就是 <code>docker</code> 命令,我们可以通过 <code>docker</code> 命令很方便地在 <code>host</code> 上构建和运行 <code>docker</code> 容器。</p>
<p><img src="https://img2018.cnblogs.com/blog/1100338/201810/1100338-20181012214102869-1003920118.png" alt="" loading="lazy"></p>
<h4 id="docker-daemon">Docker daemon</h4>
<p><code>Docker daemon</code> 是服务器组件,以 <code>Linux</code> 后台服务的方式运行,是 <code>Docker</code> 最核心的后台进程,我们也把它称为守护进程。它负责响应来自 <code>Docker Client</code> 的请求,然后将这些请求翻译成系统调用完成容器管理操作。该进程会在后台启动一个 <code>API Server</code> ,负责接收由 <code>Docker Client</code> 发送的请求,接收到的请求将通过<code>Docker daemon</code> 内部的一个路由分发调度,由具体的函数来执行请求。</p>
<p><img src="https://img2018.cnblogs.com/blog/1100338/201810/1100338-20181012214101750-1118997362.png" alt="" loading="lazy"></p>
<p>我们大致可以将其分为以下三部分:</p>
<ul>
<li><strong>Docker Server</strong></li>
<li><strong>Engine</strong></li>
<li><strong>Job</strong></li>
</ul>
<p><strong>Docker Daemon</strong>的架构如下所示:</p>
<p><img src="https://img2018.cnblogs.com/blog/1100338/201810/1100338-20181012214101184-1339527466.jpg" alt="" loading="lazy"></p>
<p><code>Docker Daemon</code> 可以认为是通过 <code>Docker Server</code> 模块接受 <code>Docker Client</code> 的请求,并在 <code>Engine</code> 中处理请求,然后根据请求类型,创建出指定的 <code>Job</code> 并运行。 <code>Docker Daemon</code> 运行在<code>Docker host</code> 上,负责创建、运行、监控容器,构建、存储镜像。</p>
<p>运行过程的作用有以下几种可能:</p>
<ul>
<li>向 <code>Docker Registry</code> 获取镜像</li>
<li>通过 <code>graphdriver</code> 执行容器镜像的本地化操作</li>
<li>通过 <code>networkdriver</code> 执行容器网络环境的配置</li>
<li>通过 <code>execdriver</code> 执行容器内部运行的执行工作</li>
</ul>
<p>由于 <code>Docker Daemon</code> 和 <code>Docker Client</code> 的启动都是通过可执行文件 <code>docker</code> 来完成的,因此两者的启动流程非常相似。 <code>Docker</code> 可执行文件运行时,运行代码通过不同的命令行 <code>flag</code> 参数,区分两者,并最终运行两者各自相应的部分。</p>
<p>启动 <code>Docker Daemon</code> 时,一般可以使用以下命令来完成</p>
<pre><code class="language-shell">docker --daemon = true
docker –d
docker –d = true
</code></pre>
<p>再由 <code>docker</code> 的 <code>main()</code> 函数来解析以上命令的相应 <code>flag</code> 参数,并最终完成 <code>Docker Daemon</code> 的启动。</p>
<p>下图可以很直观地看到 <code>Docker Daemon</code> 的启动流程:</p>
<p><img src="https://img2018.cnblogs.com/blog/1100338/201810/1100338-20181012214100787-426925315.jpg" alt="" loading="lazy"></p>
<p>默认配置下, <code>Docker daemon</code> 只能响应来自本地 <code>Host</code> 的客户端请求。如果要允许远程客户端请求,需要在配置文件中打开 <code>TCP</code> 监听。我们可以照着如下步骤进行配置:</p>
<p>1、编辑配置文件 <code>/etc/systemd/system/multi-user.target.wants/docker.service</code> ,在环境变量 <code>ExecStart</code> 后面添加 <code>-H tcp://0.0.0.0</code>,允许来自任意 IP 的客户端连接。</p>
<p><img src="https://img2018.cnblogs.com/blog/1100338/201810/1100338-20181012214100440-905260954.jpg" alt="" loading="lazy"></p>
<p>2、重启 <code>Docker daemon</code></p>
<pre><code class="language-shell">systemctl daemon-reload
systemctl restart docker.service
</code></pre>
<p>3、我们通过以下命令即可实现与远程服务器通信</p>
<pre><code class="language-shell">docker -H 服务器IP地址 info
</code></pre>
<p><code>-H</code> 是用来指定服务器主机, <code>info</code> 子命令用于查看 <code>Docker</code> 服务器的信息</p>
<h4 id="docker-image">Docker Image</h4>
<p><code>Docker</code> 镜像可以看作是一个特殊的文件系统,除了提供容器运行时所需的程序、库、资源、配置等文件外,还包含了一些为运行时准备的一些配置参数(如匿名卷、环境变量、用户等)。镜像不包含任何动态数据,其内容在构建之后也不会被改变。我们可将 <code>Docker</code> 镜像看成只读模板,通过它可以创建 <code>Docker</code> 容器。</p>
<p>镜像有多种生成方法:</p>
<ol>
<li>从无到有开始创建镜像</li>
<li>下载并使用别人创建好的现成的镜像</li>
<li>在现有镜像上创建新的镜像</li>
</ol>
<p>我们可以将镜像的内容和创建步骤描述在一个文本文件中,这个文件被称作 <code>Dockerfile</code> ,通过执行 <code>docker build <docker-file></code> 命令可以构建出 Docker 镜像,在后续的教程中,我们会用一篇专门讨论这个问题。</p>
<h4 id="docker-registry">Docker Registry</h4>
<p><code>Docker registry</code> 是存储 <code>docker image</code> 的仓库,它在 <code>docker</code> 生态环境中的位置如下图所示:</p>
<p><img src="https://img2018.cnblogs.com/blog/1100338/201810/1100338-20181012214100277-775657574.jpg" alt="" loading="lazy"></p>
<p>运行<code>docker push</code>、<code>docker pull</code>、<code>docker search</code>时,实际上是通过 <code>docker daemon</code> 与 <code>docker registry</code> 通信。</p>
<h4 id="docker-container">Docker Container</h4>
<p><code>Docker</code> 容器就是 <code>Docker</code> 镜像的运行实例,是真正运行项目程序、消耗系统资源、提供服务的地方。 <code>Docker Container</code> 提供了系统硬件环境,我们可以使用 <code>Docker Images</code> 这些制作好的系统盘,再加上我们所编写好的项目代码, <code>run</code> 一下就可以提供服务啦。</p>
<h3 id="docker组件是如何协作运行容器">Docker组件是如何协作运行容器</h3>
<p>看到这里,我相信各位读者朋友们应该已经对Docker基础架构已经熟悉的差不多了,我们还记得运行的第一个容器吗?现在我们再通过hello-world这个例子来体会一下 <code>Docker</code> 各个组件是如何协作的。</p>
<p>容器启动过程如下:</p>
<ul>
<li><code>Docker</code> 客户端执行 <code>docker run</code> 命令</li>
<li><code>Docker daemon</code> 发现本地没有 <code>hello-world</code> 镜像</li>
<li><code>daemon</code> 从 <code>Docker Hub</code> 下载镜像</li>
<li>下载完成,镜像 <code>hello-world</code> 被保存到本地</li>
<li><code>Docker daemon</code> 启动容器</li>
</ul>
<p>具体过程可以看如下这幅演示图:</p>
<p><img src="https://img2018.cnblogs.com/blog/1100338/201810/1100338-20181012214059835-734054348.png" alt="" loading="lazy"></p>
<p>我们可以通过<code>docker images</code> 可以查看到 <code>hello-world</code> 已经下载到本地</p>
<p><img src="https://img2018.cnblogs.com/blog/1100338/201810/1100338-20181012214059170-1387452245.png" alt="" loading="lazy"></p>
<p>我们可以通过<code>docker ps</code> 或者 <code>docker container ls</code> 显示正在运行的容器,我们可以看到, <code>hello-world</code> 在输出提示信息以后就会停止运行,容器自动终止,所以我们在查看的时候没有发现有容器在运行。</p>
<p><img src="https://img2018.cnblogs.com/blog/1100338/201810/1100338-20181012214058803-1594502697.png" alt="" loading="lazy"></p>
<p>我们把 <code>Docker</code> 容器的工作流程剖析的十分清楚了,我们大体可以知道 <code>Docker</code> 组件协作运行容器可以分为以下几个过程:</p>
<ol>
<li><code>Docker</code> 客户端执行 <code>docker run</code> 命令</li>
<li><code>Docker daemon</code> 发现本地没有我们需要的镜像</li>
<li><code>daemon</code> 从 <code>Docker Hub</code> 下载镜像</li>
<li>下载完成后,镜像被保存到本地</li>
<li><code>Docker daemon</code> 启动容器</li>
</ol>
<p>了解了这些过程以后,我们再来理解这些命令就不会觉得很突兀了,下面我来给大家讲讲 <code>Docker</code> 常用的一些命令操作吧。</p>
<h3 id="docker常用命令">Docker常用命令</h3>
<p>我们可以通过 <code>docker -h</code> 去查看命令的详细的帮助文档。在这里我只会讲一些平常日常比赛或者生活中我们可能会用的比较多的一些命令。</p>
<p><img src="https://img2018.cnblogs.com/blog/1100338/201810/1100338-20181014202945937-1677031749.png" alt="" loading="lazy"></p>
<p>例如,我们需要拉取一个 <code>docker</code> 镜像,我们可以用如下命令:</p>
<pre><code class="language-shell">docker pull image_name
</code></pre>
<p><code>image_name</code> 为镜像的名称,而如果我们想从 <code>Docker Hub</code> 上去下载某个镜像,我们可以使用以下命令:</p>
<pre><code class="language-shell">docker pull centos:latest
</code></pre>
<p><code>centos:lastest</code> 是镜像的名称, <code>Docker daemon</code> 发现本地没有我们需要的镜像,会自动去 <code>Docker Hub</code> 上去下载镜像,下载完成后,该镜像被默认保存到 <code>/var/lib/docker</code> 目录下。</p>
<p>接着我们如果想查看下主机下存在多少镜像,我们可以用如下命令:</p>
<pre><code class="language-shell">docker images
</code></pre>
<p>我们要想知道当前有哪些容器在运行,我们可以用如下命令:</p>
<pre><code class="language-shell">docker ps -a
</code></pre>
<p><code>-a</code> 是查看当前所有的容器,包括未运行的</p>
<p>我们该如何去对一个容器进行启动,重启和停止呢?我们可以用如下命令:</p>
<pre><code class="language-shell">docker start container_name/container_id
docker restart container_name/container_id
docker stop container_name/container_id
</code></pre>
<p>这个时候我们如果想进入到这个容器中,我们可以使用 <code>attach</code> 命令:</p>
<pre><code class="language-shell">docker attach container_name/container_id
</code></pre>
<p>那如果我们想运行这个容器中的镜像的话,并且调用镜像里面的 <code>bash</code> ,我们可以使用如下命令:</p>
<pre><code class="language-shell">docker run -t -i container_name/container_id /bin/bash
</code></pre>
<p>那如果这个时候,我们想删除指定镜像的话,由于 <code>image</code> 被某个 <code>container</code> 引用(拿来运行),如果不将这个引用的 <code>container</code> 销毁(删除),那 <code>image</code> 肯定是不能被删除。我们首先得先去停止这个容器:</p>
<pre><code class="language-shell">docker ps
docker stop container_name/container_id
</code></pre>
<p>然后我们用如下命令去删除这个容器:</p>
<pre><code class="language-shell">docker rm container_name/container_id
</code></pre>
<p>然后这个时候我们再去删除这个镜像:</p>
<pre><code>docker rmi image_name
</code></pre>
<p>此时,常用的 <code>Docker</code> 相关的命令就讲到这里为止了,我们在后续的文章中还会反复地提到这些命令。</p>
<h3 id="dockerfile是什么">Dockerfile是什么</h3>
<p>前面我们已经提到了 <code>Docker</code> 的一些基本概念。以 <code>CTF</code> 选手的角度来看,我们可以去使用 <code>Dockerfile</code> 定义镜像,依赖镜像来运行容器,可以去模拟出一个真实的漏洞场景。因此毫无疑问的说, <code>Dockerfile</code> 是镜像和容器的关键,并且 <code>Dockerfile</code> 还可以很轻易的去定义镜像内容,说了这么多,那么 <code>Dockerfile</code> 到底是个什么东西呢?</p>
<p><code>Dockerfile</code> 是自动构建 <code>docker</code> 镜像的配置文件, 用户可以使用 <code>Dockerfile</code> 快速创建自定义的镜像。<code>Dockerfile</code> 中的命令非常类似于 <code>linux</code> 下的 <code>shell</code> 命令。</p>
<p>我们可以通过下面这幅图来直观地感受下 Docker 镜像、容器和 Dockerfile 三者之间的关系。</p>
<p><img src="https://img2018.cnblogs.com/blog/1100338/201810/1100338-20181014202945505-1291953865.png" alt="" loading="lazy"></p>
<p>我们从上图中可以看到, <code>Dockerfile</code> 可以自定义镜像,通过 <code>Docker</code> 命令去运行镜像,从而达到启动容器的目的。</p>
<p><code>Dockerfile</code> 是由一行行命令语句组成,并且支持已 <code>#</code> 开头的注释行。</p>
<p>一般来说,我们可以将 <code>Dockerfile</code> 分为四个部分:</p>
<ul>
<li><strong>基础镜像(父镜像)信息指令 <code>FROM</code></strong></li>
<li><strong>维护者信息指令 <code>MAINTAINER</code></strong></li>
<li><strong>镜像操作指令 <code>RUN</code> 、 <code>EVN</code> 、 <code>ADD</code> 和 <code>WORKDIR</code> 等</strong></li>
<li><strong>容器启动指令 <code>CMD</code> 、 <code>ENTRYPOINT</code> 和 <code>USER</code> 等</strong></li>
</ul>
<p>下面是一段简单的Dockerfile的例子:</p>
<pre><code class="language-shell">FROM python:2.7
MAINTAINER Angel_Kitty <angelkitty6698@gmail.com>
COPY . /app
WORKDIR /app
RUN pip install -r requirements.txt
EXPOSE 5000
ENTRYPOINT ["python"]
CMD ["app.py"]
</code></pre>
<p>我们可以分析一下上面这个过程:</p>
<ul>
<li>1、从 <code>Docker Hub</code> 上 <code>pull</code> 下 <code>python 2.7</code> 的基础镜像</li>
<li>2、显示维护者的信息</li>
<li>3、<code>copy</code> 当前目录到容器中的 <code>/app</code> 目录下复制本地主机的 <code><src></code> ( <code>Dockerfile</code> 所在目录的相对路径)到容器里 <code><dest></code></li>
<li>4、指定工作路径为 <code>/app</code></li>
<li>5、安装依赖包</li>
<li>6、暴露 <code>5000</code> 端口</li>
<li>7、启动 <code>app</code></li>
</ul>
<p>这个例子是启动一个 <code>python flask app</code> 的 <code>Dockerfile</code> ( <code>flask</code> 是 <code>python</code> 的一个轻量的 <code>web</code> 框架),相信大家从这个例子中能够稍微理解了Dockfile的组成以及指令的编写过程。</p>
<h3 id="dockerfile常用的指令">Dockerfile常用的指令</h3>
<p>根据上面的例子,我们已经差不多知道了Dockerfile的组成以及指令的编写过程,我们再来理解一下这些常用命令就会得心应手了。</p>
<p>由于 <code>Dockerfile</code> 中所有的命令都是以下格式:<code>INSTRUCTION argument</code> ,指令 <code>(INSTRUCTION)</code> 不分大小写,但是推荐大写,和sql语句是不是很相似呢?下面我们正式来讲解一下这些指令集吧。</p>
<h4 id="from">FROM</h4>
<p><code>FROM</code> 是用于指定基础的 <code>images</code> ,一般格式为 <code>FROM <image></code> or <code>FORM <image>:<tag></code> ,所有的 <code>Dockerfile</code> 都用该以 <code>FROM</code> 开头,<code>FROM</code> 命令指明 <code>Dockerfile</code> 所创建的镜像文件以什么镜像为基础,<code>FROM</code> 以后的所有指令都会在 <code>FROM</code> 的基础上进行创建镜像。可以在同一个 <code>Dockerfile</code> 中多次使用 <code>FROM</code> 命令用于创建多个镜像。比如我们要指定 <code>python 2.7</code> 的基础镜像,我们可以像如下写法一样:</p>
<pre><code class="language-shell">FROM python:2.7
</code></pre>
<h4 id="maintainer">MAINTAINER</h4>
<p>MAINTAINER 是用于指定镜像创建者和联系方式,一般格式为 <code>MAINTAINER <name></code> 。这里我设置成我的 <code>ID</code> 和邮箱:</p>
<pre><code class="language-shell">MAINTAINER Angel_Kitty <angelkitty6698@gmail.com>
</code></pre>
<h4 id="copy">COPY</h4>
<p><code>COPY</code> 是用于复制本地主机的 <code><src></code> (为 Dockerfile 所在目录的相对路径)到容器中的 <code><dest></code>。</p>
<p>当使用本地目录为源目录时,推荐使用 <code>COPY</code> 。一般格式为 <code>COPY <src><dest></code> 。例如我们要拷贝当前目录到容器中的 <code>/app</code> 目录下,我们可以这样操作:</p>
<pre><code class="language-shell">COPY . /app
</code></pre>
<h4 id="workdir">WORKDIR</h4>
<p><code>WORKDIR</code> 用于配合 <code>RUN</code>,<code>CMD</code>,<code>ENTRYPOINT</code> 命令设置当前工作路径。可以设置多次,如果是相对路径,则相对前一个 <code>WORKDIR</code> 命令。默认路径为<code>/</code>。一般格式为 <code>WORKDIR /path/to/work/dir</code> 。例如我们设置<code> /app</code> 路径,我们可以进行如下操作:</p>
<pre><code class="language-shell">WORKDIR /app
</code></pre>
<h4 id="run">RUN</h4>
<p><code>RUN</code> 用于容器内部执行命令。每个 <code>RUN</code> 命令相当于在原有的镜像基础上添加了一个改动层,原有的镜像不会有变化。一般格式为 <code>RUN <command></code> 。例如我们要安装 <code>python</code> 依赖包,我们做法如下:</p>
<pre><code class="language-shell">RUN pip install -r requirements.txt
</code></pre>
<h4 id="expose">EXPOSE</h4>
<p><code>EXPOSE</code> 命令用来指定对外开放的端口。一般格式为 <code>EXPOSE <port> [<port>...]</code></p>
<p>例如上面那个例子,开放5000端口:</p>
<pre><code class="language-shell">EXPOSE 5000
</code></pre>
<h4 id="entrypoint">ENTRYPOINT</h4>
<p><code>ENTRYPOINT</code> 可以让你的容器表现得像一个可执行程序一样。一个 <code>Dockerfile</code> 中只能有一个 <code>ENTRYPOINT</code>,如果有多个,则最后一个生效。</p>
<p><code>ENTRYPOINT</code> 命令也有两种格式:</p>
<ul>
<li><code>ENTRYPOINT ["executable", "param1", "param2"]</code> :推荐使用的 <code>exec </code>形式</li>
<li><code>ENTRYPOINT command param1 param2</code> :<code>shell</code> 形式</li>
</ul>
<p>例如下面这个,我们要将 <code>python</code> 镜像变成可执行的程序,我们可以这样去做:</p>
<pre><code class="language-shell">ENTRYPOINT ["python"]
</code></pre>
<h4 id="cmd">CMD</h4>
<p><code>CMD</code> 命令用于启动容器时默认执行的命令,<code>CMD</code> 命令可以包含可执行文件,也可以不包含可执行文件。不包含可执行文件的情况下就要用 <code>ENTRYPOINT</code> 指定一个,然后 <code>CMD</code> 命令的参数就会作为<code>ENTRYPOINT</code>的参数。</p>
<p><code>CMD</code> 命令有三种格式:</p>
<ul>
<li><code>CMD ["executable","param1","param2"]</code>:推荐使用的 <code>exec</code> 形式。</li>
<li><code>CMD ["param1","param2"]</code>:无可执行程序形式</li>
<li><code>CMD command param1 param2</code>:shell 形式。</li>
</ul>
<p>一个 <code>Dockerfile</code> 中只能有一个<code>CMD</code>,如果有多个,则最后一个生效。而 <code>CMD</code> 的 <code>shell</code> 形式默认调用 <code>/bin/sh -c</code> 执行命令。</p>
<p><code>CMD</code> 命令会被 <code>Docker</code> 命令行传入的参数覆盖:<code>docker run busybox /bin/echo Hello</code> <code>Docker</code> 会把 <code>CMD</code> 里的命令覆盖。</p>
<p>例如我们要启动 <code>/app</code> ,我们可以用如下命令实现:</p>
<pre><code class="language-shell">CMD ["app.py"]
</code></pre>
<p>当然还有一些其他的命令,我们在用到的时候再去一一讲解一下。</p>
<h3 id="构建dockerfile">构建Dockerfile</h3>
<p>我们大体已经把Dockerfile的写法讲述完毕,我们可以自己动手写一个例子:</p>
<pre><code class="language-shell">mkdir static_web
cd static_web
touch Dockerfile
然后 vi Dockerfile开始编辑该文件
输入 i 开始编辑
以下是我们构建的Dockerfile内容
``````````
FROM nginx
MAINTAINER Angel_Kitty <angelkitty6698@gmail.com>
RUN echo '<h1>Hello, Docker!</h1>' > /usr/share/nginx/html/index.html
``````````
编辑完后 按 esc 退出编辑
然后:wq 写入 退出
</code></pre>
<p>我们在 <code>Dockerfile</code> 文件所在目录执行:</p>
<pre><code class="language-shell">docker build -t angelkitty/nginx_web:v1 .
</code></pre>
<p>我们解释一下, <code>-t</code> 是为新镜像设置仓库和名称,其中 <code>angelkitty</code> 为仓库名, <code>nginx_web</code> 为镜像名, <code>:v1</code> 为标签(不添加为默认 <code>latest</code> )</p>
<p>我们构建完成之后,使用 <code>docker images</code> 命令查看所有镜像,如果存在 <code>REPOSITORY</code> 为 <code>nginx</code> 和 <code>TAG</code> 是 <code>v1</code> 的信息,就表示构建成功。</p>
<p><img src="https://img2018.cnblogs.com/blog/1100338/201810/1100338-20181015082853781-920978466.png" alt="" loading="lazy"></p>
<p>接下来使用 <code>docker run</code> 命令来启动容器</p>
<pre><code class="language-shell">docker run --name nginx_web -d -p 8080:80 angelkitty/nginx_web:v1
</code></pre>
<p>这条命令会用 nginx 镜像启动一个容器,命名为 <code>nginx_web</code> ,并且映射了 8080 端口,这样我们可以用浏览器去访问这个 <code>nginx</code> 服务器:<code>http://localhost:8080/</code> 或者 http://本机的IP地址:8080/,页面返回信息:</p>
<p><img src="https://img2018.cnblogs.com/blog/1100338/201810/1100338-20181015082853396-674842375.png" alt="hello-docker" loading="lazy"></p>
<p>这样一个简单使用 <code>Dockerfile</code> 构建镜像,运行容器的示例就完成了!</p>
<h3 id="参考文献">参考文献</h3>
<ul>
<li>Docker — 从入门到实践</li>
<li>Docker 入门教程</li>
</ul>
</div>
<div id="MySignature" role="contentinfo">
<p id="PSignature" style="padding-top: 10px; padding-right: 10px; padding-bottom: 10px; padding-left: 60px; background: url("https://www.cnblogs.com/images/cnblogs_com/ECJTUACM-873284962/1318325/o_o_122329534672560.png") #e5f1f4 no-repeat 1% 50%; font-family: 微软雅黑; font-size: 12px; border: #e0e0e0 1px dashed"> <br>
作 者:<strong><span style="font-size: 12px; color: red">Angel_Kitty</span></strong>
<br>
出 处:https://www.cnblogs.com/ECJTUACM-873284962/
<br>
关于作者:阿里云ACE,目前主要研究方向是Web安全漏洞以及反序列化。如有问题或建议,请多多赐教!
<br>
版权声明:本文版权归作者和博客园共有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文链接。
<br>
特此声明:所有评论和私信都会在第一时间回复。也欢迎园子的大大们指正错误,共同进步。或者直接私信我
<br>
声援博主:如果您觉得文章对您有帮助,可以点击文章右下角<strong><span style="color: #ff0000; font-size: 18pt">【推荐】</span></strong>一下。您的鼓励是作者坚持原创和持续写作的最大动力!
<br>
</p>
<div id="mySponsorBox" style="padding: 30px; background: #ececec">
<p style="color: #4bd3d3; font-weight: bold; text-align: center">欢迎大家关注我的微信公众号IT老实人(IThonest),如果您觉得文章对您有很大的帮助,您可以考虑赏博主一杯咖啡以资鼓励,您的肯定将是我最大的动力。thx. </p>
<div style="border: silver 1px dashed; padding: 10px; height: 420">
<div style="float: left; width: 100%">
<img style="margin-left: auto; margin-right: auto; display: block" src="https://images.cnblogs.com/cnblogs_com/ECJTUACM-873284962/1318325/o_donate.png" width="100%">
</div>
</div>
</div>
<div id="mySponsorBox" style="padding: 30px; background: #ececec">
<p style="color: #4bd3d3; font-weight: bold; text-align: center">我的公众号是IT老实人(IThonest),一个有故事的公众号,欢迎大家来这里讨论,共同进步,不断学习才能不断进步。扫下面的二维码或者收藏下面的二维码关注吧(长按下面的二维码图片、并选择识别图中的二维码),个人QQ和微信的二维码也已给出,扫描下面👇的二维码一起来讨论吧!!!</p>
<div style="border: silver 1px dashed; padding: 10px; height: 420">
<div style="float: left; width: 100%">
<img style="margin-left: auto; margin-right: auto; display: block" src="https://images.cnblogs.com/cnblogs_com/ECJTUACM-873284962/1318325/o_chat.png" width="100%">
</div>
</div>
</div>
<div id="mySponsorBox" style="padding: 30px; background: #ececec">
<p style="color: #4bd3d3; font-weight: bold; text-align: center">欢迎大家关注我的Github,一些文章的备份和平常做的一些项目会存放在这里。</p>
<div class="github-card" data-github="AngelKitty" data-width="400" data-height="150" data-theme="default"></div>
</div><br><br>
来源:https://www.cnblogs.com/ECJTUACM-873284962/p/9789130.html
頁:
[1]