<?xml version="1.0" encoding="UTF-8"?>
<rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/"
    xmlns:atom="http://www.w3.org/2005/Atom" xmlns:media="http://search.yahoo.com/mrss/" version="2.0">
    <channel>
        
        <title>
            <![CDATA[ Docker - freeCodeCamp.org ]]>
        </title>
        <description>
            <![CDATA[ freeCodeCamp 是一个免费学习编程的开发者社区，涵盖 Python、HTML、CSS、React、Vue、BootStrap、JSON 教程等，还有活跃的技术论坛和丰富的社区活动，在你学习编程和找工作时为你提供建议和帮助。 ]]>
        </description>
        <link>https://www.freecodecamp.org/chinese/news/</link>
        <image>
            <url>https://cdn.freecodecamp.org/universal/favicons/favicon.png</url>
            <title>
                <![CDATA[ Docker - freeCodeCamp.org ]]>
            </title>
            <link>https://www.freecodecamp.org/chinese/news/</link>
        </image>
        <generator>Eleventy</generator>
        <lastBuildDate>Sun, 14 Jun 2026 09:53:39 +0000</lastBuildDate>
        <atom:link href="https://www.freecodecamp.org/chinese/news/tag/docker/rss.xml" rel="self" type="application/rss+xml" />
        <ttl>60</ttl>
        
            <item>
                <title>
                    <![CDATA[ 使用 Docker 搭建私有软件仓库 ]]>
                </title>
                <description>
                    <![CDATA[ 我写这篇文章的起因是 npm 安装软件包网速慢，如果使用淘宝的镜像源 [https://registry.npmmirror.com/] ，一些非主流的包，更新不及时，还出现下载失败。如果使用官方源 [https://registry.npmjs.org]，网速就太慢了。 我将在这里分享自己的实践经验。 选型 verdaccio 因为考虑单纯为 npm 做本地镜像源，考虑使用 verdaccio [https://verdaccio.org/] ，想做成一个后台运行的服务，可以开机自启动，就考虑到 PM2 [https://github.com/Unitech/pm2]，但是有个问题，如果想 pm2 startup 生效，就必须以 root 用户运行这个命令。对系统侵入性太大了。 就考虑以 docker 的形式运行 verdaccio。 第一版 docker-compose.yml 文件： version: '3.1' services:   verdaccio:     image: verdaccio/verdaccio     restart: always    ]]>
                </description>
                <link>https://www.freecodecamp.org/chinese/news/use-docker-to-build-private-software-repositories/</link>
                <guid isPermaLink="false">641c4da65e1a4c068f38d69e</guid>
                
                    <category>
                        <![CDATA[ Docker ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ luojiyin ]]>
                </dc:creator>
                <pubDate>Wed, 30 Aug 2023 02:12:00 +0000</pubDate>
                <media:content url="https://chinese.freecodecamp.org/news/content/images/2023/03/1_AUiK5PwnsPG_xaT9jcVoSA-2.jpeg" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>我写这篇文章的起因是 npm 安装软件包网速慢，如果使用淘宝的<a href="https://registry.npmmirror.com/">镜像源</a>，一些非主流的包，更新不及时，还出现下载失败。如果使用<a href="https://registry.npmjs.org">官方源</a>，网速就太慢了。</p>
<p>我将在这里分享自己的实践经验。</p>
<h2 id="">选型</h2>
<h3 id="verdaccio">verdaccio</h3>
<p>因为考虑单纯为 npm 做本地镜像源，考虑使用 <a href="https://verdaccio.org/">verdaccio</a>，想做成一个后台运行的服务，可以开机自启动，就考虑到 <a href="https://github.com/Unitech/pm2">PM2</a>，但是有个问题，如果想 <code>pm2 startup</code> 生效，就必须以 root 用户运行这个命令。对系统侵入性太大了。</p>
<p>就考虑以 docker 的形式运行 verdaccio。</p>
<p>第一版 docker-compose.yml 文件：</p>
<pre><code class="language-yaml">version: '3.1'

services:
  verdaccio:
    image: verdaccio/verdaccio
    restart: always
    container_name: 'verdaccio'
    ports:
      - '4873:4873'
    volumes:
      - './storage:/verdaccio/storage'
      - './config:/verdaccio/conf'
      - './plugins:/verdaccio/plugins'
</code></pre>
<p>创建 config 文件夹，在里面创建 config.yaml 文件：</p>
<pre><code class="language-shell">mkdir config
cd config
touch config.yaml
</code></pre>
<p>config.yaml 文件内容：</p>
<pre><code class="language-yaml">#
# This is the default config file. It allows all users to do anything,
# so don't use it on production systems.
#
# Look here for more config file examples:
# https://github.com/verdaccio/verdaccio/tree/master/conf
#

# path to a directory with all packages
storage: /verdaccio/storage
# path to a directory with plugins to include
plugins: /verdaccio/plugins
# print logs
# logs: ./logs

web:
  title: Verdaccio
  # comment out to disable gravatar support
  # gravatar: false
  # by default packages are ordercer ascendant (asc|desc)
  # sort_packages: asc
  # convert your UI to the dark side
  # darkMode: true
  #  HTML tags injected after manifest &lt;scripts/&gt;
  # scriptsBodyAfter:
  #    - '&lt;script type="text/javascript" src="https://my.company.com/customJS.min.js"&gt;&lt;/script&gt;'
  #  HTML tags injected before ends &lt;/head&gt;
  #  metaScripts:
  #    - '&lt;script type="text/javascript" src="https://code.jquery.com/jquery-3.5.1.slim.min.js"&gt;&lt;/script&gt;'
  #    - '&lt;script type="text/javascript" src="https://browser.sentry-cdn.com/5.15.5/bundle.min.js"&gt;&lt;/script&gt;'
  #    - '&lt;meta name="robots" content="noindex" /&gt;'
  #  HTML tags injected first child at &lt;body/&gt;
  #  bodyBefore:
  #    - '&lt;div id="myId"&gt;html before webpack scripts&lt;/div&gt;'
  #  Public path for template manifest scripts (only manifest)
  #  publicPath: http://somedomain.org/
# translate your registry, api i18n not available yet
# i18n:
# list of the available translations https://github.com/verdaccio/ui/tree/master/i18n/translations
#   web: en-US

auth:
  htpasswd:
    file: ./htpasswd
    # Maximum amount of users allowed to register, defaults to "+inf".
    # You can set this to -1 to disable registration.
    # max_users: 1000

# a list of other known repositories we can talk to
uplinks:
  npmjs:
    url: https://registry.npmjs.org/
  taobao:
    url: https://registry.npmmirror.com/
  tencent:
    url: https://mirrors.cloud.tencent.com/npm/
packages:
  '@*/*':
    # scoped packages
    access: $all
    publish: $authenticated
    unpublish: $authenticated
    proxy: npmjs

  '**':
    # allow all users (including non-authenticated users) to read and
    # publish all packages
    #
    # you can specify usernames/groupnames (depending on your auth plugin)
    # and three keywords: "$all", "$anonymous", "$authenticated"
    access: $all

    # allow all known users to publish/publish packages
    # (anyone can register by default, remember?)
    publish: $authenticated
    unpublish: $authenticated

    # if package is not available locally, proxy requests to 'npmjs' registry
    proxy: npmjs

server:
  # deprecated
  keepAliveTimeout: 60
#  rateLimit:
#    windowMs: 1000
#    max: 10000

middlewares:
  audit:
    enabled: true
listen: 0.0.0.0:4873

# log settings
logs:
  # Logger as STDOUT
  { type: stdout, format: pretty, level: http }
  # Logger as STDOUT as JSON
  # { type: stdout, format: json, level: http }
  # Logger as STDOUT as JSON
  # { type: stdout, format: pretty-timestamped, level: http }
  # Logger as STDOUT as custom prettifier
  # { type: stdout, plugin: { dest: '@verdaccio/logger-prettify' : options: { foo: 1, bar: 2}}, level: http }
  # Logger as file
  # { type: file, path: verdaccio.log, level: http}
  # FIXME: this should be documented
  # More info about log rotation https://github.com/pinojs/pino/blob/master/docs/help.md#log-rotation

# This affect the web and api (not developed yet)
i18n:
  web: en-US
</code></pre>
<h4 id="">意外</h4>
<p>启动容器</p>
<pre><code class="language-shell">docker-compose up 
</code></pre>
<p>报错 <code> permission denied, mkdir</code></p>
<p><a href="https://github.com/verdaccio/verdaccio/issues/3665">具体见这里</a>。</p>
<p>第二版</p>
<pre><code class="language-yaml">version: '3.1'

services:
  verdaccio:
    image: verdaccio/verdaccio
    restart: always
    user: 1000:1000
    container_name: 'verdaccio'
    ports:
      - '4873:4873'
    volumes:
      - './storage:/verdaccio/storage'
      - './config:/verdaccio/conf'
      - './plugins:/verdaccio/plugins'
</code></pre>
<p>通过修改文件夹的所有者</p>
<pre><code class="language-shell">id 
uid=1000(luo) gid=1000(luo) 组=1000(luo),4(adm),24(cdrom),27(sudo)

sudo  chown -R  1000:100  storage/  config/  plugins/
</code></pre>
<p>就解决问题了，user 的设置要你自己 Linux 服务器上一致，通过 <code>id</code> 命令获得，格式为  <code>user: uid: gid</code>。</p>
<p>启动</p>
<pre><code class="language-shell">docker-compose up -d 
</code></pre>
<p>在网页浏览器里输入 <code>http://ip:4873</code>，然后再终端输入，进行设置，使用本地的软件源</p>
<pre><code class="language-shell">npm set registry http://ip:4873/
</code></pre>
<p><img src="https://chinese.freecodecamp.org/news/content/images/2023/03/----_20230323213523.png" alt="----_20230323213523" width="1304" height="760" loading="lazy"></p>
<h3 id="nexus3">Nexus 3</h3>
<p>选一个通用型软件镜像服务（Nexus 3），大部分的开发语言都支持，一次投入，多次收益，如图：</p>
<p><img src="https://chinese.freecodecamp.org/news/content/images/2023/03/nexus3.png" alt="nexus3" width="2561" height="1946" loading="lazy"></p>
<p>docker-compose.yml 文件</p>
<pre><code class="language-yaml">version: "3.6"

services:
  nexus:
    image: sonatype/nexus3:3.49.0
    restart: always
    environment:
      - INSTALL4J_ADD_VM_PARAMS=-Xms2g -Xmx2g -XX:MaxDirectMemorySize=2g -Djava.util.prefs.userRoot=/nexus-data/javaprefs -Duser.time    volumes:
      - ./nexus-data:/nexus-data
    ports:
      - "8081:8081"
</code></pre>
<p>Java 应用要限制一下内存，避免把宿主机的内存全吃掉，可根据实际情况调整。</p>
<p>启动</p>
<pre><code class="language-shell">docker-compose up -d 
</code></pre>
<p>还是遇到权限问题</p>
<pre><code class="language-shell">mkdir: cannot create directory '../sonatype-work/nexus3/log': Permission denied

mkdir: cannot create directory '../sonatype-work/nexus3/tmp': Permission denied
</code></pre>
<p>解决办法：<a href="https://soulteary.com/2018/10/08/how-to-migrate-nexus.html">参考资料</a></p>
<pre><code class="language-shell">sudo chown -R 200 ~/dockerVolume/nexus
</code></pre>
<p>重新启动</p>
<pre><code class="language-shell">docker-compose up -d
</code></pre>
<p>获得 admin 的登录密码</p>
<pre><code class="language-shell">
# 在启动应用的目录中执行

cat nexus-data/admin.password

# 或者直接使用 Docker CLI 执行容器命令
docker exec -it nexus.lab.io cat /nexus-data/admin.password
</code></pre>
<p>通过网页浏览器输入 <code>http://ip:8081</code> 访问管理后台。在输入了正确的初始账号和密码后，新版软件会人性化的引导我们设置新密码，以及设置是否允许匿名用户使用。</p>
<p>如果是个人使用，或者团队在内网使用，可以勾选“允许匿名访问”。</p>
<p>然后建个 npm(hosted) 和 npm(proxy)，组成一个 npm(grounp)，<br>
具体教程可以看 <a href="https://juejin.cn/post/6911642325559017480">这个</a>，但不要像它里面开放多个端口，没必要。</p>
<p>设置 npm 使用本地软件源（根据你的服务器的实际 IP 或者域名进行修改）</p>
<pre><code class="language-shell">npm set registry  http://192.168.2.114:8081/repository/npm-group/
</code></pre>
<h2 id="">总结</h2>
<p>docker 存储方面的权限设置是个问题，解决办法：</p>
<ol>
<li>自己构建 docker 镜像，从根源上解决问题，这难度比较大，要自己不断学习尝试。</li>
<li>多查找网络资料，通过设置解决。</li>
</ol>
<p>docker 部署方便，避免对系统的入侵，具有良好的隔离性，并且多平台支持，但是在权限安全方面还有很多要注意的。</p>
<p>如果你对这篇文章有任何疑问，或者如果你想和我探讨更多技术内容，欢迎和我联系。我的邮箱是：luojiyin@hotmail.com。</p>
<!--kg-card-end: markdown--> ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ Docker Compose 是什么？通过示例学习如何使用它 ]]>
                </title>
                <description>
                    <![CDATA[ Docker 帮助你在你的机器上快速建立一个开发环境。完成整个过程只需要几分钟时间。 但是，让我们假设你被分配到一个项目上，该项目需要至少 10 个不同的服务处于运行状态来运行你的项目。例如，假设你的项目需要 Java 8、Node 14、MySQL、MongoDB、Ruby on rails、RabbitMQ 和其他。 在这种情况下，你必须从 Docker 中单独提取所有这些镜像，并在其容器中启动所有这些镜像。在某些时候，一个进程可能依赖于另一个进程来运行。所以，你必须给它们排序。 如果这是一个一次性的过程就好了。但是，不仅仅是一次——每天、每次你开始在你的项目上工作时——你都必须启动所有这些服务。 这是一个乏味的过程，对吗？ 为了克服这个问题，Docker 引入了一个叫做多容器（Docker Compose）的概念。在学习 Docker Compose 之前，让我们快速了解一下如何在 Docker 中启动数据库主机。 在本教程的例子部分，我们将启动一个 NodeJS 容器和 MongoDB 容器。在一开始就学习 MongoDB，会让你在我们转向 Docker Compo ]]>
                </description>
                <link>https://www.freecodecamp.org/chinese/news/what-is-docker-compose-how-to-use-it/</link>
                <guid isPermaLink="false">648e75303c820a06f4b65c12</guid>
                
                    <category>
                        <![CDATA[ Docker ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ Miya Liu ]]>
                </dc:creator>
                <pubDate>Mon, 12 Jun 2023 03:40:00 +0000</pubDate>
                <media:content url="https://chinese.freecodecamp.org/news/content/images/2023/06/What-is-Docker-compose-1.png" medium="image" />
                <content:encoded>
                    <![CDATA[ <p data-test-label="translation-intro">
        <strong>原文：</strong> <a href="https://www.freecodecamp.org/news/what-is-docker-compose-how-to-use-it/" target="_blank" rel="noopener noreferrer" data-test-label="original-article-link">What is Docker Compose? How to Use it with an Example</a>
      </p><p>Docker 帮助你在你的机器上快速建立一个开发环境。完成整个过程只需要几分钟时间。</p><p>但是，让我们假设你被分配到一个项目上，该项目需要至少 10 个不同的服务处于运行状态来运行你的项目。例如，假设你的项目需要 Java 8、Node 14、MySQL、MongoDB、Ruby on rails、RabbitMQ 和其他。</p><p>在这种情况下，你必须从 Docker 中单独提取所有这些镜像，并在其容器中启动所有这些镜像。在某些时候，一个进程可能依赖于另一个进程来运行。所以，你必须给它们排序。</p><p>如果这是一个一次性的过程就好了。但是，不仅仅是一次——每天、每次你开始在你的项目上工作时——你都必须启动所有这些服务。</p><p>这是一个乏味的过程，对吗？</p><p>为了克服这个问题，Docker 引入了一个叫做多容器（Docker Compose）的概念。在学习 Docker Compose 之前，让我们快速了解一下如何在 Docker 中启动数据库主机。</p><p>在本教程的例子部分，我们将启动一个 NodeJS 容器和 MongoDB 容器。在一开始就学习 MongoDB，会让你在我们转向 Docker Compose 时有一个很好的理解。</p><p>我们将把本教程分成两个部分：</p><ul><li>如何使用 Docker 作为数据库主机（MongoDB）？</li><li>通过一个例子了解 Docker Compose 如何工作（NodeJS 和 MongoDB）</li></ul><h2 id="-docker-">如何使用 Docker 作为数据库主机</h2><p>如果你有过后端开发的经验，你可能有机会处理多个数据库。例如，像 MySQL/Postgres 这样的数据库用来处理关系型数据，Cassandra/MongoDB 用来处理非结构化数据。</p><p>想知道一个秘密吗？你可以在不在本地机器上安装数据库的情况下进行后端开发工作。是的，你可以使用 Docker 作为数据库主机。它在特定的镜像文件中默认拥有所有的依赖项。</p><h2 id="-docker--1">为什么我们需要 Docker 数据库</h2><p>Docker 帮助我们在不同的平台和环境中保持一致的版本。假设你的团队中有一群人在 MongoDB 5.0 版本上工作。如果一个新成员加入你的团队，他们需要用精确的配置手动设置相同的版本。如果他们安装了最新版本的 MongoDB（6.0）呢？这将导致一些冲突。如果它蔓延到其他人的设备上，这将是一场噩梦。</p><p>为了解决这个问题，你可以在 Docker 中使用带有自定义配置的 MongoDB，并将 MongoDB 镜像推送到 Docker Hub。如果有新人加入，他们可以拉取镜像并开始实施，而无需任何手动配置。</p><p>让我们来看看在 Docker 中使用数据库的优势。</p><ul><li>通过使用这种实现方式，我们可以确保团队中的每个人都使用准确的运行时和配置，而无需任何外部资源。</li><li>它非常容易设置，我们可以使用 Docker 桌面端快速启动/停止服务器</li></ul><h2 id="-docker-mongodb">如何使用 Docker 设置 MongoDB</h2><p>如果你不熟悉 Docker Hub，这里有一个简短的介绍。Docker hub 是一个平台，你可以在这里找到并分享公开或私有的 Docker 镜像。它与 GitHub / GitLab 非常相似。简而言之，它是一个 Docker 镜像的存储库。</p><p>第一步，从 Docker Hub 拉取官方的 MongoDB 镜像。</p><figure class="kg-card kg-image-card kg-width-wide kg-card-hascaption"><img src="https://www.freecodecamp.org/news/content/images/2023/04/image-43.png" class="kg-image" alt="image-43" width="600" height="400" loading="lazy"><figcaption>Docker Hub 中的 MongoDB 镜像</figcaption></figure><figure class="kg-card kg-code-card"><pre><code class="language-bash">docker pull mongo:latest</code></pre><figcaption>从 Docker Hub 中拉取 MongoDB 镜像的控制台命令</figcaption></figure><figure class="kg-card kg-image-card kg-width-wide kg-card-hascaption"><img src="https://www.freecodecamp.org/news/content/images/2023/04/image-44.png" class="kg-image" alt="image-44" width="600" height="400" loading="lazy"><figcaption>从 Docker Hub 拉取 Mongo 镜像的输出示例</figcaption></figure><p>当你完成了 Mongo 镜像的拉取，打开你的 Docker 桌面端，你就能在那里看到它。</p><figure class="kg-card kg-image-card kg-width-wide kg-card-hascaption"><img src="https://www.freecodecamp.org/news/content/images/2023/04/image-45.png" class="kg-image" alt="image-45" width="600" height="400" loading="lazy"><figcaption>Docker 桌面端显示 Mongo 镜像</figcaption></figure><p>Let's run our MongoDB image using the <code>docker run</code> command.</p><figure class="kg-card kg-code-card"><pre><code class="language-bash">docker run -d -p 27017:27017 --name mongo-server-local mongo: latest</code></pre><figcaption>在 Docker 中运行 MongoDB 的命令</figcaption></figure><figure class="kg-card kg-image-card kg-width-wide kg-card-hascaption"><img src="https://www.freecodecamp.org/news/content/images/2023/04/image-46.png" class="kg-image" alt="image-46" width="600" height="400" loading="lazy"><figcaption>在 Docker 中运行 MongoDB 的输出示例</figcaption></figure><p>我们已经成功运行了 Docker 镜像。现在我们可以看到容器在 Docker 桌面端运行。</p><figure class="kg-card kg-image-card kg-width-wide kg-card-hascaption"><img src="https://www.freecodecamp.org/news/content/images/2023/04/image-48.png" class="kg-image" alt="image-48" width="600" height="400" loading="lazy"><figcaption>Mongo 容器在 Docker 桌面端运行</figcaption></figure><p>所以，MongoDB 服务器正在你的机器上运行。让我们在浏览器中确认这一点。在你的浏览器上进入 <a href="http://localhost:27017/" rel="noopener">http://localhost:27017</a>，你应该能够看到下面截图中所示的信息：</p><figure class="kg-card kg-image-card kg-width-wide kg-card-hascaption"><img src="https://www.freecodecamp.org/news/content/images/2023/04/image-49.png" class="kg-image" alt="image-49" width="600" height="400" loading="lazy"><figcaption>使用 Docker 运行 MongoDB 服务器时的输出示例</figcaption></figure><p>有意思吧？</p><p>我们可以随时使用 Docker 停止/启动 MongoDB 服务器。</p><h3 id="-">重要提示</h3><ul><li>不建议在生产中使用 Docker 作为数据库。</li><li>不要在大规模应用中使用 Docker 数据库。</li></ul><h2 id="-docker-compose">什么是 docker-compose</h2><p>让我们再来看看 docker-compose。</p><p>Docker Compose 是一个工具，你可以用来定义和分享多容器应用程序。这意味着你可以使用一个单一的资源来运行一个有多个容器的项目。</p><p>例如，假设你正在用 NodeJS 和 MongoDB 一起构建一个项目。你可以创建一个单一的镜像，将两个容器作为一个服务来启动——你不需要分别启动每个容器。</p><p>有意思吧？这就解决了我在本文一开始就提出的问题。</p><p>为了实现这个目标，我们需要定义一个 <code>docker-compose.yml</code>。</p><h3 id="docker-compose-yml-"><strong>docker-compose.yml 文件</strong></h3><p>compose 文件是一个 YML 文件，定义了 Docker 容器的服务、网络和卷。有几个版本的 compose 文件格式可用——1、2、2.x 和 3.x。</p><p>在进一步开展工作之前，这里有一个 <a href="https://docs.docker.com/compose/">Docker Compose 团队</a>给我们的重要说明。</p><blockquote>从 2023 年 6 月底开始，Compose V1 将不再被支持，并将从所有 Docker 桌面版本中移除。</blockquote><p>我们在本文中使用的是版本 3。</p><figure class="kg-card kg-code-card"><pre><code class="language-docker-compose">version: '3'
services:
  app:
    image: node:latest
    container_name: app_main
    restart: always
    command: sh -c "yarn install &amp;&amp; yarn start"
    ports:
      - 8000:8000
    working_dir: /app
    volumes:
      - ./:/app
    environment:
      MYSQL_HOST: localhost
      MYSQL_USER: root
      MYSQL_PASSWORD: 
      MYSQL_DB: test
  mongo:
    image: mongo
    container_name: app_mongo
    restart: always
    ports:
      - 27017:27017
    volumes:
      - ~/mongo:/data/db
volumes:
  mongodb:</code></pre><figcaption>使用 docker compose 运行多个容器的命令</figcaption></figure><p>让我们来拆解一下上面的代码，并逐条理解：</p><ul><li><code>version</code> 指的是 docker-compose 的版本（最新的是 3）</li><li><code>services</code> 定义了我们需要运行的服务</li><li><code>app</code> 是你的一个容器的自定义名称</li><li><code>image</code> 指的是我们要拉取的镜像，这里我们使用的是 <code>node:latest</code> 和 <code>mongo</code></li><li><code>container_name</code> 是每个容器的名称</li><li><code>restart</code> 启动/重启一个服务容器</li><li><code>port</code> 定义了运行该容器的自定义端口</li><li><code>working_dir</code> 是服务容器的当前工作目录</li><li><code>environment</code> 定义了环境变量，如 DB 凭证等</li><li><code>command</code> 是运行该服务的命令</li></ul><h3 id="--1">如何运行多容器</h3><p>我们需要使用 docker build 来构建我们的多容器。</p><figure class="kg-card kg-code-card"><pre><code class="language-bash">docker compose build</code></pre><figcaption>构建 docker-compose.yml 的命令</figcaption></figure><p>成功构建后，我们可以使用 <code>up</code> 命令运行容器。</p><figure class="kg-card kg-code-card"><pre><code class="language-bash">docker compose up</code></pre><figcaption>使用 docker-compose 运行多容器的命令</figcaption></figure><p>如果你想在后台运行容器，只需使用 <code>-d</code> 标志（detach）。</p><figure class="kg-card kg-code-card"><pre><code class="language-bash">docker compose up -d</code></pre><figcaption>使用 docker-compose 在后台运行多个容器的命令</figcaption></figure><figure class="kg-card kg-image-card kg-width-wide kg-card-hascaption"><img src="https://www.freecodecamp.org/news/content/images/2023/04/image-50.png" class="kg-image" alt="image-50" width="600" height="400" loading="lazy"><figcaption>使用 docker-compose 在后台运行多个容器的输出示例</figcaption></figure><p>好的，我们可以开始了。容器已经启动并运行。让我们检查一下容器列表。</p><figure class="kg-card kg-code-card"><pre><code class="language-bash">docker compose ps</code></pre><figcaption>列出正在运行的容器服务的命令</figcaption></figure><figure class="kg-card kg-image-card kg-width-wide kg-card-hascaption"><img src="https://www.freecodecamp.org/news/content/images/2023/04/image-51.png" class="kg-image" alt="image-51" width="600" height="400" loading="lazy"><figcaption>列出正在运行的容器服务的输出示例</figcaption></figure><p>很棒，我们可以看到有两个容器在同时运行。</p><figure class="kg-card kg-image-card kg-width-wide kg-card-hascaption"><img src="https://www.freecodecamp.org/news/content/images/2023/04/image-54.png" class="kg-image" alt="image-54" width="600" height="400" loading="lazy"><figcaption>使用 docker 运行 nodejs 服务的输出示例</figcaption></figure><figure class="kg-card kg-image-card kg-width-wide kg-card-hascaption"><img src="https://www.freecodecamp.org/news/content/images/2023/04/image-55.png" class="kg-image" alt="image-55" width="600" height="400" loading="lazy"><figcaption>使用 docker 运行 mongodb 服务的输出示例</figcaption></figure><p>为了查看你的 MongoDB 中的数据，你必须安装 MongoDB Compass。</p><p>下面是它的屏幕截图。</p><figure class="kg-card kg-image-card kg-width-wide kg-card-hascaption"><img src="https://www.freecodecamp.org/news/content/images/2023/04/image-56.png" class="kg-image" alt="image-56" width="600" height="400" loading="lazy"><figcaption>mongodb compass 中的 MongoDB 服务器视图</figcaption></figure><h2 id="--2"><strong>总结</strong></h2><p>在这篇文章中，你已经通过一个例子了解了 Docker Compose 是如何工作的。使用多个容器，你可以运行任何类型的服务，如 RabbitMQ 或 Apache Kafka，并在一个单一的服务源中运行。希望你喜欢阅读这篇文章。</p><p>如果你想了解更多关于 Docker 的信息，请在我的网站（<a href="https://5minslearn.gogosoon.com">https://5minslearn.gogosoon.com</a>）上订阅我的文章，那里有我所有博客的综合列表。</p> ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ 什么是 Docker？通过实例学习如何使用容器 ]]>
                </title>
                <description>
                    <![CDATA[ 原文：What is Docker? Learn How to Use Containers – Explained with Examples [https://www.freecodecamp.org/news/what-is-docker-learn-how-to-use-containers-with-examples/] ，作者：Sebastian Sigl [https://www.freecodecamp.org/news/author/sesigl/] 容器是当今软件开发的一个重要工具。当你利用容器时，在任何环境中运行应用程序都变得很容易。 运行容器的最流行技术是 Docker [https://www.docker.com/]，它可以在任何操作系统上运行。 在这篇博文中，你将学习 Docker 最基本的 3 个用例：  * 使用 Docker 在本地运行一个数据库  * 使用 Docker 化的数据库运行自动测试  * 使用 Docker 在本地和生产中运行你的应用程序 你将使用一个 Java Spring Boot [https://spring.io/p ]]>
                </description>
                <link>https://www.freecodecamp.org/chinese/news/what-is-docker-learn-how-to-use-containers-with-examples/</link>
                <guid isPermaLink="false">6266687899ec7406219e7a06</guid>
                
                    <category>
                        <![CDATA[ Docker ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ luojiyin ]]>
                </dc:creator>
                <pubDate>Mon, 25 Apr 2022 03:20:00 +0000</pubDate>
                <media:content url="https://chinese.freecodecamp.org/news/content/images/2022/04/how-to-use-docker-containers.png" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>原文：<a href="https://www.freecodecamp.org/news/what-is-docker-learn-how-to-use-containers-with-examples/">What is Docker? Learn How to Use Containers – Explained with Examples</a>，作者：<a href="https://www.freecodecamp.org/news/author/sesigl/">Sebastian Sigl</a></p><!--kg-card-begin: markdown--><p>容器是当今软件开发的一个重要工具。当你利用容器时，在任何环境中运行应用程序都变得很容易。</p>
<p>运行容器的最流行技术是 <a href="https://www.docker.com/">Docker</a>，它可以在任何操作系统上运行。</p>
<p>在这篇博文中，你将学习 Docker 最基本的 3 个用例：</p>
<ul>
<li>使用 Docker 在本地运行一个数据库</li>
<li>使用 Docker 化的数据库运行自动测试</li>
<li>使用 Docker 在本地和生产中运行你的应用程序</li>
</ul>
<p>你将使用一个 Java <a href="https://spring.io/projects/spring-boot">Spring Boot</a> 应用程序，但所有学习内容都适用于你选择的其他任何编程语言。</p>
<p>为了运行所有的例子，你需要：</p>
<ul>
<li><a href="https://docs.docker.com/engine/install/">安装 Docker</a></li>
<li><a href="https://www.java.com/de/download/">安装 Java</a></li>
</ul>
<h2 id="docker">使用 Docker 为运行应用程序提供独立环境</h2>
<blockquote>
<p>Docker 减少了重复的、无意义的配置任务，并在整个开发生命周期中用于快速、简单和可移植的应用开发——桌面和云。（源自：<a href="https://www.docker.com/use-cases/">https://www.docker.com/use-cases/</a> ）</p>
</blockquote>
<p>Docker 的超能力的核心是利用所谓的 <a href="https://en.wikipedia.org/wiki/Cgroups">cgroups</a> 来创建轻量级的、隔离的、可移植的和高性能的环境，你可以在几秒钟内启动。</p>
<p>让我们来看看你如何使用 Docker 来提高生产力。</p>
<h2 id="database">Database 容器</h2>
<p>使用 Docker，你可以在几秒钟内启动许多类型的数据库。这很容易，而且不会因为你运行数据库所需的其他要求而污染你的本地系统。一切都与 Docker 容器打包在一起。</p>
<p>通过在 <a href="https://hub.docker.com/">hub.docker.com</a> 搜索，你可以为许多数据库找到现成的容器。</p>
<p>使用<code>docker run</code>命令，你可以启动一个 <a href="https://hub.docker.com/_/mysql/">MySQL 的容器</a>。</p>
<pre><code class="language-sh">docker run --rm -v "$PWD/data":/var/lib/mysql --name mysql -e MYSQL_ROOT_PASSWORD=admin-password -e MYSQL_DATABASE=my-database -p 3306:3306 mysql:8.0.28-debian
</code></pre>
<p>该命令使用了运行 Docker 容器的高级功能：</p>
<ul>
<li><code>-v "$PWD/data"</code> 映射了你的本地目录<code>./data</code>到 Docker 容器，这使你能够启动Docker容器而不丢失你的数据</li>
<li><code>-p 3306:3306</code> 映射容器的 <code>3306</code> 端口到我们的机器的 <code>3306</code> 端口上，以便其他应用程序可以使用它</li>
<li><code>-e MYSQL_DATABASE=my-database</code> 设置一个环境变量，自动创建一个名为<code>my-database</code>的新数据库</li>
<li><code>-e MYSQL_ROOT_PASSWORD=admin-password</code> 设置一个环境变量来设置管理密码</li>
<li><code>--rm</code> 停止时移除容器</li>
</ul>
<p>这些环境变量和更多的环境变量都记录在 <a href="https://hub.docker.com/_/mysql/?tab=description">Docker镜像的页面</a>。</p>
<h3 id="">如何使用数据库容器进行开发</h3>
<p>你将使用一个流行的技术栈来构建，一个基于<a href="https://www.w3schools.com/java/java_intro.asp">Java</a> 和<a href="https://spring.io/projects/spring-boot">Spring Boot</a> 的 Web 应用程序。为了专注于 Docker 部分，你可以从官方的 <a href="https://spring.io/guides/gs/accessing-data-rest/">用Rest指南访问JPA数据</a> 克隆一个简单的演示应用程序。</p>
<pre><code class="language-sh">#下载示例应用程序
git clone https://github.com/spring-guides/gs-accessing-data-rest.git

#打开最终的应用程序文件夹
cd complete
</code></pre>
<p>该应用程序自带一个内存数据库，这对生产来说没有价值，因为它不允许多个服务访问和修改（mutate）一个数据库。一个<a href="https://www.mysql.com/">MySQL</a>数据库更适合于将你的应用程序扩展到更多的读和写。</p>
<p>因此，将 MySQL 驱动添加到你的 <code>pom.xml</code> 文件：</p>
<pre><code class="language-xml">       &lt;!-- Disable in memory database --&gt;
       &lt;!--
       &lt;dependency&gt;
           &lt;groupId&gt;com.h2database&lt;/groupId&gt;
           &lt;artifactId&gt;h2&lt;/artifactId&gt;
           &lt;scope&gt;runtime&lt;/scope&gt;
       &lt;/dependency&gt;
       --&gt;
 
       &lt;!-- MySQL driver --&gt;
       &lt;dependency&gt;
           &lt;groupId&gt;mysql&lt;/groupId&gt;
           &lt;artifactId&gt;mysql-connector-java&lt;/artifactId&gt;
           &lt;scope&gt;runtime&lt;/scope&gt;
       &lt;/dependency&gt;
</code></pre>
<p>现在，你需要通过添加配置文件 <code>src/main/resources/application.properties</code> 来添加连接到数据库的配置。</p>
<pre><code class="language-properties">#数据库配置
spring.datasource.url=jdbc:mysql://localhost:3306/my-database
spring.datasource.username=root
spring.datasource.password=admin-password
 
#自动创建表格和数据库
spring.jpa.hibernate.ddl-auto=update
</code></pre>
<p>你现在可以启动应用程序并调用现有的端点（endpoints）：</p>
<pre><code class="language-sh">#获取所有人
curl http://localhost:8080/people

#添加一个人
curl -i -H "Content-Type:application/json" -d '{"firstName": "Frodo", "lastName": "Baggins"}' http://localhost:8080/people

#再次获取所有人，现在返回创建的人
curl http://localhost:8080/people
</code></pre>
<p>你成功地使用了你的初级应用程序，它在你的数据库中写入和读取数据。使用 MySQL Docker 数据库可以让你在几秒钟内建立一个强大的数据库，而且你可以从任何应用程序中使用它。</p>
<h3 id="">如何使用数据库容器进行集成测试</h3>
<p>该应用程序已经有数据库的相关测试。但是，因为你用一个实际的 MySQL 数据库替换了你的内存数据库，如果你停止你的数据库，测试就不会成功运行。</p>
<pre><code class="language-sh"># 停止数据库
docker rm -f mysql

# 运行测试
./mvnw clean test

... skipped output ...
[ERROR] Tests run: 7, Failures: 0, Errors: 7, Skipped: 0
... skipped output ...
</code></pre>
<p>为了快速启动和停止运行测试的容器，有一个方便的工具叫 <a href="https://github.com/testcontainers">testcontainers</a>。在那里你可以找到许多编程语言的库，包括 Java。</p>
<p>首先，你需要添加一些依赖项到你的 <code>pom.xml</code> 文件：</p>
<pre><code class="language-xml">       &lt;!-- testcontainer --&gt;
       &lt;dependency&gt;
           &lt;groupId&gt;org.testcontainers&lt;/groupId&gt;
           &lt;artifactId&gt;testcontainers&lt;/artifactId&gt;
           &lt;version&gt;1.16.3&lt;/version&gt;
           &lt;scope&gt;test&lt;/scope&gt;
       &lt;/dependency&gt;
       &lt;dependency&gt;
           &lt;groupId&gt;org.testcontainers&lt;/groupId&gt;
           &lt;artifactId&gt;mysql&lt;/artifactId&gt;
           &lt;version&gt;1.16.3&lt;/version&gt;
           &lt;scope&gt;test&lt;/scope&gt;
       &lt;/dependency&gt;
       &lt;dependency&gt;
           &lt;groupId&gt;org.testcontainers&lt;/groupId&gt;
           &lt;artifactId&gt;junit-jupiter&lt;/artifactId&gt;
           &lt;version&gt;1.16.3&lt;/version&gt;
           &lt;scope&gt;test&lt;/scope&gt;
       &lt;/dependency&gt;
</code></pre>
<p>你需要更新测试以利用 testcontainers，它在每次测试运行时启动数据库。在测试中添加一个注解和一个字段来利用它：</p>
<pre><code class="language-java">//添加 imports
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.DynamicPropertyRegistry;
import org.springframework.test.context.DynamicPropertySource;
import org.testcontainers.containers.MySQLContainer;
import org.testcontainers.junit.jupiter.Container;
import org.testcontainers.junit.jupiter.Testcontainers;
 
@SpringBootTest
@AutoConfigureMockMvc
@Testcontainers // Annotation to enable testcontainers
public class AccessingDataRestApplicationTests {
 
   //访问已启动数据库的字段
   @Container
   private static MySQLContainer database = new MySQLContainer&lt;&gt;("mysql:5.7.34");
 
   //使用启动的数据库设置数据库配置
   @DynamicPropertySource
   static void databaseProperties(DynamicPropertyRegistry registry) {
       registry.add("spring.datasource.url", database::getJdbcUrl);
       registry.add("spring.datasource.username", database::getUsername);
       registry.add("spring.datasource.password", database::getPassword);
   }
</code></pre>
<p>对于每个测试的执行，启动数据库做好准备，这使你在执行测试时可以使用一个实际的数据库。所有的连接、设置、启动和清理都为你完成。</p>
<h2 id="docker">Docker 化你的应用程序</h2>
<p>使用简单的 Docker 工具对你的应用程序进行 Docker 化是可能的，但不推荐。</p>
<p>你可以建立你的应用程序，使用一个包含 Java 的基础容器，然后复制并运行你的应用程序。但是有很多陷阱，每种语言和框架都是如此。所以一定要寻找能让你的生活更轻松的工具。</p>
<p>在这个例子中，你将使用 <a href="https://github.com/GoogleContainerTools/jib">Jib</a> 和 <a href="https://github.com/GoogleContainerTools/distroless">distroless containers</a> 来轻松构建一个 Docker 容器。将两者结合使用，可以得到一个最小的、安全的、可复制的容器，它在本地和生产中的工作方式是一样的。</p>
<p>要使用 Jib，你需要把它作为一个maven插件添加到你的 <code>pom.xml</code> 文件：</p>
<pre><code class="language-xml">&lt;build&gt;
       &lt;plugins&gt;
           &lt;plugin&gt;
               &lt;groupId&gt;org.springframework.boot&lt;/groupId&gt;
               &lt;artifactId&gt;spring-boot-maven-plugin&lt;/artifactId&gt;
           &lt;/plugin&gt;
 
        &lt;!-- Jib plugin --&gt;
           &lt;plugin&gt;
               &lt;groupId&gt;com.google.cloud.tools&lt;/groupId&gt;
               &lt;artifactId&gt;jib-maven-plugin&lt;/artifactId&gt;
               &lt;version&gt;3.2.1&lt;/version&gt;
               &lt;configuration&gt;
                   &lt;from&gt;
                       &lt;image&gt;gcr.io/distroless/java17:nonroot&lt;/image&gt;
                   &lt;/from&gt;
                   &lt;to&gt;
                       &lt;image&gt;my-docker-image&lt;/image&gt;
                   &lt;/to&gt;
               &lt;/configuration&gt;
           &lt;/plugin&gt;
       &lt;/plugins&gt;
   &lt;/build&gt;
</code></pre>
<p>现在你可以建立镜像并运行应用程序：</p>
<pre><code class="language-sh"># 构建 docker 容器
./mvnw compile jib:dockerBuild

# 查找你的构建镜像
docker images

# 启动数据库
docker run --rm -v "$PWD/data":/var/lib/mysql --name mysql -e MYSQL_ROOT_PASSWORD=admin-password -e MYSQL_DATABASE=my-database -p 3306:3306 mysql:8.0.28-debian


# 启动包含你的应用程序的 docker 容器
docker run --net=host my-docker-image

… skipped output…
2022-04-15 17:43:51.509  INFO 1 --- [           main] o.s.b.w.embedded.tomcat.TomcatWebServer  : Tomcat started on port(s): 8080 (http) with context path ''
2022-04-15 17:43:51.521  INFO 1 --- [           main] c.e.a.AccessingDataRestApplication       : Started AccessingDataRestApplication in 6.146 seconds (JVM running for 6.568)
</code></pre>
<p>该应用程序以网络模式主机 <code>--net=host</code> 启动，这使得它很容易只连接到你启动的数据库。另外，你可以创建一个 <code>docker network</code>，在同一个网络中启动两者。</p>
<p>你可以将你的容器推送到容器 registry，并从任何容器协调工具中使用它，在生产中运行你的应用程序。</p>
<h2 id="">总结</h2>
<p>在本教程中，你学到了如何利用 Docker 来创建、测试和运行应用程序而不污染你的系统。</p>
<p>一切都在你独立的 Docker 环境中，并在本地工作，像持续集成系统和生产系统中，你可能启动数百个你的应用程序。</p>
<p>你可以在<a href="https://github.com/sesigl/docker-for-development-example-application">我的 GitHub Docker For Development 应用实例库</a> 中找到可以使用的例子。</p>
<p>我希望你喜欢这篇文章。</p>
<p>如果你喜欢它，觉得有必要给我点赞，或者只是想联系我，<a href="https://twitter.com/sesigl">在Twitter上关注我</a>。</p>
<p>我在 eBay Kleinanzeigen 工作，这是全球最大的电子商务公司之一。顺便说一下，<a href="https://www.ebay-kleinanzeigen.de/careers">我们正在招聘</a>！</p>
<h2 id="">参考文献</h2>
<ul>
<li><a href="https://hub.docker.com/_/mysql/">Docker Hub：MySQL 镜像</a></li>
<li><a href="https://docs.docker.com/engine/reference/commandline/run/">Docker 文档：run 命令</a></li>
<li><a href="https://code.visualstudio.com/docs/remote/containers">Visual Studio Code：Remote 容器</a></li>
<li><a href="https://www.freecodecamp.org/news/learn-java-free-java-courses-for-beginners/">Learn Java – free Java courses</a></li>
<li><a href="https://www.youtube.com/watch?v=H6gR_Cv4yWI">YouTube：用 Jib 更快地构建容器</a></li>
</ul>
<!--kg-card-end: markdown--> ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ Docker 挂载卷——如何挂载一个本地目录 ]]>
                </title>
                <description>
                    <![CDATA[ 原文：Docker Mount Volume – How To Mount a Local Directory [https://www.freecodecamp.org/news/docker-mount-volume-guide-how-to-mount-a-local-directory/] ，作者：Sebastian Sigl [https://www.freecodecamp.org/news/author/sesigl/] 容器使软件工程更容易、更高效，Docker [https://www.docker.com/]容器很受欢迎，也很容易使用。 容器对于本地开发来说是必不可少的。它们让你在本地环境中测试你的应用程序，并开始建立所需的基础设施。 Docker容器本质上是不可变的。这意味着重新启动一个容器会删除你在容器中存储的所有数据。但是Docker提供了卷和绑定挂载，这是两种在Docker容器中持久保存数据的机制。 本教程将教你如何将本地目录绑定到你的Docker容器上，并交替使用Docker管理的卷。了解了这两点，你就能在更多的用例中使用Docker容器，从而提高 ]]>
                </description>
                <link>https://www.freecodecamp.org/chinese/news/docker-mount-volume-guide-how-to-mount-a-local-directory/</link>
                <guid isPermaLink="false">6251612199ec7406219e6247</guid>
                
                    <category>
                        <![CDATA[ Docker ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ luojiyin ]]>
                </dc:creator>
                <pubDate>Fri, 08 Apr 2022 10:00:00 +0000</pubDate>
                <media:content url="https://chinese.freecodecamp.org/news/content/images/2022/04/Docker-mount-volume-guide.png" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>原文：<a href="https://www.freecodecamp.org/news/docker-mount-volume-guide-how-to-mount-a-local-directory/">Docker Mount Volume – How To Mount a Local Directory</a>，作者：<a href="https://www.freecodecamp.org/news/author/sesigl/">Sebastian Sigl</a></p><!--kg-card-begin: markdown--><p>容器使软件工程更容易、更高效，<a href="https://www.docker.com/">Docker</a>容器很受欢迎，也很容易使用。</p>
<p>容器对于本地开发来说是必不可少的。它们让你在本地环境中测试你的应用程序，并开始建立所需的基础设施。</p>
<p>Docker容器本质上是不可变的。这意味着重新启动一个容器会删除你在容器中存储的所有数据。但是Docker提供了卷和绑定挂载，这是两种在Docker容器中持久保存数据的机制。</p>
<p>本教程将教你如何将本地目录绑定到你的Docker容器上，并交替使用Docker管理的卷。了解了这两点，你就能在更多的用例中使用Docker容器，从而提高你的工作效率。</p>
<h2 id="dockerrunv">使用 <code>docker run -v</code> 进行本地目录挂载</h2>
<blockquote>
<p><code>docker run</code>命令首先在指定的镜像上创建一个可写的容器层，然后使用指定的命令启动。（来源 <a href="https://www.bing.com/search?form=MOZLBR&amp;ptag=MOZZ0000000011&amp;pc=MOZD&amp;q=docker+run+">docker.com</a>）<br>
使用参数<code>-v</code>允许你绑定一个本地目录。</p>
</blockquote>
<p><code>-v</code>或<code>--volume</code>允许你挂载本地目录和文件到你的容器。例如，你可以启动一个MySQL数据库并挂载数据目录，将实际数据存储在你挂载的目录中。</p>
<pre><code class="language-shell"># run mysql container in the background

$ docker run --name mysql-db -v $(pwd)/datadir:/var/lib/mysql -e MYSQL_ROOT_PASSWORD=my-secret-pw -d mysql:8.0.28-debian

# show content of data directory
$ ls -la datadir
total 383848
-rw-r-----    1 sebarthel  staff    196608 Mar 26 22:47 #ib_16384_0.dblwr
-rw-r-----    1 sebarthel  staff   8585216 Mar 26 22:47 #ib_16384_1.dblwr
drwxr-x---   12    sebarthel  staff       384 Mar 26 22:47 #innodb_temp
drwxr-xr-x@  27 sebarthel  staff       864 Mar 26 22:47 .
drwxr-xr-x    3 sebarthel  staff        96 Mar 26 22:47 ..
-rw-r-----    1 sebarthel  staff        56 Mar 26 22:47 auto.cnf
-rw-r-----    1 sebarthel  staff       913 Mar 26 22:47 binlog.000001
(more directories)

# stop mysql container
docker rm -f mysql-db
</code></pre>
<p>绑定目录是一种双向的同步。你在主机上改变的每个文件都会在容器中改变，而容器中改变的每个文件都会在主机上改变。因此，如果你停止和启动数据库，你可以挂载同一个目录，你的配置和存储的数据将是可用的。</p>
<p>这种方法的优点是使用起来很直接，而且容易访问。你应该使用绑定的本地目录来存放你想在主机上改变或观察的文件，如配置文件和日志文件。</p>
<h2 id="dockervolumes">如何使用Docker Volumes来保存变化</h2>
<p>你可以使用Docker卷，而不是绑定你的本地目录。Docker卷是你的Docker存储目录中的某个地方的一个目录，可以挂载到一个或许多容器上。它们是完全可管理的，不依赖于某些操作系统。</p>
<p>让我们创建一个Docker卷并挂载它来保存MySQL数据：</p>
<pre><code class="language-shell"># create volume
docker volume create mysql-data

# run mysql container in the background
$ docker run --name mysql-db -v mysql-data:/var/lib/mysql -e MYSQL_ROOT_PASSWORD=my-secret-pw -d mysql:latest

# stop mysql container
docker rm -f mysql-db

# remove volume
docker volume remove mysql-data
</code></pre>
<p>在删除Docker卷之前，你可以打开你的Docker GUI，通过点击<code>data</code>标签来检查卷。</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2022/03/docker-ui-volume.png" alt="docker-ui-volume" width="600" height="400" loading="lazy"></p>
<p>你可以看到这些文件，但它们被隔离在一个Docker卷中。建议使用它们来保存那些你不需要从主机系统观察或改变的文件。众所周知，这种方法比本地目录绑定有更好的性能。</p>
<h1 id="">总结</h1>
<p>当你知道如何持久化你的数据，并且在停止容器时不丢失它们时，Docker容器会变得更加强大。</p>
<p>你通过提供Docker运行<code>-v</code>参数将本地目录和卷绑定到一个容器。你需要给出绝对的本地路径或卷的名称，并将其映射到容器内的目录<code>-v &lt;source&gt;:&lt;target&gt;</code>。</p>
<p>我希望你喜欢这篇文章。</p>
<p>如果你喜欢它，觉得有必要给我鼓掌，或者只是想联系我，<a href="https://twitter.com/sesigl">在Twitter上关注我</a>。</p>
<p>我在eBay Kleinanzeigen工作。顺便说一下，<a href="https://www.ebay-kleinanzeigen.de/careers">我们正在招聘</a>！</p>
<h2 id="">参考资料</h2>
<ul>
<li><a href="https://towardsdatascience.com/how-to-mount-a-directory-inside-a-docker-container-4cee379c298b">如何在Docker容器内挂载一个目录</a></li>
<li><a href="https://hub.docker.com/_/mysql/">使用MySql的Docker镜像</a></li>
<li><a href="https://maximorlov.com/docker-compose-syntax-volume-or-bind-mount/">Docker-Compose卷或绑定挂载语法</a></li>
<li><a href="https://www.thegeekdiary.com/how-to-pause-and-resume-docker-containers/">如何暂停和恢复Docker容器</a></li>
<li><a href="https://blog.logrocket.com/docker-volumes-vs-bind-mounts/">Docker卷与绑定挂载</a></li>
<li><a href="https://docs.docker.com/engine/reference/commandline/volume_create/">Docker文档：卷创建的命令</a></li>
<li><a href="https://docs.docker.com/storage/volumes/#backup-restore-or-migrate-data-volumes">Docker文档：卷的备份和恢复</a></li>
<li><a href="https://blog.jessfraz.com/post/docker-containers-on-the-desktop/">Docker容器桌面管理端</a></li>
</ul>
<!--kg-card-end: markdown--> ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ 如何删除所有 Docker 镜像——Docker 清理指南 ]]>
                </title>
                <description>
                    <![CDATA[ 原文：How to Remove All Docker Images – A Docker Cleanup Guide [https://www.freecodecamp.org/news/how-to-remove-all-docker-images-a-docker-cleanup-guide/] ，作者：Sebastian Sig [https://www.freecodecamp.org/news/author/sesigl/] 容器在当今的技术世界中无处不在。最流行的容器管理技术是 Docker [https://www.docker.com/] 。它使使用容器变得简单，并帮助你轻松地让应用程序启动和运行。 不幸的是，这可能会占用大量的磁盘空间，最终你将会有一个完整的磁盘。 如果你在设备或服务器上使用Docker，这并不重要。本指南告诉你如何分析已使用的磁盘空间和清理不同的Docker资源。 你所需要的只是一个正在运行的Docker守护进程和一个终端。 如何分析Docker使用了多少空间 你可以通过运行下面的命令来查看有多少空间被使用： $ docker syst ]]>
                </description>
                <link>https://www.freecodecamp.org/chinese/news/how-to-remove-all-docker-images-a-docker-cleanup-guide/</link>
                <guid isPermaLink="false">6245896d7f18d1062895c4a3</guid>
                
                    <category>
                        <![CDATA[ Docker ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ luojiyin ]]>
                </dc:creator>
                <pubDate>Thu, 31 Mar 2022 10:30:00 +0000</pubDate>
                <media:content url="https://chinese.freecodecamp.org/news/content/images/2022/03/docker-cleanup-guide.png" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>原文：<a href="https://www.freecodecamp.org/news/how-to-remove-all-docker-images-a-docker-cleanup-guide/">How to Remove All Docker Images – A Docker Cleanup Guide</a>，作者：<a href="https://www.freecodecamp.org/news/author/sesigl/">Sebastian Sig</a></p><!--kg-card-begin: markdown--><p>容器在当今的技术世界中无处不在。最流行的容器管理技术是 <a href="https://www.docker.com/">Docker</a>。它使使用容器变得简单，并帮助你轻松地让应用程序启动和运行。</p>
<p>不幸的是，这可能会占用大量的磁盘空间，最终你将会有一个完整的磁盘。</p>
<p>如果你在设备或服务器上使用Docker，这并不重要。本指南告诉你如何分析已使用的磁盘空间和清理不同的Docker资源。</p>
<p>你所需要的只是一个正在运行的Docker守护进程和一个终端。</p>
<h2 id="docker">如何分析Docker使用了多少空间</h2>
<p>你可以通过运行下面的命令来查看有多少空间被使用：</p>
<pre><code class="language-sh">$ docker system df

TYPE            TOTAL     ACTIVE    SIZE      RECLAIMABLE
Images          61        16        21.1GB    15.25GB (72%)
Containers      69        0         12.26MB   12.26MB (100%)
Local Volumes   3         2         539.1MB   50.04MB (9%)
Build Cache     76        0         1.242GB   1.242GB
</code></pre>
<p>你可以通过使用verbose选项 <code>-v</code> 获得更多信息：</p>
<pre><code class="language-sh">$ docker system df -v

REPOSITORY        TAG   IMAGE ID     CREATED       SIZE      SHARED 
teamatldocker/jira    e50b8390945c   4 weeks ago     842.3MB   0B       
vw                    ed9e125a8925   2 months ago    1.659GB   134.8MB 

Containers space usage:

CONTAINER ID   IMAGE                    COMMAND                   SIZE 
94e03a4a17d0   teamatldocker/jira       "/sbin/tini -- /usr/…"    1.4MB 

Local Volumes space usage:

VOLUME NAME                     LINKS     SIZE
play-with-jira_postgresqldata   1         84.19MB   
play-with-jira_jiradata         1         404.8MB

Build cache usage: 1.242GB

CACHE ID       CACHE TYPE     SIZE      CREATED        LAST USED 
oxil5sdicb91   regular        135MB     2 months ago   2 months ago  
kxz13fmdbodg   regular        13B       2 months ago   2 months ago 
nysus21ej7pf   regular        0B        2 months ago   2 months ago
</code></pre>
<p>正如你所看到的，你可以得到以下信息：</p>
<ul>
<li>镜像空间的使用</li>
<li>容器空间的使用</li>
<li>本地卷的空间使用，以及</li>
<li>构建缓存的使用情况</li>
</ul>
<h2 id="docker">如何在Docker中清理一切</h2>
<p>你可以清理一切，也可以清理Docker中的特定资源，如镜像、容器卷或构建缓存。</p>
<p>要尽可能地清理，不包括正在使用的组件，请运行这个命令：</p>
<pre><code class="language-sh">docker system prune -a
</code></pre>
<p><code>-a</code> 包括未使用的和悬空的容器。不提供`-a'将只删除悬空的镜像，这些镜像是没有标记的镜像，与任何其他镜像没有关系。</p>
<p>如果你想清理大部分Docker资源，但仍然保留有标签的镜像，你可以执行这个命令：</p>
<pre><code class="language-sh">docker system prune
</code></pre>
<p>这就是你快速释放磁盘空间所需要的一切。此外，你还可以单独清理组件。</p>
<p>这里有几个更有用的命令：</p>
<h3 id="">清理未使用和悬空的镜像</h3>
<pre><code class="language-sh">docker image prune
</code></pre>
<h3 id="">只清理悬空的镜像</h3>
<pre><code class="language-sh">docker image prune -a
</code></pre>
<h3 id="">清理停止运行的容器</h3>
<pre><code class="language-sh">docker container prune
</code></pre>
<h3 id="">清理未使用的卷宗</h3>
<pre><code class="language-sh">docker volume prune
</code></pre>
<h2 id="docker">如何持续有效地管理你已使用的Docker空间</h2>
<p>你可以在日常或启动时运行一些东西。要跳过通常的提示，你需要在你想自动运行的命令中添加<code>-f</code>。</p>
<p>请记住，这将导致你更频繁地下载镜像，因为你定期删除Docker资源。</p>
<p>如果你没有磁盘空间问题，那么不用担心。一旦Docker磁盘使用量过大引起你的注意，就立即清理。</p>
<h2 id="">结语</h2>
<p>如今，有很多方法可以使用<code>docker</code>命令来清理Docker磁盘空间。如果你想定期清理Docker资源，你甚至可以自动执行这些命令。</p>
<p>我希望你喜欢这篇文章。</p>
<p>如果你喜欢它，觉得有必要给我点赞，或者只是想联系我，<a href="https://twitter.com/sesigl">在Twitter上关注我</a>。</p>
<p>我在eBay Kleinanzeigen工作，这是全球最大的公司之一。顺便说一下，<a href="https://jobs.ebayclassifiedsgroup.com/ebay-kleinanzeigen">我们正在招聘</a>!</p>
<!--kg-card-end: markdown--> ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ 使用 Cypress 创建测试镜像并完成 E2E 测试 ]]>
                </title>
                <description>
                    <![CDATA[ 缘由 最近在做一个 Buildkite 的 Dashboard 的项目 Powerboard [https://github.com/guzhongren/Powerboard]，项目是托管在 GitHub 的 Git Pages 上的; 项目只是一个纯前端项目，且 E2E 测试是用 Cypress [https://www.cypress.io/]构建的；如果要进行 E2E 测试一般情况都是对着部署在 Git Pages 上的网站直接测试，而且也是这么做的😄。 痛点 测试滞后 这么做肯定是有问题的，产品都上线了才做测试，肯定已经迟了；如果程序有问题，那么就会影响所有用户。这种情况应该算是 P1 级别的产品事故，对用户来说简直就是灾难。应该在部署之前就应该完成 E2E 测试，如果测试通过不了，就不应该部署代码。所以测试应该前移。 解决方案 由于我们的测试需要自动化，需要在 Pipeline 上执行，所以必须是一个可以独立运行的程序和 Cypress 程序同时运行，并最终返回测试结果，由 Pipeline 来决定是否终止 Pipeline 运行。 在 GitHub Actions ]]>
                </description>
                <link>https://www.freecodecamp.org/chinese/news/do-e2e-test-with-cypress-image/</link>
                <guid isPermaLink="false">61e413486161280665ed8273</guid>
                
                    <category>
                        <![CDATA[ cypress ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Docker ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ 谷中仁 ]]>
                </dc:creator>
                <pubDate>Mon, 17 Jan 2022 07:00:00 +0000</pubDate>
                <media:content url="https://chinese.freecodecamp.org/news/content/images/2022/01/pexels-photo-5667741-1.jpeg" medium="image" />
                <content:encoded>
                    <![CDATA[ <h2 id="-">缘由</h2><p>最近在做一个 Buildkite 的 Dashboard 的项目 <a href="https://github.com/guzhongren/Powerboard" rel="noopener noreffer">Powerboard</a>，项目是托管在 GitHub 的 Git Pages 上的; 项目只是一个纯前端项目，且 E2E 测试是用 <a href="https://www.cypress.io/" rel="noopener noreffer">Cypress</a>构建的；如果要进行 E2E 测试一般情况都是对着部署在 Git Pages 上的网站直接测试，而且也是这么做的😄。</p><h2 id="--1">痛点</h2><h3 id="--2">测试滞后</h3><p>这么做肯定是有问题的，产品都上线了才做测试，肯定已经迟了；如果程序有问题，那么就会影响所有用户。这种情况应该算是 P1 级别的产品事故，对用户来说简直就是灾难。应该在部署之前就应该完成 E2E 测试，如果测试通过不了，就不应该部署代码。所以测试应该前移。</p><h2 id="--3">解决方案</h2><p>由于我们的测试需要自动化，需要在 Pipeline 上执行，所以必须是一个可以独立运行的程序和 Cypress 程序同时运行，并最终返回测试结果，由 Pipeline 来决定是否终止 Pipeline 运行。</p><p>在 GitHub Actions 的 Pipeline 上同时运行程序只能依靠 <code>docker-compose</code>, 在这我们可以使用 Cypress 官方出品的 <a href="https://hub.docker.com/r/cypress/included" rel="noopener noreffer">cypress/included</a>, 通过编排程序来进行测试。</p><h3 id="cypress-included">cypress/included</h3><p>cypress/included 可以让我们挂载 cypress 的测试脚本，然后自动执行，并在最终返回 Linux 命令状态值，如 0 ， 非 0 值。</p><h3 id="docker-compose">Docker-compose</h3><p><a href="https://docs.docker.com/compose/" rel="noopener noreffer">Docker-compose</a> 是一套容器编排工具，可以很轻松的管理容器的启动顺序等。在本地项目搭建中非常有用，比如构建数据库，执行 shell/yaml lint 等。</p><h2 id="--4">执行方案</h2><h3 id="--5">构建应用镜像</h3><p>在测试之前需要将应用构建好并部署好，我们可以用 Node 镜像打包应用，并利用容器的多阶段构建(<a href="https://docs.docker.com/develop/develop-images/multistage-build/" rel="noopener noreffer">multi-stage builds</a>) 完成应用轻量化构建，并部署在 <a href="https://hub.docker.com/_/nginx" rel="noopener noreffer">Nginx</a> 中。</p><pre><code class="language-yaml">FROM node:17-alpine as distPackage
COPY ./ /app
WORKDIR /app
RUN yarn
RUN yarn build

FROM nginx:latest
COPY --from=distPackage /app/dist /usr/share/nginx/html
</code></pre><h3 id="-service">编排 service</h3><p>因为我们的程序需要在测试的时候就要部署好，所以我们可以利用 Docker-compose 的 <a href="https://docs.docker.com/compose/compose-file/compose-file-v3/#build" rel="noopener noreffer">build</a> 参数，在容器启动时构建应用并部署。并在 cypress/included 启动是执行测试命令 <code>npx cy:docker</code>, 具体就是<code>cross-env ENV=docker cypress run --spec 'cypress/integration/dashboard.spec.js</code>。</p><pre><code class="language-yaml">version: '3'
services:
  web:
    build:
      context: ./
      dockerfile: ./Dockerfile
    container_name: web
    restart: always
    ports:
      - '80:80'

  e2e:
    image: cypress/included:9.2.1
    container_name: cypress
    depends_on:
      - web
    environment:
      - CYPRESS_baseUrl=http://web
      - ENV=docker
    command: npx cy:docker
    working_dir: /e2e
    volumes:
      - ./:/e2e

</code></pre><p>这样我们就可以独立的运行起真实程序和正式的测试程序了，具体的 Pipeline 可以参考 Powerboard 的 <a href="https://github.com/guzhongren/Powerboard/blob/main/.github/workflows/main.yml" rel="noopener noreffer">Workflow</a>。</p><pre><code class="language-yml">      - name: E2E
        run: |
          docker-compose up --build e2e

</code></pre><h2 id="--6">总结</h2><p><code>Docker-compopse</code> 有很好的应用编排能力，可以很轻松的构建多服务程序；并在构建应用的时候可以使用多阶段构建来优化镜像大小。使用 <code>Cypress</code> 可以提高开发效率并可在 <code>Pipeline</code> 上保证程序的正确性。</p><h2 id="refs">Refs</h2><ul><li><a href="https://guzhongren.github.io/" rel="noopener noreffer">博客:https://guzhongren.github.io/</a></li><li><a href="https://www.cypress.io/" rel="noopener noreffer">Cypress: https://www.cypress.io/</a></li><li><a href="https://hub.docker.com/r/cypress/included" rel="noopener noreffer">cypress/included: https://hub.docker.com/r/cypress/included</a></li><li><a href="https://docs.github.com/en/actions" rel="noopener noreffer">GitHub Actions: https://docs.github.com/en/actions</a></li><li><a href="https://github.com/guzhongren/Powerboard" rel="noopener noreffer">Powerboard: https://github.com/guzhongren/Powerboard</a></li></ul><p>欢迎阅读我的<a href=" https://guzhongren.github.io/2022/01/%E4%BD%BF%E7%94%A8cypress%E5%88%9B%E5%BB%BA%E6%B5%8B%E8%AF%95%E9%95%9C%E5%83%8F%E5%B9%B6%E5%AE%8C%E6%88%90e2e%E6%B5%8B%E8%AF%95/">更多文章</a>。</p><!--kg-card-begin: markdown--><p><img src="https://cdn.jsdelivr.net/gh/guzhongren/data-hosting@master/20210819/%E6%89%AB%E7%A0%81_%E6%90%9C%E7%B4%A2%E8%81%94%E5%90%88%E4%BC%A0%E6%92%AD%E6%A0%B7%E5%BC%8F-%E7%99%BD%E8%89%B2%E7%89%88.ae9zxgscqcg.png" alt="谷哥说-微信公众号" width="600" height="400" loading="lazy"></p>
<!--kg-card-end: markdown--><p></p> ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ Docker 完全手册 ]]>
                </title>
                <description>
                    <![CDATA[ 容器化的概念很早就有了。2013 年 Docker 引擎 [https://docs.docker.com/get-started/overview/#docker-engine]的出现使应用程序容器化变得更加容易。 根据 Stack Overflow 开发者调查-2020 [https://insights.stackoverflow.com/survey/2020#overview]，Docker [https://docker.com/] 是开发者 #1 最想要的平台 [https://insights.stackoverflow.com/survey/2020#technology-most-loved-dreaded-and-wanted-platforms-wanted5] 、#2 最喜欢的平台 [https://insights.stackoverflow.com/survey/2020#technology-most-loved-dreaded-and-wanted-platforms-loved5] ，以及#3 最流行的平台 [https://insights.st ]]>
                </description>
                <link>https://www.freecodecamp.org/chinese/news/the-docker-handbook/</link>
                <guid isPermaLink="false">6066ec5a4c5a5f056433056e</guid>
                
                    <category>
                        <![CDATA[ Docker ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ ZhichengChen ]]>
                </dc:creator>
                <pubDate>Fri, 02 Apr 2021 08:00:00 +0000</pubDate>
                <media:content url="https://chinese.freecodecamp.org/news/content/images/2021/04/docker-1280x612-2021.png" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>容器化的概念很早就有了。2013 年 <a href="https://docs.docker.com/get-started/overview/#docker-engine">Docker 引擎</a>的出现使应用程序容器化变得更加容易。</p>
<p>根据 <a href="https://insights.stackoverflow.com/survey/2020#overview">Stack Overflow 开发者调查-2020</a>，<a href="https://docker.com/">Docker</a> 是开发者 <a href="https://insights.stackoverflow.com/survey/2020#technology-most-loved-dreaded-and-wanted-platforms-wanted5">#1 最想要的平台</a>、<a href="https://insights.stackoverflow.com/survey/2020#technology-most-loved-dreaded-and-wanted-platforms-loved5">#2 最喜欢的平台</a>，以及<a href="https://insights.stackoverflow.com/survey/2020#technology-platforms">#3 最流行的平台</a>。</p>
<p>尽管 Docker 功能强大，但上手确并不容易。因此，本书将介绍从基础知识到更高层次容器化的的所有内容。读完整本书之后，你应该能够：</p>
<ul>
<li>容器化（几乎）任何应用程序</li>
<li>将自定义 Docker 镜像上传到在线仓库</li>
<li>使用 Docker Compose 处理多个容器</li>
</ul>
<h2 id="">前提</h2>
<ul>
<li>熟悉 Linux 终端操作</li>
<li>熟悉 JavaScript（稍后的的演示项目用到了 JavaScript）</li>
</ul>
<h2 id="">目录</h2>
<ul>
<li>容器化和 Docker 简介</li>
<li>怎样安装 Docker
<ul>
<li>怎样在 macOS 里安装 Docker</li>
<li>怎样在 Windows 上安装 Docker</li>
<li>怎样在 Linux 上安装 Docker</li>
</ul>
</li>
<li>初识 Docker - Docker 基本知识介绍
<ul>
<li>什么是容器？</li>
<li>什么是 Docker 镜像？</li>
<li>什么是仓库？</li>
<li>Docker 架构概述</li>
<li>全景图</li>
</ul>
</li>
<li>Docker 容器操作基础知识
<ul>
<li>怎样运行容器</li>
<li>怎样公开端口</li>
<li>如何使用分离模式</li>
<li>怎样列表展示容器</li>
<li>怎样命名或者重命名一个容器</li>
<li>怎样停止或者杀死运行中的容器</li>
<li>怎样重新启动容器</li>
<li>怎样创建而不运行容器</li>
<li>怎样移除挂起的容器</li>
<li>怎样以交互式模式运行容器</li>
<li>怎样在容器里执行命令</li>
<li>如何处理可执行镜像</li>
</ul>
</li>
<li>Docker 镜像操作基础知识
<ul>
<li>如何创建 Docker 镜像</li>
<li>如何标记 Docker 镜像</li>
<li>如何删除、列表展示镜像</li>
<li>理解 Docker 镜像的分层</li>
<li>怎样从源码构建 NGINX</li>
<li>怎样优化 Docker 镜像</li>
<li>拥抱 Alpine Linux</li>
<li>怎样创建可执行 Docker 镜像</li>
<li>怎样在线共享 Docker 镜像</li>
</ul>
</li>
<li>怎样容器化 JavaScript 应用
<ul>
<li>如何编写开发 Dockerfile</li>
<li>如何在 Docker 中使用绑定挂载</li>
<li>如何在 Docker 中使用匿名卷</li>
<li>如何在 Docker 中执行多阶段构建</li>
<li>如何忽略不必要的文件</li>
</ul>
</li>
<li>Docker 中的网络操作基础知识
<ul>
<li>Docker 网络基础</li>
<li>如何在 Docker 中创建用户定义的桥接网络</li>
<li>如何在 Docker 中将容器连接到网络</li>
<li>如何在 Docker 中从网络分离容器</li>
<li>如何删除 Docker 中的网络</li>
</ul>
</li>
<li>如何容器化多容器 JavaScript 应用程序
<ul>
<li>如何运行数据库服务</li>
<li>如何在 Docker 中使用命名卷</li>
<li>如何从 Docker 中的容器访问日志</li>
<li>如何在 Docker 中创建网络并连接数据库服务</li>
<li>如何编写 Dockerfile</li>
<li>如何在正在运行的容器中执行命令</li>
<li>如何在 Docker 中编写管理脚本</li>
</ul>
</li>
<li>如何使用 Docker-Compose 组合项目
<ul>
<li>Docker Compose 基础</li>
<li>如何在 Docker Compose 中启动服务</li>
<li>如何在 Docker Compose 中列表展示服务</li>
<li>如何在 Docker Compose 正在运行的服务中执行命令</li>
<li>如何访问 Docker Compose 中正在运行的服务日志</li>
<li>如何在 Docker Compose 中停止服务</li>
<li>如何在 Docker Compose 中编写全栈应用程序</li>
</ul>
</li>
<li>结论</li>
</ul>
<h2 id="">项目代码</h2>
<p>可以在<a href="https://github.com/fhsinchy/docker-handbook-projects/">这个仓库</a>中找到示例项目的代码，欢迎 ⭐。</p>
<p>完整代码在 <a href="https://github.com/fhsinchy/docker-handbook-projects/tree/containerized"><code>containerized</code></a> 分支。</p>
<h2 id="">贡献</h2>
<p>这本书是完全开源的，欢迎高质量的贡献。可以在<a href="https://github.com/fhsinchy/the-docker-handbook">这个仓库</a>中找到完整的内容。</p>
<p>我通常先在本书的 GitBook 版本上进行更改和更新，然后在将其发布在 freeCodeCamp 专栏。你可以在<a href="https://docker.farhan.info/">这个链接</a>中找到本书的最新编辑中版本。别忘了评分支持。</p>
<p>如果你正在寻找本书的完整稳定版本，那么 freeCodeCamp 是最好的选择。如果你有所收获，请分享给你的朋友。</p>
<p>不管阅读本书的哪个版本，都不要忘记留下你的意见。欢迎提出建设性的批评。</p>
<h2 id="docker">容器化和 Docker 简介</h2>
<p>摘自 <a href="https://www.ibm.com/cloud/learn/containerization#toc-what-is-co-r25Smlqq">IBM</a>,</p>
<blockquote>
<p>容器化意味着封装或打包软件代码及其所有依赖项，以便它可以在任何基础架构上统一且一致地运行。</p>
</blockquote>
<p>换句话说，容器化可以将软件及其所有依赖项打包在一个自包含的软件包中，这样就可以省略麻烦的配置，直接运行。</p>
<p>举一个现实生活的场景。假设你已经开发了一个很棒的图书管理应用程序，该应用程序可以存储所有图书的信息，还可以为别人提供图书借阅服务。</p>
<p>如果列出依赖项，如下所示：</p>
<ul>
<li>Node.js</li>
<li>Express.js</li>
<li>SQLite3</li>
</ul>
<p>理论上应该是这样。但是实际上还要搞定其他一些事情。 <a href="https://nodejs.org/">Node.js</a> 使用了 <code>node-gyp</code> 构建工具来构建原生加载项。根据<a href="https://github.com/nodejs/node-gyp">官方存储库</a>中的<a href="https://github.com/nodejs/node-gyp#installation">安装说明</a>，此构建工具需要 Python 2 或 3 和相应的的 C/C ++ 编译器工具链。</p>
<p>考虑到所有这些因素，最终的依赖关系列表如下：</p>
<ul>
<li>Node.js</li>
<li>Express.js</li>
<li>SQLite3</li>
<li>Python 2 or 3</li>
<li>C/C++ tool-chain</li>
</ul>
<p>无论使用什么平台，安装 Python 2 或 3 都非常简单。在 Linux 上，设置 C/C ++ 工具链也非常容易，但是在 Windows 和 Mac 上，这是一项繁重的工作。</p>
<p>在 Windows 上，C++ 构建工具包有数 GB 之大，安装需要花费相当长的时间。在 Mac 上，可以安装庞大的 <a href="https://developer.apple.com/xcode/">Xcode</a> 应用程序，也可以安装小巧的 <a href="https://developer.apple.com/download/">Xcode 命令行工具</a>包。</p>
<p>不管安装了哪一种，它都可能会在 OS 更新时中断。实际上，该问题非常普遍，甚至连官方仓库都专门提供了 <a href="https://github.com/nodejs/node-gyp/blob/master/macOS_Catalina.md">macOS Catalina 的安装说明</a>。</p>
<p>这里假设你已经解决了设置依赖项的所有麻烦，并且已经准备好开始。这是否意味着现在开始就一帆风顺了？当然不是。</p>
<p>如果你使用 Linux 而同事使用 Windows 该怎么办？现在，必须考虑如何处理这两个不同的操作系统不一致的路径，或诸如 <a href="https://nginx.org/">nginx</a> 之类的流行技术在 Windows 上未得到很好的优化的事实，以及诸如 <a href="https://redis.io/">Redis</a> 之类的某些技术甚至都不是针对 Windows 预先构建的。</p>
<p>即使你完成了整个开发，如果负责管理服务器的人员部署流程搞错了，该怎么办？</p>
<p>所有这些问题都可以通过以下方式解决：</p>
<ul>
<li>在与最终部署环境匹配的隔离环境（称为容器）中开发和运行应用程序。</li>
<li>将你的应用程序及其所有依赖项和必要的部署配置放入一个文件（称为镜像）中。</li>
<li>并通过具有适当授权的任何人都可以访问的中央服务器（称为仓库）共享该镜像。</li>
</ul>
<p>然后，你的同事就可以从仓库中下载镜像，可以在没有平台冲突的隔离环境中运行应用，甚至可以直接在服务器上进行部署，因为该镜像也可以进行生产环境配置。</p>
<p>这就是容器化背后的想法：将应用程序放在一个独立的程序包中，使其在各种环境中都可移植且可回溯。</p>
<p>现在的问题是：Docker 在这里扮演什么角色？</p>
<p>正如我之前讲的，容器化是一种将一切统一放入盒子中来解决软件开发过程中的问题的思想。</p>
<p>这个想法有很多实现。<a href="https://www.docker.com/">Docker</a> 就是这样的实现。这是一个开放源代码的容器化平台，可让你对应用程序进行容器化，使用公共或私有仓库共享它们，也可以<a href="https://docs.docker.com/get-started/orchestration/">编排</a>它们。</p>
<p>目前，Docker 并不是市场上唯一的容器化工具，却是最受欢迎的容器化工具。我喜欢的另一个容器化引擎是 Red Hat 开发的 <a href="https://podman.io/">Podman</a>。其他工具，例如 Google 的 <a href="https://github.com/GoogleContainerTools/kaniko">Kaniko</a>，CoreOS 的 <a href="https://coreos.com/rkt/">rkt</a> 都很棒，但和 Docker 还是有差距。</p>
<p>此外，如果你想了解容器的历史，可以阅读 <a href="https://blog.aquasec.com/a-brief-history-of-containers-from-1970s-chroot-to-docker-2016">A Brief History of Containers: From the 1970s Till Now</a>，它描述了该技术的很多重要节点。</p>
<h2 id="docker">怎样安装 Docker</h2>
<p>Docker 的安装因使用的操作系统而异。但这整个过程都非常简单。</p>
<p>Docker可在 Mac、Windows 和 Linux 这三个主要平台上完美运行。在这三者中，在 Mac 上的安装过程是最简单的，因此我们从这里开始。</p>
<h3 id="macosdocker">怎样在 macOS 里安装 Docker</h3>
<p>在 Mac 上，要做的就是跳转到官方的<a href="https://www.docker.com/products/docker-desktop">下载页面</a>，然后单击_Download for Mac(stable)_按钮。</p>
<p>你会看到一个常规的 <em>Apple Disk Image</em> 文件，在该文件的内有 Docker 应用程序。所要做的就是将文件拖放到 Applications 目录中。</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2021/01/drag-docker-in-applications-directory.png" alt="drag-docker-in-applications-directory" width="600" height="400" loading="lazy"></p>
<p>只需双击应用程序图标即可启动 Docker。应用程序启动后，将看到 Docker 图标出现在菜单栏上。</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2021/01/docker-icon-in-menubar.png" alt="docker-icon-in-menubar" width="600" height="400" loading="lazy"></p>
<p>现在，打开终端并执行 <code>docker --version</code> 和 <code>docker-compose --version</code> 以验证是否安装成功。</p>
<h3 id="windowsdocker">怎样在 Windows 上安装 Docker</h3>
<p>在 Windows 上，步骤几乎相同，当然还需要执行一些额外的操作。安装步骤如下：</p>
<ol>
<li>跳转到<a href="https://docs.microsoft.com/zh-cn/windows/wsl/install-win10">此站点</a>，然后按照说明在 Windows 10 上安装 WSL2。</li>
<li>然后跳转到官方<a href="https://www.docker.com/products/docker-desktop">下载页面</a> 并单击 <em>Download for Windows(stable)</em> 按钮。</li>
<li>双击下载的安装程序，然后使用默认设置进行安装。</li>
</ol>
<p>安装完成后，从开始菜单或桌面启动 <em>Docker Desktop</em>。Docker 图标应显示在任务栏上。</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2021/01/docker-icon-in-taskbar.png" alt="docker-icon-in-taskbar" width="600" height="400" loading="lazy"></p>
<p>现在，打开 Ubuntu 或从 Microsoft Store 安装的任何发行版。执行 <code>docker --version</code> 和 <code>docker-compose --version</code> 命令以确保安装成功。</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2021/01/docker-and-compose-version-on-windows.png" alt="docker-and-compose-version-on-windows" width="600" height="400" loading="lazy"></p>
<p>也可以从常规命令提示符或 PowerShell 访问 Docker，只是我更喜欢使用 WSL2。</p>
<h3 id="linuxdocker">怎样在 Linux 上安装 Docker</h3>
<p>在 Linux 上安装 Docker 的过程有所不同，具体操作取决于你所使用的发行版，它们之间差异可能更大。但老实说，安装与其他两个平台一样容易（如果不能算更容易的话）。</p>
<p>Windows 或 Mac 上的 Docker Desktop 软件包是一系列工具的集合，例如<code>Docker Engine</code>、<code>Docker Compose</code>、<code>Docker Dashboard</code>、<code>Kubernetes</code> 和其他一些好东西。</p>
<p>但是，在 Linux 上，没有得到这样的捆绑包。可以手动安装所需的所有必要工具。 不同发行版的安装过程如下：</p>
<ul>
<li>如果你使用的是 Ubuntu，则可以遵循官方文档中的<a href="https://docs.docker.com/engine/install/ubuntu/">在 Ubuntu 上安装 Docker 引擎</a>部分。</li>
<li>对于其他发行版，官方文档中提供了 <em>不同发行版的安装指南</em>。
<ul>
<li><a href="https://docs.docker.com/engine/install/debian/">在 Debian上安装 Docker Engine</a></li>
<li><a href="https://docs.docker.com/engine/install/fedora/">在 Fedora 上安装 Docker Engine</a></li>
<li><a href="https://docs.docker.com/engine/install/centos/">在 CentOS 上安装 Docker Engine</a></li>
</ul>
</li>
<li>如果你使用的发行版未在文档中列出，则可以参考<a href="https://docs.docker.com/engine/install/binaries/">从二进制文件安装 Docker 引擎</a>指南。</li>
<li>无论参考什么程序，都必须完成一些非常重要的 <a href="https://docs.docker.com/engine/install/linux-postinstall/">Linux 的安装后续步骤</a>。</li>
<li>完成 docker 安装后，必须安装另一个名为 Docker Compose 的工具。 可以参考官方文档中的 <a href="https://docs.docker.com/compose/install/">Install Docker Compose</a> 指南。</li>
</ul>
<p>安装完成后，打开终端并执行 <code>docker --version</code> 和 <code>docker-compose --version</code> 以确保安装成功。</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2021/01/docker-and-compose-version-on-linux.png" alt="docker-and-compose-version-on-linux" width="600" height="400" loading="lazy"></p>
<p>尽管无论使用哪个平台，Docker 的性能都很好，但与其他平台相比，我更喜欢 Linux。在整本书中，我将使用<a href="https://releases.ubuntu.com/20.10/">Ubuntu 20.10</a> 或者 <a href="https://fedoramagazine.org/announcing-fedora-33/">Fedora 33</a>。</p>
<p>一开始就需要阐明的另一件事是，在整本书中，我不会使用任何 GUI 工具操作 Docker。</p>
<p>我在各个平台用过很多不错的 GUI 工具，但是介绍常见的 docker 命令是本书的主要目标之一。</p>
<h2 id="dockerdocker">初识 Docker - 介绍 Docker 基本知识</h2>
<p>已经在计算机上启动并运行了 Docker，现在该运行第一个容器了。打开终端并执行以下命令：</p>
<pre><code>docker run hello-world

# Unable to find image 'hello-world:latest' locally
# latest: Pulling from library/hello-world
# 0e03bdcc26d7: Pull complete 
# Digest: sha256:4cf9c47f86df71d48364001ede3a4fcd85ae80ce02ebad74156906caff5378bc
# Status: Downloaded newer image for hello-world:latest
# 
# Hello from Docker!
# This message shows that your installation appears to be working correctly.
# 
# To generate this message, Docker took the following steps:
#  1. The Docker client contacted the Docker daemon.
#  2. The Docker daemon pulled the "hello-world" image from the Docker Hub.
#     (amd64)
#  3. The Docker daemon created a new container from that image which runs the
#     executable that produces the output you are currently reading.
#  4. The Docker daemon streamed that output to the Docker client, which sent it
#     to your terminal.
#
# To try something more ambitious, you can run an Ubuntu container with:
#  $ docker run -it ubuntu bash
# 
# Share images, automate workflows, and more with a free Docker ID:
#  https://hub.docker.com/
#
# For more examples and ideas, visit:
#  https://docs.docker.com/get-started/
</code></pre>
<p><a href="https://hub.docker.com/_/hello-world">hello-world</a> 镜像是使用 Docker 进行最小化容器化的一个示例。它有一个从 <a href="https://github.com/docker-library/hello-world/blob/master/hello.c">hello.c</a> 文件编译的程序，负责打印出终端看到的消息。</p>
<p>现在，在终端中，可以使用 <code>docker ps -a</code> 命令查看当前正在运行或过去运行的所有容器：</p>
<pre><code>docker ps -a

# CONTAINER ID        IMAGE               COMMAND             CREATED             STATUS                     PORTS               NAMES
# 128ec8ceab71        hello-world         "/hello"            14 seconds ago      Exited (0) 13 seconds ago                      exciting_chebyshev
</code></pre>
<p>在输出中，使用 <code>hello-world</code> 镜像运行了名为 <code>exciting_chebyshev</code> 的容器，其容器标识为 <code>128ec8ceab71</code>。它已经在 <code>Exited (0) 13 seconds ago</code>，其中 <code>(0)</code> 退出代码表示在容器运行时未发生任何错误。</p>
<p>现在，为了了解背后发生的事情，必须熟悉 Docker 体系结构和三个非常基本的容器化概念，如下所示：</p>
<ul>
<li><a href="https://www.freecodecamp.org/news/@fhsinchy/s/the-docker-handbook/~/drafts/-MS1b3opwENd_9qH1jTO/hello-world-in-docker#container">容器</a></li>
<li><a href="https://www.freecodecamp.org/news/@fhsinchy/s/the-docker-handbook/~/drafts/-MS1b3opwENd_9qH1jTO/hello-world-in-docker#image">镜像</a></li>
<li><a href="https://www.freecodecamp.org/news/@fhsinchy/s/the-docker-handbook/~/drafts/-MS1b3opwENd_9qH1jTO/hello-world-in-docker#registry">仓库</a></li>
</ul>
<p>我已经按字母顺序列出了这三个概念，并且将从列表中的第一个开始介绍。</p>
<h3 id="">什么是容器？</h3>
<p>在容器化世界中，没有什么比容器的概念更基础的了。</p>
<p>官方 Docker <a href="https://www.docker.com/resources/what-container">resources</a> 网站说 -</p>
<blockquote>
<p>容器是应用程序层的抽象，可以将代码和依赖项打包在一起。容器不虚拟化整个物理机，仅虚拟化主机操作系统。</p>
</blockquote>
<p>可以认为容器是下一代虚拟机。</p>
<p>就像虚拟机一样，容器是与主机系统是彼此之间完全隔离的环境。它也比传统虚拟机轻量得多，因此可以同时运行大量容器，而不会影响主机系统的性能。</p>
<p>容器和虚拟机实际上是虚拟化物理硬件的不同方法。两者之间的主要区别是虚拟化方式。</p>
<p>虚拟机通常由称为虚拟机监控器的程序创建和管理，例如 <a href="https://www.virtualbox.org/">Oracle VM VirtualBox</a>，<a href="https://www.vmware.com/">VMware Workstation</a>，<a href="https://www.linux-kvm.org/">KVM</a>，<a href="https://docs.microsoft.com/zh-cn/virtualization/hyper-v-on-windows/about/">Microsoft Hyper-V</a> 等等。 该虚拟机监控程序通常位于主机操作系统和虚拟机之间，充当通信介质。</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2021/01/virtual-machines.svg" alt="virtual-machines" width="600" height="400" loading="lazy"></p>
<p>每个虚拟机都有自己的 guest 操作系统，该操作系统与主机操作系统一样消耗资源。</p>
<p>在虚拟机内部运行的应用程序与 guest 操作系统进行通信，该 guest 操作系统在与虚拟机监控器进行通信，后者随后又与主机操作系统进行通信，以将必要的资源从物理基础设施分配给正在运行的应用程序。</p>
<p>虚拟机内部运行的应用程序与物理基础设施之间存在很长的通信链。在虚拟机内部运行的应用程序可能只拥有少量资源，因为 guest 操作系统会占用很大的开销。</p>
<p>与虚拟机不同，容器以更智能的方式完成虚拟化工作。在容器内部没有完整的 guest 操作系统，它只是通过容器运行时使用主机操作系统，同时保持隔离 – 就像传统的虚拟机一样。</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2021/01/containers.svg" alt="containers" width="600" height="400" loading="lazy"></p>
<p>容器运行时（即 Docker）位于容器和主机操作系统之间，而不是虚拟机监控器中。容器与容器运行时进行通信，容器运行时再与主机操作系统进行通信，以从物理基础设施中获取必要的资源。</p>
<p>由于消除了整个主机操作系统层，因此与传统的虚拟机相比，容器的更轻量，资源占用更少。</p>
<p>为了说明这一点，请看下面的代码片段：</p>
<pre><code>uname -a
# Linux alpha-centauri 5.8.0-22-generic #23-Ubuntu SMP Fri Oct 9 00:34:40 UTC 2020 x86_64 x86_64 x86_64 GNU/Linux

docker run alpine uname -a
# Linux f08dbbe9199b 5.8.0-22-generic #23-Ubuntu SMP Fri Oct 9 00:34:40 UTC 2020 x86_64 Linux
</code></pre>
<p>在上面的代码片段中，在主机操作系统上执行了 <code>uname -a</code> 命令以打印出内核详细信息。然后在下一行，我在运行 <a href="https://alpinelinux.org/">Alpine Linux</a> 的容器内执行了相同的命令。</p>
<p>从输出中可以看到，该容器确实正在使用主机操作系统中的内核。这证明了容器虚拟化主机操作系统而不是拥有自己的操作系统这一点。</p>
<p>如果你使用的是 Windows 计算机，则会发现所有容器都使用 WSL2 内核。发生这种情况是因为 WSL2 充当了 Windows 上 Docker 的后端。在 macOS 上，默认后端是在  <a href="https://github.com/moby/hyperkit">HyperKit</a> 虚拟机管理程序上运行的 VM。</p>
<h3 id="docker">什么是 Docker 镜像？</h3>
<p>镜像是分层的自包含文件，充当创建容器的模板。它们就像容器的冻结只读副本。 镜像可以通过仓库进行共享。</p>
<p>过去，不同的容器引擎具有不同的镜像格式。但是后来，<a href="https://opencontainers.org/">开放式容器计划（OCI）</a>定义了容器镜像的标准规范，该规范被主要的容器化引擎所遵循。这意味着使用 Docker 构建的映像可以与 Podman 等其他运行时一起使用，而不会有兼容性问题。</p>
<p>容器只是处于运行状态的镜像。当从互联网上获取镜像并使用该镜像运行容器时，实际上是在先前的只读层之上创建了另一个临时可写层。</p>
<p>在本书的后续部分中，这一概念将变得更加清晰。但就目前而言，请记住，镜像是分层只读文件，其中保留着应用程序所需的状态。</p>
<h3 id="">什么是仓库？</h3>
<p>已经了解了这个难题的两个非常重要的部分，即 <em>Containers</em> 和 <em>Images</em>。最后一个是 <em>Registry</em>。</p>
<p>镜像仓库是一个集中式的位置，可以在其中上传镜像，也可以下载其他人创建的镜像。 <a href="https://hub.docker.com/">Docker Hub</a> 是 Docker 的默认公共仓库。另一个非常流行的镜像仓库是 Red Hat 的 <a href="https://quay.io/">Quay</a>。</p>
<p>在本书中，我将使用 Docker Hub 作为首选仓库。</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2021/01/docker-hub.png" alt="docker-hub" width="600" height="400" loading="lazy"></p>
<p>可以免费在 Docker Hub 上共享任意数量的公共镜像。供世界各地的人们下载免费使用。可在我的个人资料（<a href="https://hub.docker.com/u/fhsinchy">fhsinchy</a>）页面上找到我上传的镜像。</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2021/01/my-images-on-docker-hub.png" alt="my-images-on-docker-hub" width="600" height="400" loading="lazy"></p>
<p>除了 Docker Hub 或 Quay，还可以创建自己的镜像仓库来托管私有镜像。计算机中还运行着一个本地仓库，该仓库缓存从远程仓库提取的镜像。</p>
<h3 id="docker">Docker 架构概述</h3>
<p>既然已经熟悉了有关容器化和 Docker 的大多数基本概念，那么现在是时候了解 Docker 作为软件的架构了。</p>
<p>该引擎包括三个主要组件：</p>
<ol>
<li><strong>Docker 守护程序：</strong> 守护程序（<code>dockerd</code>）是一个始终在后台运行并等待来自客户端的命令的进程。守护程序能够管理各种 Docker 对象。</li>
<li><strong>Docker 客户端：</strong> 客户端（<code>docker</code>）是一个命令行界面程序，主要负责传输用户发出的命令。</li>
<li><strong>REST API：</strong> REST API 充当守护程序和客户端之间的桥梁。使用客户端发出的任何命令都将通过 API 传递，最终到达守护程序。</li>
</ol>
<p>根据官方<a href="https://docs.docker.com/get-started/overview/#docker-architecture">文档</a>,</p>
<blockquote>
<p>“ Docker 使用客户端-服务器体系结构。Docker <em>client</em> 与 Docker <em>daemon</em> 对话，daemon 繁重地构建、运行和分发 Docker 容器”。</p>
</blockquote>
<p>作为用户，通常将使用客户端组件执行命令。然后，客户端使用 REST API 来访问长期运行的守护程序并完成工作。</p>
<h3 id="">全景图</h3>
<p>好吧，说的够多了。 现在是时候了解刚刚学习的所有这些知识如何和谐地工作了。在深入解释运行 <code>docker run hello-world</code> 命令时实际发生的情况之前，看一下下面的图片：</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2021/01/docker-run-hello-world.svg" alt="docker-run-hello-world" width="600" height="400" loading="lazy"></p>
<p>该图像是在官方<a href="https://docs.docker.com/engine/images/architecture.svg">文档</a>中找到的图像的略微修改版本。 执行命令时发生的事件如下：</p>
<ol>
<li>执行 <code>docker run hello-world</code> 命令，其中 <code>hello-world</code> 是镜像的名称。</li>
<li>Docker 客户端访问守护程序，告诉它获取 <code>hello-world</code> 镜像并从中运行一个容器。</li>
<li>Docker 守护程序在本地仓库中查找镜像，并发现它不存在，所以在终端上打印 <code>Unable to find image 'hello-world:latest' locally</code>。</li>
<li>然后，守护程序访问默认的公共仓库 Docker Hub，拉取 <code>hello-world</code> 镜像的最新副本，并在命令行中展示 <code>Unable to find image 'hello-world:latest' locally</code>。</li>
<li>Docker 守护程序根据新拉取的镜像创建一个新容器。</li>
<li>最后，Docker 守护程序运行使用 <code>hello-world</code> 镜像创建的容器，该镜像在终端上输出文本。</li>
</ol>
<p>Docker 守护程序的默认行为是在 hub 中查找本地不存在的镜像。但是，拉取了镜像之后，它将保留在本地缓存中。因此，如果再次执行该命令，则在输出中将看不到以下几行：</p>
<pre><code>Unable to find image 'hello-world:latest' locally
latest: Pulling from library/hello-world
0e03bdcc26d7: Pull complete
Digest: sha256:d58e752213a51785838f9eed2b7a498ffa1cb3aa7f946dda11af39286c3db9a9
Status: Downloaded newer image for hello-world:latest
</code></pre>
<p>如果公共仓库中有可用镜像的更新版本，则守护程序将再次拉取该镜像。那个 <code>:latest</code>  是一个标记。镜像通常包含有意义的标记以指示版本或内部版本。稍后，将更详细地介绍这一点。</p>
<h2 id="docker">Docker 容器操作基础知识</h2>
<p>在前面的部分中，已经了解了 Docker 的构建模块，还使用 <code>docker run</code> 命令运行了一个容器。</p>
<p>在本节中，将详细介绍容器的操作。容器操作是每天要执行的最常见的任务之一，因此，正确理解各种命令至关重要。</p>
<p>但是请记住，这并不是可以在 Docker 上执行的所有命令的详尽列表。我只会介绍最常见的那些。当想知道某一命令的更多用法时，可以访问 Docker 命令行的官方<a href="https://docs.docker.com/engine/reference/commandline/container/">参考</a>。</p>
<h3 id="">怎样运行容器</h3>
<p>之前，已经使用 <code>docker run</code> 来使用 <code>hello-world</code> 镜像创建和启动容器。此命令的通用语法如下：</p>
<pre><code>docker run &lt;image name&gt;
</code></pre>
<p>尽管这是一个完全有效的命令，但是有一种更好的方式可以将命令分配给 <code>docker</code> 守护程序。</p>
<p>在版本 <code>1.13</code> 之前，Docker 仅具有前面提到的命令语法。后来，命令行经过了<a href="https://www.docker.com/blog/whats-new-in-docker-1-13/">重构</a>具有了以下语法：</p>
<pre><code>docker &lt;object&gt; &lt;command&gt; &lt;options&gt;
</code></pre>
<p>使用以下语法：</p>
<ul>
<li><code>object</code> 表示将要操作的 Docker 对象的类型。这可以是 <code>container</code>、<code>image</code>、<code>network</code> 或者 <code>volume</code> 对象。</li>
<li><code>command</code> 表示守护程序要执行的任务，即 <code>run</code> 命令。</li>
<li><code>options</code> 可以是任何可以覆盖命令默认行为的有效参数，例如端口映射的 <code>--publish</code> 选项。</li>
</ul>
<p>现在，遵循此语法，可以将 <code>run</code> 命令编写如下：</p>
<pre><code>docker container run &lt;image name&gt;
</code></pre>
<p><code>image name</code> 可以是在线仓库或本地系统中的任何镜像。例如，可以尝试使用<a href="https://hub.docker.com/r/fhsinchy/hello-dock">fhsinchy / hello-dock</a> 镜像运行容器。 该镜像包含一个简单的 <a href="https://vuejs.org/">Vue.js</a>应用程序，该应用程序在容器内部的端口 80 上运行。</p>
<p>请在终端上执行以下命令以使用此镜像运行容器：</p>
<pre><code>docker container run --publish 8080:80 fhsinchy/hello-dock

# /docker-entrypoint.sh: /docker-entrypoint.d/ is not empty, will attempt to perform configuration
# /docker-entrypoint.sh: Looking for shell scripts in /docker-entrypoint.d/
# /docker-entrypoint.sh: Launching /docker-entrypoint.d/10-listen-on-ipv6-by-default.sh
# 10-listen-on-ipv6-by-default.sh: Getting the checksum of /etc/nginx/conf.d/default.conf
# 10-listen-on-ipv6-by-default.sh: Enabled listen on IPv6 in /etc/nginx/conf.d/default.conf
# /docker-entrypoint.sh: Launching /docker-entrypoint.d/20-envsubst-on-templates.sh
# /docker-entrypoint.sh: Configuration complete; ready for start up
</code></pre>
<p>该命令不言自明。唯一需要说明的部分是 <code>--publish 8080:80</code> 部分，将在下一个小节中进行说明。</p>
<h3 id="">怎样公开端口</h3>
<p>容器是隔离的环境。主机系统对容器内部发生的事情一无所知。因此，从外部无法访问在容器内部运行的应用程序。</p>
<p>要允许从容器外部进行访问，必须将容器内的相应端口发布到本地网络上的端口。<code>--publish</code> 或 <code>-p</code> 选项的通用语法如下：</p>
<pre><code>--publish &lt;host port&gt;:&lt;container port&gt;
</code></pre>
<p>在上一小节中编写了 <code>--publish 8080:80</code> 时，这意味着发送到主机系统端口 8080 的任何请求都将转发到容器内的端口 80。</p>
<p>现在要在浏览器上访问该应用程序，只需访问  <code>http://127.0.0.1:8080</code>。</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2021/01/hello-dock.png" alt="hello-dock" width="600" height="400" loading="lazy"></p>
<p>可以在终端窗口按下 <code>ctrl + c</code> 组合键或关闭终端窗口来停止容器。</p>
<h3 id="">如何使用分离模式</h3>
<p><code>run</code> 命令的另一个非常流行的选项是 <code>---detach</code> 或 <code>-d</code> 选项。 在上面的示例中，为了使容器继续运行，必须将终端窗口保持打开状态。关闭终端窗口会停止正在运行的容器。</p>
<p>这是因为，默认情况下，容器在前台运行，并像从终端调用的任何其他普通程序一样将其自身附加到终端。</p>
<p>为了覆盖此行为并保持容器在后台运行，可以在 <code>run</code> 命令中包含 <code>--detach</code> 选项，如下所示：</p>
<pre><code>docker container run --detach --publish 8080:80 fhsinchy/hello-dock

# 9f21cb77705810797c4b847dbd330d9c732ffddba14fb435470567a7a3f46cdc
</code></pre>
<p>与前面的示例不同，这次不会看到很多文字，而只获得新创建的容器的 ID。</p>
<p>提供选项的顺序并不重要。 如果将 <code>--publish</code> 选项放在 <code>--detach</code> 选项之前，效果相同。</p>
<p>使用 <code>run</code> 命令时必须记住的一件事是镜像名称必须最后出现。如果在镜像名称后放置任何内容，则将其作为参数传递给容器入口点（在<a href="https://www.freecodecamp.org/news/the-docker-handbook/#executing-commands-inside-a-container">在容器内执行命令</a>小节做了解释），可能会导致意外情况。</p>
<h3 id="">怎样列表展示容器</h3>
<p><code>container ls</code> 命令可用于列出当前正在运行的容器。执行以下命令：</p>
<pre><code>docker container ls

# CONTAINER ID        IMAGE                 COMMAND                  CREATED             STATUS              PORTS                  NAMES
# 9f21cb777058        fhsinchy/hello-dock   "/docker-entrypoint.…"   5 seconds ago       Up 5 seconds        0.0.0.0:8080-&gt;80/tcp   gifted_sammet
</code></pre>
<p>一个名为 <code>gifted_sammet</code> 的容器正在运行。它是在 <code>5 seconds ago</code> 前创建的，状态为 <code>Up 5 seconds</code>，这表明自创建以来，该容器一直运行良好。</p>
<p><code>CONTAINER ID</code> 为 <code>9f21cb777058</code>，这是完整容器 ID 的前 12 个字符。完整的容器 ID 是 <code>9f21cb77705810797c4b847dbd330d9c732ffddba14fb435470567a7a3f46cdc</code>，该字符长 64 个字符。在上一节中 <code>docker container run</code>  命令行的输的就是完整的容器 ID 。</p>
<p>列表的 <code>PORTS</code> 列下，本地网络的端口 8080 指向容器内的端口 80。name <code>gifted_sammet</code> 是由 Docker 生成的，可能与你的计算机的不同。</p>
<p><code>container ls</code> 命令仅列出系统上当前正在运行的容器。为了列出过去运行的所有容器，可以使用 <code>--all</code> 或 <code>-a</code> 选项。</p>
<pre><code>docker container ls --all

# CONTAINER ID        IMAGE                 COMMAND                  CREATED             STATUS                     PORTS                  NAMES
# 9f21cb777058        fhsinchy/hello-dock   "/docker-entrypoint.…"   2 minutes ago       Up 2 minutes               0.0.0.0:8080-&gt;80/tcp   gifted_sammet
# 6cf52771dde1        fhsinchy/hello-dock   "/docker-entrypoint.…"   3 minutes ago       Exited (0) 3 minutes ago                          reverent_torvalds
# 128ec8ceab71        hello-world           "/hello"                 4 minutes ago       Exited (0) 4 minutes ago                          exciting_chebyshev
</code></pre>
<p>如你所见，列表 <code>reverent_torvalds</code> 中的第二个容器是较早创建的，并以状态代码 0 退出，这表明在容器运行期间未产生任何错误。</p>
<h3 id="">怎样命名或者重命名一个容器</h3>
<p>默认情况下，每个容器都有两个标识符。 如下：</p>
<ul>
<li><code>CONTAINER ID</code> - 64 个字符的随机字符串。</li>
<li><code>NAME</code> - 两个随机词的组合，下划线连接。</li>
</ul>
<p>基于这两个随机标识符来引用容器非常不方便。如果可以使用自定义的名称来引用容器，那就太好了。</p>
<p>可以使用 <code>--name</code> 选项来命名容器。要使用名为 <code>hello-dock-container</code> 的 <code> fhsinchy/hello-dock</code> 镜像运行另一个容器，可以执行以下命令：</p>
<pre><code>docker container run --detach --publish 8888:80 --name hello-dock-container fhsinchy/hello-dock

# b1db06e400c4c5e81a93a64d30acc1bf821bed63af36cab5cdb95d25e114f5fb
</code></pre>
<p>本地网络上的 8080 端口被 <code>gifted_sammet</code> 容器（在上一小节中创建的容器）占用了。这就是为什么必须使用其他端口号（例如 8888）的原因。要进行验证，执行 <code> container ls</code> 命令：</p>
<pre><code>docker container ls

# CONTAINER ID        IMAGE                 COMMAND                  CREATED             STATUS              PORTS                  NAMES
# b1db06e400c4        fhsinchy/hello-dock   "/docker-entrypoint.…"   28 seconds ago      Up 26 seconds       0.0.0.0:8888-&gt;80/tcp   hello-dock-container
# 9f21cb777058        fhsinchy/hello-dock   "/docker-entrypoint.…"   4 minutes ago       Up 4 minutes        0.0.0.0:8080-&gt;80/tcp   gifted_sammet
</code></pre>
<p>一个名为 <code>hello-dock-container</code> 的新容器已经启动。</p>
<p>甚至可以使用 <code>container rename</code> 命令来重命名旧容器。该命令的语法如下：</p>
<pre><code>docker container rename &lt;container identifier&gt; &lt;new name&gt;
</code></pre>
<p>要将 <code>gifted_sammet</code> 容器重命名为 <code>hello-dock-container-2</code>，可以执行以下命令：</p>
<pre><code>docker container rename gifted_sammet hello-dock-container-2
</code></pre>
<p>该命令不会产生任何输出，但是可以使用 <code>container ls</code> 命令来验证是否已进行更改。 <code>rename</code> 命令不仅适用于处于运行状态的容器和还适用于处于停止状态的容器。</p>
<h3 id="">怎样停止或者杀死运行中的容器</h3>
<p>可以通过简单地关闭终端窗口或单击 <code>ctrl + c</code> 来停止在前台运行的容器。但是，不能以相同方式停止在后台运行的容器。</p>
<p>有两个命令可以完成此任务。 第一个是 <code>container stop</code> 命令。该命令的通用语法如下：</p>
<pre><code>docker container stop &lt;container identifier&gt;
</code></pre>
<p>其中 <code>container identifier</code> 可以是容器的 ID 或名称。</p>
<p>应该还记得上一节中启动的容器。它仍在后台运行。使用 <code>docker container ls</code> 获取该容器的标识符（在本演示中，我将使用 <code>hello-dock-container</code> 容器）。现在执行以下命令来停止容器：</p>
<pre><code>docker container stop hello-dock-container

# hello-dock-container
</code></pre>
<p>如果使用 name 作为标识符，则 name 将作为输出返回。<code>stop</code> 命令通过发送信号<code>SIGTERM</code> 来正常关闭容器。如果容器在一定时间内没有停止运行，则会发出 <code>SIGKILL</code> 信号，该信号会立即关闭容器。</p>
<p>如果要发送 <code>SIGKILL</code> 信号而不是 <code>SIGTERM</code> 信号，则可以改用 <code>container kill</code> 命令。<code>container kill</code> 命令遵循与 <code>stop</code> 命令相同的语法。</p>
<pre><code>docker container kill hello-dock-container-2

# hello-dock-container-2
</code></pre>
<h3 id="">怎样重新启动容器</h3>
<p>当我说重启时，我指的如下是两种情况：</p>
<ul>
<li>重新启动先前已停止或终止的容器。</li>
<li>重新启动正在运行的容器。</li>
</ul>
<p>正如上一小节中学到的，停止的容器保留在系统中。如果需要，可以重新启动它们。<code>container start</code>  命令可用于启动任何已停止或终止的容器。该命令的语法如下：</p>
<pre><code>docker container start &lt;container identifier&gt;
</code></pre>
<p>可以通过执行 <code>container ls --all</code> 命令来获取所有容器的列表，然后寻找状态为 <code>Exited</code> 的容器。</p>
<pre><code>docker container ls --all

# CONTAINER ID        IMAGE                 COMMAND                  CREATED             STATUS                        PORTS               NAMES
# b1db06e400c4        fhsinchy/hello-dock   "/docker-entrypoint.…"   3 minutes ago       Exited (0) 47 seconds ago                         hello-dock-container
# 9f21cb777058        fhsinchy/hello-dock   "/docker-entrypoint.…"   7 minutes ago       Exited (137) 17 seconds ago                       hello-dock-container-2
# 6cf52771dde1        fhsinchy/hello-dock   "/docker-entrypoint.…"   7 minutes ago       Exited (0) 7 minutes ago                          reverent_torvalds
# 128ec8ceab71        hello-world           "/hello"                 9 minutes ago       Exited (0) 9 minutes ago                          exciting_chebyshev
</code></pre>
<p>现在要重新启动 <code>hello-dock-container</code> 容器，可以执行以下命令：</p>
<pre><code>docker container start hello-dock-container

# hello-dock-container
</code></pre>
<p>现在，可以使用 <code>container ls</code> 命令查看正在运行的容器列表，以确保该容器正在运行。</p>
<p>默认情况下，<code>container start</code> 命令以分离模式启动容器，并保留之前进行的端口配置。因此，如果现在访问 <code>http：//127.0.0.1：8080</code>，应该能够像以前一样访问 <code>hello-dock</code> 应用程序。</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2021/01/hello-dock.png" alt="hello-dock" width="600" height="400" loading="lazy"></p>
<p>现在，在想重新启动正在运行的容器，可以使用 <code>container restart</code> 命令。<code>container restart</code> 命令遵循与 <code>container start</code> 命令完全相同的语法。</p>
<pre><code>docker container restart hello-dock-container-2

# hello-dock-container-2
</code></pre>
<p>这两个命令之间的主要区别在于，<code>container restart</code> 命令尝试停止目标容器，然后再次启动它，而 start 命令只是启动一个已经停止的容器。</p>
<p>在容器停止的情况下，两个命令完全相同。但是如果容器正在运行，则必须使用<code>container restart</code> 命令。</p>
<h3 id="">怎样创建而不运行容器</h3>
<p>到目前为止，在本节中，已经使用 <code>container run</code> 命令启动了容器，该命令实际上是两个单独命令的组合。这两个命令如下：</p>
<ul>
<li><code>container create</code> 命令从给定的镜像创建一个容器。</li>
<li><code>container start</code> 命令将启动一个已经创建的容器。</li>
</ul>
<p>现在，要使用这两个命令执行<a href="https://www.freecodecamp.org/news/the-docker-handbook/#running-containers">运行容器</a>部分中显示的演示，可以执行以下操作：</p>
<pre><code>docker container create --publish 8080:80 fhsinchy/hello-dock

# 2e7ef5098bab92f4536eb9a372d9b99ed852a9a816c341127399f51a6d053856

docker container ls --all

# CONTAINER ID        IMAGE                 COMMAND                  CREATED             STATUS              PORTS               NAMES
# 2e7ef5098bab        fhsinchy/hello-dock   "/docker-entrypoint.…"   30 seconds ago      Created                                 hello-dock
</code></pre>
<p>通过 <code>container ls --all</code> 命令的输出可以明显看出，已经使用 <code>fhsinchy/hello-dock</code> 镜像创建了一个名称为 <code>hello-dock</code> 的容器。 容器的 <code>STATUS</code> 目前处于 <code>Created</code> 状态，并且鉴于其未运行，因此不使用  <code>--all</code> 选项就不会列出该容器。</p>
<p>一旦创建了容器，就可以使用 <code>container start</code> 命令来启动它。</p>
<pre><code>docker container start hello-dock

# hello-dock

docker container ls

# CONTAINER ID        IMAGE                 COMMAND                  CREATED              STATUS              PORTS                  NAMES
# 2e7ef5098bab        fhsinchy/hello-dock   "/docker-entrypoint.…"   About a minute ago   Up 29 seconds       0.0.0.0:8080-&gt;80/tcp   hello-dock
</code></pre>
<p>容器 <code>STATUS</code> 已从 <code>Created</code> 更改为 <code>Up 29 seconds</code>，这表明容器现在处于运行状态。端口配置也显示在以前为空的 <code>PORTS</code> 列中。</p>
<p>尽管可以在大多数情况下使用 <code>container run</code> 命令，但本书稍后还会有一些情况要求使用 <code>container create</code> 命令。</p>
<h3 id="">怎样移除挂起的容器</h3>
<p>如你所见，已被停止或终止的容器仍保留在系统中。这些挂起的容器可能会占用空间或与较新的容器发生冲突。</p>
<p>可以使用 <code>container rm</code> 命令删除停止的容器。 通用语法如下：</p>
<pre><code>docker container rm &lt;container identifier&gt;
</code></pre>
<p>要找出哪些容器没有运行，使用 <code>container ls --all</code> 命令并查找状态为 <code>Exited</code> 的容器。</p>
<pre><code>docker container ls --all

# CONTAINER ID        IMAGE                 COMMAND                  CREATED             STATUS                      PORTS                  NAMES
# b1db06e400c4        fhsinchy/hello-dock   "/docker-entrypoint.…"   6 minutes ago       Up About a minute           0.0.0.0:8888-&gt;80/tcp   hello-dock-container
# 9f21cb777058        fhsinchy/hello-dock   "/docker-entrypoint.…"   10 minutes ago      Up About a minute           0.0.0.0:8080-&gt;80/tcp   hello-dock-container-2
# 6cf52771dde1        fhsinchy/hello-dock   "/docker-entrypoint.…"   10 minutes ago      Exited (0) 10 minutes ago                          reverent_torvalds
# 128ec8ceab71        hello-world           "/hello"                 12 minutes ago      Exited (0) 12 minutes ago                          exciting_chebyshev
</code></pre>
<p>从输出中可以看到，ID为 <code>6cf52771dde1</code> 和 <code>128ec8ceab71</code> 的容器未运行。要删除 <code>6cf52771dde1</code>，可以执行以下命令：</p>
<pre><code>docker container rm 6cf52771dde1

# 6cf52771dde1
</code></pre>
<p>可以使用 <code>container ls</code> 命令检查容器是否被删除。也可以一次删除多个容器，方法是将其标识符一个接一个地传递，每个标识符之间用空格隔开。</p>
<p>也可以使用 <code>container prune</code> 命令来一次性删除所有挂起的容器。</p>
<p>可以使用 <code>container ls --all</code> 命令检查容器列表，以确保已删除了挂起的容器：</p>
<pre><code>docker container ls --all

# CONTAINER ID        IMAGE                 COMMAND                  CREATED             STATUS              PORTS                  NAMES
# b1db06e400c4        fhsinchy/hello-dock   "/docker-entrypoint.…"   8 minutes ago       Up 3 minutes        0.0.0.0:8888-&gt;80/tcp   hello-dock-container
# 9f21cb777058        fhsinchy/hello-dock   "/docker-entrypoint.…"   12 minutes ago      Up 3 minutes        0.0.0.0:8080-&gt;80/tcp   hello-dock-container-2
</code></pre>
<p>如果按照本书的顺序进行操作，则应该只在列表中看到 <code>hello-dock-container</code> 和 <code>hello-dock-container-2</code>。 建议停止并删除两个容器，然后再继续进行下一部分。</p>
<p><code>container run</code> 和 <code>container start</code> 命令还有 <code>--rm</code> 选项，它们表示希望容器在停止后立即被移除。 执行以下命令，使用 <code>--rm</code> 选项启动另一个 <code>hello-dock</code> 容器：</p>
<pre><code>docker container run --rm --detach --publish 8888:80 --name hello-dock-volatile fhsinchy/hello-dock

# 0d74e14091dc6262732bee226d95702c21894678efb4043663f7911c53fb79f3
</code></pre>
<p>可以使用 <code>container ls</code> 命令来验证容器是否正在运行：</p>
<pre><code>docker container ls

# CONTAINER ID   IMAGE                 COMMAND                  CREATED              STATUS              PORTS                  NAMES
# 0d74e14091dc   fhsinchy/hello-dock   "/docker-entrypoint.…"   About a minute ago   Up About a minute   0.0.0.0:8888-&gt;80/tcp   hello-dock-volatile
</code></pre>
<p>现在，如果停止了容器，使用 <code>container ls --all</code> 命令再次检查：</p>
<pre><code>docker container stop hello-dock-volatile

# hello-dock-volatile

docker container ls --all

# CONTAINER ID   IMAGE     COMMAND   CREATED   STATUS    PORTS     NAMES
</code></pre>
<p>该容器已被自动删除。从现在开始，我将对大多数容器使用 <code>--rm</code> 选项。不需要的地方我会明确提到。</p>
<h3 id="">怎样以交互式模式运行容器</h3>
<p>到目前为止，只运行了 <a href="https://hub.docker.com/_/hello-world">hello-world</a> 镜像或 <a href="https://chinese.freecodecamp.org/news/the-docker-handbook/https/hub.docker.com/r/fhsinchy/hello-dock">fhsinchy/hello-dock</a> 镜像。这些镜像用于执行非交互式的简单程序。</p>
<p>好吧，镜像并不是那么简单。镜像可以将整个 Linux 发行版封装在其中。</p>
<p>流行的发行版，例如 <a href="https://ubuntu.com/">Ubuntu</a>，<a href="https://fedora.org/">Fedora</a> 和 <a href="https://debian.org/">Debian</a> 都在 hub 有官方的 Docker 镜像。编程语言，例如 <a href="https://hub.docker.com/_/python">python</a>、<a href="https://hub.docker.com/_/php">php</a>、[go](https：// hub.docker.com/_/golang) 或类似 <a href="https://hub.docker.com/_/node">node</a> 和 <a href="https://hub.docker.com/r/hayd/deno">deno</a> 都有其官方镜像。</p>
<p>这些镜像不但仅运行某些预配置的程序。还将它们配置为默认情况下运行的 shell 程序。在镜像是操作系统的情况下，它可以是诸如 <code>sh</code> 或 <code>bash</code> 之类的东西，在镜像是编程语言或运行时的情况下，通常是它们的默认语言的 shell。</p>
<p>正如可能从以前的计算机中学到的一样，shell 是交互式程序。被配置为运行这样的程序的镜像是交互式镜像。这些镜像需要在  <code>container run</code>  命令中传递特殊的 <code>-it</code> 选项。</p>
<p>例如，如果通过执行 <code>docker container run ubuntu</code> 使用 <code>ubuntu</code> 镜像运行一个容器，将不会发生任何事情。但是，如果使用 <code>-it</code> 选项执行相同的命令，会直接进入到 Ubuntu 容器内的 bash 上。</p>
<pre><code>docker container run --rm -it ubuntu

# root@dbb1f56b9563:/# cat /etc/os-release
# NAME="Ubuntu"
# VERSION="20.04.1 LTS (Focal Fossa)"
# ID=ubuntu
# ID_LIKE=debian
# PRETTY_NAME="Ubuntu 20.04.1 LTS"
# VERSION_ID="20.04"
# HOME_URL="https://www.ubuntu.com/"
# SUPPORT_URL="https://help.ubuntu.com/"
# BUG_REPORT_URL="https://bugs.launchpad.net/ubuntu/"
# PRIVACY_POLICY_URL="https://www.ubuntu.com/legal/terms-and-policies/privacy-policy"
# VERSION_CODENAME=focal
# UBUNTU_CODENAME=focal
</code></pre>
<p>从  <code>cat /etc/os-release</code>  命令的输出中可以看到，我确实正在与在 Ubuntu 容器中运行的 bash 进行交互。</p>
<p><code>-it</code> 选项提供了与容器内的程序进行交互的场景。此选项实际上是将两个单独的选项混在一起。</p>
<ul>
<li>选项  <code>-i</code> 或  <code>--interactive</code> 连接到容器的输入流，以便可以将输入发送到 bash。</li>
<li><code>-t</code> 或 <code>--tty</code>  选项可通过分配伪 tty 来格式化展示并提供类似本机终端的体验。</li>
</ul>
<p>当想以交互方式运行容器时，可以使用 <code>-it</code> 选项。以交互式方式运行 <code>node</code> 镜像，如下：</p>
<pre><code>docker container run -it node

# Welcome to Node.js v15.0.0.
# Type ".help" for more information.
# &gt; ['farhan', 'hasin', 'chowdhury'].map(name =&gt; name.toUpperCase())
# [ 'FARHAN', 'HASIN', 'CHOWDHURY' ]
</code></pre>
<p>任何有效的 JavaScript 代码都可以在 node shell 中执行。除了输入 <code>-it</code>，还可以输入 <code>--interactive --tty</code>，效果一样，只不过更冗长。</p>
<h3 id="">怎样在容器里执行命令</h3>
<p>在本书中<a href="https://www.freecodecamp.org/news/@fhsinchy/s/the-docker-handbook/~/drafts/-MS1b3opwENd_9qH1jTO/hello-world-in-docker">初识 Docker</a> 部分中，已经了解了在 Alpine Linux 容器内执行命令。 它是这样的：</p>
<pre><code>docker run alpine uname -a
# Linux f08dbbe9199b 5.8.0-22-generic #23-Ubuntu SMP Fri Oct 9 00:34:40 UTC 2020 x86_64 Linux
</code></pre>
<p>在此命令中，在 Alpine Linux 容器中执行了 <code>uname -a</code> 命令。像这样的场景（要做的就是在特定的容器内执行特定的命令）非常常见。</p>
<p>假设想使用 <code>base64</code> 程序对字符串进行编码。几乎所有基于 Linux 或 Unix 的操作系统都可以使用此功能（但 Windows 则不可用）。</p>
<p>在这种情况下，可以使用 <a href="https://hub.docker.com/_/busybox">busybox</a> 之类的镜像快速启动容器，然后执行命令。</p>
<p>使用 <code>base64</code> 编码字符串的通用语法如下：</p>
<pre><code>echo -n my-secret | base64

# bXktc2VjcmV0
</code></pre>
<p>将命令传递到未运行的容器的通用语法如下：</p>
<pre><code>docker container run &lt;image name&gt; &lt;command&gt;
</code></pre>
<p>要使用 busybox 镜像执行 base64  编码，可以执行以下命令：</p>
<pre><code>docker container run --rm busybox echo -n my-secret | base64

# bXktc2VjcmV0
</code></pre>
<p>这里发生的是，在 <code>container run</code> 命令中，镜像名称后传递的任何内容都将传递到镜像的默认入口里。</p>
<p>入口点就像是通往镜像的网关。除可执行镜像外的大多数镜像（在<a href="https://www.freecodecamp.org/news/@fhsinchy/s/the-docker-handbook/~/drafts/-MS1b3opwENd_9qH1jTO/container-manipulation-basics#working-with-executable-images">使用可执行镜像</a>小节中说明）使用 shell 或 <code>sh</code> 作为默认入口点。因此，任何有效的 shell 命令都可以作为参数传递给它们。</p>
<h3 id="">如何处理可执行镜像</h3>
<p>在上一节中，我简要提到了可执行镜像。这些镜像旨在表现得像可执行程序。</p>
<p>以的 <a href="https://github.com/fhsinchy/rmbyext">rmbyext</a> 项目为例。这是一个简单的 Python 脚本，能够递归删除给定扩展名的文件。 要了解有关该项目的更多信息，可以查看<a href="https://github.com/fhsinchy/rmbyext">仓库</a>。</p>
<p>如果同时安装了 Git 和 Python，则可以通过执行以下命令来安装此脚本：</p>
<pre><code>pip install git+https://github.com/fhsinchy/rmbyext.git#egg=rmbyext
</code></pre>
<p>假设的系统上已经正确设置了 Python，则该脚本应该可以在终端的任何位置使用。使用此脚本的通用语法如下：</p>
<pre><code>rmbyext &lt;file extension&gt;
</code></pre>
<p>要对其进行测试，请在一个空目录下打开终端，并在其中创建具有不同扩展名的一些文件。可以使用 <code>touch</code> 命令来做到这一点。现在，计算机上有一个包含以下文件的目录：</p>
<pre><code>touch a.pdf b.pdf c.txt d.pdf e.txt

ls

# a.pdf  b.pdf  c.txt  d.pdf  e.txt
</code></pre>
<p>要从该目录删除所有 <code>pdf</code> 文件，可以执行以下命令：</p>
<pre><code>rmbyext pdf

# Removing: PDF
# b.pdf
# a.pdf
# d.pdf
</code></pre>
<p>该程序的可执行镜像能够将文件扩展名用作参数，并像 <code>rmbyext</code> 程序一样删除它们。</p>
<p><a href="https://hub.docker.com/r/fhsinchy/rmbyext">fhsinchy/rmbyext</a> 镜像的行为类似。该镜像包含 <code>rmbyext</code> 脚本的副本，并配置为在容器内的目录 <code>/zone</code>上运行该脚本。</p>
<p>现在的问题是容器与本地系统隔离，因此在容器内运行的 <code>rmbyext</code> 程序无法访问本地文件系统。因此，如果可以通过某种方式将包含 pdf 文件的本地目录映射到容器内的 <code>/zone</code> 目录，则容器应该可以访问这些文件。</p>
<p>授予容器直接访问本地文件系统的一种方法是使用<a href="https://docs.docker.com/storage/bind-mounts/">绑定挂载</a>。</p>
<p>绑定挂载可以在本地文件系统目录（源）与容器内另一个目录（目标）之间形成双向数据绑定。这样，在目标目录中进行的任何更改都将在源目录上生效，反之亦然。</p>
<p>让我们看一下绑定挂载的实际应用。要使用此镜像而不是程序本身删除文件，可以执行以下命令：</p>
<pre><code>docker container run --rm -v $(pwd):/zone fhsinchy/rmbyext pdf

# Removing: PDF
# b.pdf
# a.pdf
# d.pdf
</code></pre>
<p>已经在命令中看到了 <code>-v $(pwd):/zone</code>  部分，你可能已经猜到了 <code>-v</code>  或 <code>--volume</code> 选项用于为容器创建绑定挂载。该选项可以使用三个以冒号（<code>:</code>）分隔的字段。该选项的通用语法如下：</p>
<pre><code>--volume &lt;local file system directory absolute path&gt;:&lt;container file system directory absolute path&gt;:&lt;read write access&gt;
</code></pre>
<p>第三个字段是可选的，但必须传递本地目录的绝对路径和容器内目录的绝对路径。</p>
<p>在这里，源目录是 <code>/home/fhsinchy/the-zone</code>。假设终端当前在目录中，则 <code>$(pwd)</code> 将替换为包含先前提到的 <code>.pdf</code> 和 <code>.txt</code> 文件的 <code>/home/fhsinchy/the-zone</code>。</p>
<p>可以在<a href="https://www.gnu.org/software/bash/manual/html_node/Command-Substitution.html">command substitution here</a> 上了解更多信息。</p>
<p><code>--volume</code> 或  <code>-v</code>  选项对 <code>container run</code> 以及 <code>container create</code> 命令均有效。我们将在接下来的部分中更详细地探讨卷，因此，如果在这里不太了解它们，请不要担心。</p>
<p>常规镜像和可执行镜像之间的区别在于，可执行镜像的入口点设置为自定义程序而不是 <code>sh</code>，在本例中为 <code>rmbyext</code> 程序。正如在上一小节中所学到的那样，在 <code>container run</code> 命令中在镜像名称之后编写的所有内容都将传递到镜像的入口点。</p>
<p>所以最后，<code>docker container run --rm -v $(pwd):/zone fhsinchy/rmbyext pdf</code> 命令转换为容器内的  <code>rmbyext pdf</code> 。可执行镜像并不常见，但在某些情况下可能非常有用。</p>
<h2 id="docker">Docker 镜像操作基础知识</h2>
<p>现在，已经对如何使用公开可用的镜像运行容器有了深入的了解，是时候学习如何创建自己的镜像了。</p>
<p>在本部分中，将学习创建镜像，使用镜像运行容器以及在线共享镜像的基础知识。</p>
<p>我建议在 <a href="https://marketplace.visualstudio.com/items?itemName=ms-azuretools.vscode-docker">Visual Studio Code</a> 中安装官方的 <a href="https://marketplace.visualstudio.com/items?itemName=ms-azuretools.vscode">Docker Extension</a> 。 这将提升开发效率。</p>
<h3 id="docker">怎样创建  Docker 镜像</h3>
<p>正如我在<a href="https://www.freecodecamp.org/news/the-docker-handbook/#image">初识 Docker</a> 部分中已经解释的那样，镜像是分层的自包含文件，它们充当用于创建 Docker 容器的模板。它们就像是容器的冻结的只读副本。</p>
<p>为了使用程序创建镜像，必须对要从镜像中获得什么有清晰的认识。以官方 <a href="https://hub.docker.com/_/nginx">nginx</a> 镜像为例。只需执行以下命令即可使用该镜像启动容器：</p>
<pre><code>docker container run --rm --detach --name default-nginx --publish 8080:80 nginx

# b379ecd5b6b9ae27c144e4fa12bdc5d0635543666f75c14039eea8d5f38e3f56

docker container ls

# CONTAINER ID        IMAGE               COMMAND                  CREATED             STATUS              PORTS                  NAMES
# b379ecd5b6b9        nginx               "/docker-entrypoint.…"   8 seconds ago       Up 8 seconds        0.0.0.0:8080-&gt;80/tcp   default-nginx
</code></pre>
<p>现在，如果在浏览器中访问 <code>http://127.0.0.1:8080</code>，则会看到一个默认的响应页面。<br>
<img src="https://www.freecodecamp.org/news/content/images/2021/01/nginx-default.png" alt="nginx-default" width="600" height="400" loading="lazy"></p>
<p>看起来不错，但是，如果想制作一个自定义的 NGINX 镜像，该镜像的功能与正式镜像完全一样，但是由你自己创建，可行吗？老实说，完全可行。实际上，只需这样做。</p>
<p>为了制作自定义的 NGINX 镜像，必须清楚了解镜像的最终状态。我认为镜像应如下所示：</p>
<ul>
<li>该镜像应预安装 NGINX，可以使用程序包管理器完成该镜像，也可以从源代码构建该镜像。</li>
<li>该镜像在运行时应自动启动 NGINX。</li>
</ul>
<p>很简单，如果你已经克隆了本书中链接的项目仓库，请进入项目根目录并在其中查找名为 <code>custom-nginx</code> 的目录。</p>
<p>现在，在该目录中创建一个名为 <code>Dockerfile</code> 的新文件。<code>Dockerfile</code> 是指令的集合，该指令会被守护程序生成镜像。<code>Dockerfile</code> 的内容如下：</p>
<pre><code>FROM ubuntu:latest

EXPOSE 80

RUN apt-get update &amp;&amp; \
    apt-get install nginx -y &amp;&amp; \
    apt-get clean &amp;&amp; rm -rf /var/lib/apt/lists/*

CMD ["nginx", "-g", "daemon off;"]
</code></pre>
<p>镜像是多层文件，在此文件中，编写的每一行（称为说明）都会为镜像创建一个层。</p>
<ul>
<li>每个有效的 <code>Dockerfile</code> 均以 <code>FROM</code>  指令开头。该指令为生成的镜像设置基本镜像。通过在此处将 <code>ubuntu：latest</code> 设置为基本镜像，可以在自定义镜像中使用 Ubuntu 的所有功能，因此可以使用 <code>apt-get</code> 命令之类的东西来轻松安装软件包。</li>
<li><code>EXPOSE</code> 指令表示需要发布的端口。使用此指令并不意味着不需要  <code>--publish</code>  端口。仍然需要显式使用 <code>--publish</code> 选项。该 <code>EXPOSE</code> 指令的工作原理类似于文档，适用于试图使用你的镜像运行容器的人员。它还有一些其他用途，我将不在这里讨论。</li>
<li><code>Dockerfile</code> 中的 <code>RUN</code> 指令在容器 shell 内部执行命令。<code>apt-get update &amp;&amp; apt-get install nginx -y</code> 命令检查更新的软件包版本并安装 NGINX。<code>apt-get clean &amp;&amp; rm -rf /var/lib/apt/lists/*</code> 命令用于清除程序包缓存，因为不希望镜像中出现任何不必要的文件。这两个命令是简单的 Ubuntu 东西，没什么特别的。此处的 <code>RUN</code> 指令以 <code>shell</code> 形式编写。这些也可以以 <code>exec</code> 形式编写。 可以查阅<a href="https://docs.docker.com/engine/reference/builder/#run">官方参考</a>了解更多信息。</li>
<li>最后，<code>CMD</code>  指令为镜像设置了默认命令。该指令以 <code>exec</code> 形式编写，此处包含三个独立的部分。这里，<code>nginx</code>  是指 NGINX 可执行文件。 <code>-g</code> 和 <code>daemon off</code> 是 NGINX 的选项。 在容器内将 NGINX 作为单个进程运行是一种最佳实践，因此请使用此选项。<code>CMD</code> 指令也可以以 <code>shell</code> 形式编写。 可以查阅<a href="https://docs.docker.com/engine/reference/builder/#cmd">官方参考</a>了解更多信息。</li>
</ul>
<p>既然具有有效的 <code>Dockerfile</code>，可以从中构建镜像。就像与容器相关的命令一样，可以使用以下语法来执行与镜像相关的命令：</p>
<pre><code>docker image &lt;command&gt; &lt;options&gt;
</code></pre>
<p>要使用刚刚编写的  <code>Dockerfile</code> 构建镜像，请在 <code>custom-nginx</code> 目录中打开终端并执行以下命令：</p>
<pre><code>docker image build .

# Sending build context to Docker daemon  3.584kB
# Step 1/4 : FROM ubuntu:latest
#  ---&gt; d70eaf7277ea
# Step 2/4 : EXPOSE 80
#  ---&gt; Running in 9eae86582ec7
# Removing intermediate container 9eae86582ec7
#  ---&gt; 8235bd799a56
# Step 3/4 : RUN apt-get update &amp;&amp;     apt-get install nginx -y &amp;&amp;     apt-get clean &amp;&amp; rm -rf /var/lib/apt/lists/*
#  ---&gt; Running in a44725cbb3fa
### LONG INSTALLATION STUFF GOES HERE ###
# Removing intermediate container a44725cbb3fa
#  ---&gt; 3066bd20292d
# Step 4/4 : CMD ["nginx", "-g", "daemon off;"]
#  ---&gt; Running in 4792e4691660
# Removing intermediate container 4792e4691660
#  ---&gt; 3199372aa3fc
# Successfully built 3199372aa3fc
</code></pre>
<p>为了执行镜像构建，守护程序需要两条非常具体的信息。Dockerfile 的名称和构建上下文。在上面执行的命令中：</p>
<ul>
<li><code>docker image build</code> 是用于构建镜像的命令。守护程序在上下文中找到任何名为 Dockerfile 的文件。</li>
<li>最后的 <code>.</code>  设置了此构建的上下文。上下文是指在构建过程中守护程序可以访问的目录。</li>
</ul>
<p>现在要使用此镜像运行容器，可以将 <code>container run</code> 命令与在构建过程中收到的镜像 ID 结合使用。在我这里，通过上一个代码块中的 <code>Successfully built 3199372aa3fc</code> 行可以看到 id 为 <code>3199372aa3fc</code>。</p>
<pre><code>docker container run --rm --detach --name custom-nginx-packaged --publish 8080:80 3199372aa3fc

# ec09d4e1f70c903c3b954c8d7958421cdd1ae3d079b57f929e44131fbf8069a0

docker container ls

# CONTAINER ID        IMAGE               COMMAND                  CREATED             STATUS              PORTS                  NAMES
# ec09d4e1f70c        3199372aa3fc        "nginx -g 'daemon of…"   23 seconds ago      Up 22 seconds       0.0.0.0:8080-&gt;80/tcp   custom-nginx-packaged
</code></pre>
<p>要进行验证，请访问 <code>http://127.0.0.1:8080</code> ，应该会看到默认的响应页面。</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2021/01/nginx-default.png" alt="nginx-default" width="600" height="400" loading="lazy"></p>
<h3 id="docker">如何标记 Docker 镜像</h3>
<p>就像容器一样，可以为镜像分配自定义标识符，而不必依赖于随机生成的 ID。如果是镜像，则称为标记而不是命名。在这种情况下，使用 <code>--tag</code> 或  <code>-t</code> 选项。</p>
<p>该选项的通用语法如下：</p>
<pre><code>--tag &lt;image repository&gt;:&lt;image tag&gt;
</code></pre>
<p>repository 通常指镜像名称，而 tag 指特定的构建或版本。</p>
<p>以官方 <a href="https://hub.docker.com/_/mysql">mysql</a> 镜像为例。如果想使用特定版本的MySQL（例如5.7）运行容器，则可以执行 <code>docker container run mysql:5.7</code>，其中 <code>mysql</code> 是镜像 repository，<code>5.7</code> 是 tag。</p>
<p>为了用  <code>custom-nginx:packaged</code> 标签标记自定义 NGINX 镜像，可以执行以下命令：</p>
<pre><code>docker image build --tag custom-nginx:packaged .

# Sending build context to Docker daemon  1.055MB
# Step 1/4 : FROM ubuntu:latest
#  ---&gt; f63181f19b2f
# Step 2/4 : EXPOSE 80
#  ---&gt; Running in 53ab370b9efc
# Removing intermediate container 53ab370b9efc
#  ---&gt; 6d6460a74447
# Step 3/4 : RUN apt-get update &amp;&amp;     apt-get install nginx -y &amp;&amp;     apt-get clean &amp;&amp; rm -rf /var/lib/apt/lists/*
#  ---&gt; Running in b4951b6b48bb
### LONG INSTALLATION STUFF GOES HERE ###
# Removing intermediate container b4951b6b48bb
#  ---&gt; fdc6cdd8925a
# Step 4/4 : CMD ["nginx", "-g", "daemon off;"]
#  ---&gt; Running in 3bdbd2af4f0e
# Removing intermediate container 3bdbd2af4f0e
#  ---&gt; f8837621b99d
# Successfully built f8837621b99d
# Successfully tagged custom-nginx:packaged
</code></pre>
<p>除了现在可以将镜像称为  <code>custom-nginx:packaged</code>（而不是一些较长的随机字符串）之外，什么都不会改变。</p>
<p>如果在构建期间忘记为镜像添加标记，或者你想更改标记，可以使用 <code>image tag</code> 命令执行此操作：</p>
<pre><code>docker image tag &lt;image id&gt; &lt;image repository&gt;:&lt;image tag&gt;

## 或者 ##

docker image tag &lt;image repository&gt;:&lt;image tag&gt; &lt;new image repository&gt;:&lt;new image tag&gt;
</code></pre>
<h3 id="">如何删除、列表展示镜像</h3>
<p>就像 <code>container ls</code> 命令一样，可以使用 <code>image ls</code> 命令列出本地系统中的所有镜像：</p>
<pre><code>docker image ls

# REPOSITORY     TAG        IMAGE ID       CREATED         SIZE
# &lt;none&gt;         &lt;none&gt;     3199372aa3fc   7 seconds ago   132MB
# custom-nginx   packaged   f8837621b99d   4 minutes ago   132MB
</code></pre>
<p>可以使用  <code>image rm</code>  命令删除此处列出的镜像。通用语法如下：</p>
<pre><code>docker image rm &lt;image identifier&gt;
</code></pre>
<p>标识符可以是镜像 ID 或镜像仓库。 如果使用仓库，则还必须指定标记。要删除 <code>custom-nginx:packaged</code> 镜像，可以执行以下命令：</p>
<pre><code>docker image rm custom-nginx:packaged

# Untagged: custom-nginx:packaged
# Deleted: sha256:f8837621b99d3388a9e78d9ce49fbb773017f770eea80470fb85e0052beae242
# Deleted: sha256:fdc6cdd8925ac25b9e0ed1c8539f96ad89ba1b21793d061e2349b62dd517dadf
# Deleted: sha256:c20e4aa46615fe512a4133089a5cd66f9b7da76366c96548790d5bf865bd49c4
# Deleted: sha256:6d6460a744475a357a2b631a4098aa1862d04510f3625feb316358536fcd8641
</code></pre>
<p>还可以使用 <code>image prune</code> 命令来清除所有未标记的挂起的镜像，如下所示：</p>
<pre><code>docker image prune --force

# Deleted Images:
# deleted: sha256:ba9558bdf2beda81b9acc652ce4931a85f0fc7f69dbc91b4efc4561ef7378aff
# deleted: sha256:ad9cc3ff27f0d192f8fa5fadebf813537e02e6ad472f6536847c4de183c02c81
# deleted: sha256:f1e9b82068d43c1bb04ff3e4f0085b9f8903a12b27196df7f1145aa9296c85e7
# deleted: sha256:ec16024aa036172544908ec4e5f842627d04ef99ee9b8d9aaa26b9c2a4b52baa

# Total reclaimed space: 59.19MB
</code></pre>
<p><code>--force</code>  或 <code>-f</code> 选项会跳过所有确认问题。也可以使用 <code>--all</code> 或  <code>-a</code>  选项删除本地仓库中的所有缓存镜像。</p>
<h3 id="docker">理解 Docker 镜像的分层</h3>
<p>从本书的开始，我就一直在说镜像是多层文件。在本小节中，我将演示镜像的各个层，以及它们如何在该镜像的构建过程中发挥重要作用。</p>
<p>在本演示中，我将使用上一小节的 <code>custom-nginx:packaged</code> 镜像。</p>
<p>要可视化镜像的多个层，可以使用 <code>image history</code> 命令。<code>custom-nginx:packaged</code> 图像的各个层可以如下所示：</p>
<pre><code>docker image history custom-nginx:packaged

# IMAGE               CREATED             CREATED BY                                      SIZE                COMMENT
# 7f16387f7307        5 minutes ago       /bin/sh -c #(nop)  CMD ["nginx" "-g" "daemon…   0B                             
# 587c805fe8df        5 minutes ago       /bin/sh -c apt-get update &amp;&amp;     apt-get ins…   60MB                
# 6fe4e51e35c1        6 minutes ago       /bin/sh -c #(nop)  EXPOSE 80                    0B                  
# d70eaf7277ea        17 hours ago        /bin/sh -c #(nop)  CMD ["/bin/bash"]            0B                  
# &lt;missing&gt;           17 hours ago        /bin/sh -c mkdir -p /run/systemd &amp;&amp; echo 'do…   7B                  
# &lt;missing&gt;           17 hours ago        /bin/sh -c [ -z "$(apt-get indextargets)" ]     0B                  
# &lt;missing&gt;           17 hours ago        /bin/sh -c set -xe   &amp;&amp; echo '#!/bin/sh' &gt; /…   811B                
# &lt;missing&gt;           17 hours ago        /bin/sh -c #(nop) ADD file:435d9776fdd3a1834…   72.9MB
</code></pre>
<p>此镜像有八层。最上面的一层是最新的一层，当向下移动时，这些层会变老。最顶层是通常用于运行容器的那一层。</p>
<p>现在，让我们仔细看看从镜像 <code>d70eaf7277ea</code> 到镜像 <code>7f16387f7307</code> 的所有镜像。我将忽略 <code>IMAGE</code> 是 <code>&lt;missing&gt;</code> 的最下面的四层，因为它们与我们无关。</p>
<ul>
<li><code>d70eaf7277ea</code>  是由 <code>/bin/sh -c #(nop)  CMD ["/bin/bash"]</code> 创建的，它指示Ubuntu 内的默认 shell 已成功加载。</li>
<li><code>6fe4e51e35c1</code> 是由 <code>/bin/sh -c #(nop)  EXPOSE 80</code> 创建的，这是代码中的第二条指令。</li>
<li><code>587c805fe8df</code> 是由 <code>/bin/sh -c apt-get update &amp;&amp; apt-get install nginx -y &amp;&amp; apt-get clean &amp;&amp; rm -rf /var/lib/apt/lists/*</code> 创建的，这是代码中的第三条指令。如果在执行此指令期间安装了所有必需的软件包，可以看到该镜像的大小为  <code>60MB</code>。</li>
<li>最后，最上层的 <code>7f16387f7307</code> 是由 <code>/bin/sh -c #(nop)  CMD ["nginx", "-g", "daemon off;"]</code> 创建的，它为该镜像设置了默认命令。</li>
</ul>
<p>如你所见，该镜像由许多只读层组成，每个层都记录了由某些指令触发的一组新的状态更改。当使用镜像启动容器时，会在其他层之上获得一个新的可写层。</p>
<p>每次使用 Docker 时都会发生这种分层现象，这是通过一个称为 union file system 的技术概念而得以实现的。 在这里，联合意味着集合论中的联合。根据 <a href="https://en.wikipedia.org/wiki/UnionFS">Wikipedia</a></p>
<blockquote>
<p>它允许透明地覆盖独立文件系统（称为分支）的文件和目录，从而形成单个一致的文件系统。合并分支内具有相同路径的目录的内容将在新的虚拟文件系统内的单个合并目录中一起看到。</p>
</blockquote>
<p>通过利用这一概念，Docker 可以避免数据重复，并且可以将先前创建的层用作以后构建的缓存。这样便产生了可在任何地方使用的紧凑，有效的镜像。</p>
<h3 id="nginx">怎样从源码构建 NGINX</h3>
<p>在上一小节中，了解了 <code>FROM</code>、<code>EXPOSE</code>、<code>RUN</code> 和 <code>CMD</code> 指令。在本小节中，将学到更多有关其他指令的信息。</p>
<p>在本小节中，将再次创建一个自定义的 NGINX 镜像。但是，不同之处在于，将从源代码构建 NGINX，而不是像上一个示例那样使用诸如 <code>apt-get</code> 之类的软件包管理器进行安装。</p>
<p>从源代码构建 NGINX，首先需要 NGINX 的源代码。 如果克隆了我的项目仓库，则会在 <code>custom-nginx</code> 目录中看到一个名为 <code>nginx-1.19.2.tar.gz</code> 的文件。将使用此归档文件作为构建 NGINX 的源。</p>
<p>在开始编写代码之前，先规划一下流程。 这次的镜像创建过程可以分七个步骤完成。如下：</p>
<ul>
<li>获得用于构建应用程序的基础镜像，例如 <a href="https://hub.docker.com/_/ubuntu">ubuntu</a>。</li>
<li>在基础镜像上安装必要的构建依赖项。</li>
<li>复制  <code>nginx-1.19.2.tar.gz</code>  文件到镜像里。</li>
<li>解压缩压缩包的内容并删除压缩包。</li>
<li>使用  <code>make</code> 工具配置构建，编译和安装程序。</li>
<li>删除解压缩的源代码。</li>
<li>运行<code>nginx</code>可执行文件。</li>
</ul>
<p>现在有了一个规划，让我们开始打开旧的 <code>Dockerfile</code> 并按如下所示更新其内容：</p>
<pre><code>FROM ubuntu:latest

RUN apt-get update &amp;&amp; \
    apt-get install build-essential\ 
                    libpcre3 \
                    libpcre3-dev \
                    zlib1g \
                    zlib1g-dev \
                    libssl-dev \
                    -y &amp;&amp; \
    apt-get clean &amp;&amp; rm -rf /var/lib/apt/lists/*

COPY nginx-1.19.2.tar.gz .

RUN tar -xvf nginx-1.19.2.tar.gz &amp;&amp; rm nginx-1.19.2.tar.gz

RUN cd nginx-1.19.2 &amp;&amp; \
    ./configure \
        --sbin-path=/usr/bin/nginx \
        --conf-path=/etc/nginx/nginx.conf \
        --error-log-path=/var/log/nginx/error.log \
        --http-log-path=/var/log/nginx/access.log \
        --with-pcre \
        --pid-path=/var/run/nginx.pid \
        --with-http_ssl_module &amp;&amp; \
    make &amp;&amp; make install

RUN rm -rf /nginx-1.19.2

CMD ["nginx", "-g", "daemon off;"]
</code></pre>
<p>如你所见，<code>Dockerfile</code> 中的代码反映了我上面提到的七个步骤。</p>
<ul>
<li><code>FROM</code> 指令将 Ubuntu 设置为基本映像，从而为构建任何应用程序提供了理想的环境。</li>
<li><code>RUN</code>  指令安装了从源代码构建 NGINX 所需的标准软件包。</li>
<li>这里的  <code>COPY</code>  指令是新的东西。该指令负责在映像内复制 <code>nginx-1.19.2.tar.gz</code> 文件。 <code>COPY</code> 指令的通用语法是  <code>COPY &lt;source&gt; &lt;destination&gt;</code>，其中 source 在本地文件系统中，而 destination 在镜像内部。作为目标的 <code>.</code> 表示镜像内的工作目录，除非另有设置，否则默认为 <code>/</code>。</li>
<li>这里的第二条 <code>RUN</code> 指令使用 <code>tar</code> 从压缩包中提取内容，然后将其删除。</li>
<li>存档文件包含一个名为 <code>nginx-1.19.2</code> 的目录，其中包含源代码。因此，下一步，将 <code>cd</code>  进入该目录并执行构建过程。 可以阅读 <a href="https://itsfoss.com/install-software-from-source-code/">How to Install Software from Source Code… and Remove it Afterwards</a> 文章，以了解有关该主题的更多信息。</li>
<li>构建和安装完成后，使用  <code>rm</code>  命令删除 <code>nginx-1.19.2</code> 目录。</li>
<li>在最后一步，像以前一样以单进程模式启动 NGINX。</li>
</ul>
<p>现在，要使用此代码构建镜像，请执行以下命令：</p>
<pre><code>docker image build --tag custom-nginx:built .

# Step 1/7 : FROM ubuntu:latest
#  ---&gt; d70eaf7277ea
# Step 2/7 : RUN apt-get update &amp;&amp;     apt-get install build-essential                    libpcre3                     libpcre3-dev                     zlib1g                     zlib1g-dev                     libssl-dev                     -y &amp;&amp;     apt-get clean &amp;&amp; rm -rf /var/lib/apt/lists/*
#  ---&gt; Running in 2d0aa912ea47
### LONG INSTALLATION STUFF GOES HERE ###
# Removing intermediate container 2d0aa912ea47
#  ---&gt; cbe1ced3da11
# Step 3/7 : COPY nginx-1.19.2.tar.gz .
#  ---&gt; 7202902edf3f
# Step 4/7 : RUN tar -xvf nginx-1.19.2.tar.gz &amp;&amp; rm nginx-1.19.2.tar.gz
 ---&gt; Running in 4a4a95643020
### LONG EXTRACTION STUFF GOES HERE ###
# Removing intermediate container 4a4a95643020
#  ---&gt; f9dec072d6d6
# Step 5/7 : RUN cd nginx-1.19.2 &amp;&amp;     ./configure         --sbin-path=/usr/bin/nginx         --conf-path=/etc/nginx/nginx.conf         --error-log-path=/var/log/nginx/error.log         --http-log-path=/var/log/nginx/access.log         --with-pcre         --pid-path=/var/run/nginx.pid         --with-http_ssl_module &amp;&amp;     make &amp;&amp; make install
#  ---&gt; Running in b07ba12f921e
### LONG CONFIGURATION AND BUILD STUFF GOES HERE ###
# Removing intermediate container b07ba12f921e
#  ---&gt; 5a877edafd8b
# Step 6/7 : RUN rm -rf /nginx-1.19.2
#  ---&gt; Running in 947e1d9ba828
# Removing intermediate container 947e1d9ba828
#  ---&gt; a7702dc7abb7
# Step 7/7 : CMD ["nginx", "-g", "daemon off;"]
#  ---&gt; Running in 3110c7fdbd57
# Removing intermediate container 3110c7fdbd57
#  ---&gt; eae55f7369d3
# Successfully built eae55f7369d3
# Successfully tagged custom-nginx:built
</code></pre>
<p>这段代码还不错，但是我们可以在某些地方进行改进。</p>
<ul>
<li>可以使用  <code>ARG</code>  指令创建自变量，而不是像 <code>nginx-1.19.2.tar.gz</code> 这样的文件名进行硬编码。这样，只需更改参数即可更改版本或文件名。</li>
<li>可以让守护程序在构建过程中下载文件，而不是手动下载存档。还有另一种类似于<code>COPY</code>  的指令，称为 <code>ADD</code>  指令，该指令能够从互联网添加文件。</li>
</ul>
<p>打开 <code>Dockerfile</code> 文件，并按如下所示更新其内容：</p>
<pre><code>FROM ubuntu:latest

RUN apt-get update &amp;&amp; \
    apt-get install build-essential\ 
                    libpcre3 \
                    libpcre3-dev \
                    zlib1g \
                    zlib1g-dev \
                    libssl-dev \
                    -y &amp;&amp; \
    apt-get clean &amp;&amp; rm -rf /var/lib/apt/lists/*

ARG FILENAME="nginx-1.19.2"
ARG EXTENSION="tar.gz"

ADD https://nginx.org/download/${FILENAME}.${EXTENSION} .

RUN tar -xvf ${FILENAME}.${EXTENSION} &amp;&amp; rm ${FILENAME}.${EXTENSION}

RUN cd ${FILENAME} &amp;&amp; \
    ./configure \
        --sbin-path=/usr/bin/nginx \
        --conf-path=/etc/nginx/nginx.conf \
        --error-log-path=/var/log/nginx/error.log \
        --http-log-path=/var/log/nginx/access.log \
        --with-pcre \
        --pid-path=/var/run/nginx.pid \
        --with-http_ssl_module &amp;&amp; \
    make &amp;&amp; make install

RUN rm -rf /${FILENAME}}

CMD ["nginx", "-g", "daemon off;"]
</code></pre>
<p>该代码几乎与先前的代码块相同，除了在第 13、14 行有一条名为 <code>ARG</code> 的新指令，以及在第 16 行用法了 <code>ADD</code> 指令。有关更新代码的说明如下：</p>
<ul>
<li><code>ARG</code> 指令可以像其他语言一样声明变量。以后可以使用 <code>${argument name}</code> 语法访问这些变量或参数。在这里，我将文件名 <code>nginx-1.19.2</code> 和文件扩展名 <code>tar.gz</code> 放在了两个单独的参数中。这样，我只需在一个地方进行更改就可以在 NGINX 的较新版本或存档格式之间进行切换。在上面的代码中，我向变量添加了默认值。变量值也可以作为 <code>image build</code> 命令的选项传递。你可以查阅<a href="https://docs.docker.com/engine/reference/builder/#arg">官方参考</a>了解更多详细信息。</li>
<li>在 <code>ADD</code> 指令中，我使用上面声明的参数动态形成了下载 URL。<code>https://nginx.org/download/${FILENAME}.${EXTENSION}</code> 行将在构建过程生成类似于 <code>https://nginx.org/download/nginx-1.19.2.tar.gz</code> 的内容。可以通过一次更改文件版本或扩展名的方式来更改文件版本或扩展名，这里要使用 <code>ARG</code> 指令。</li>
<li>默认情况下，<code>ADD</code> 指令不会提取从互联网获取的文件，因此在第18行使用了 <code>tar</code>。</li>
</ul>
<p>其余代码几乎不变。 现在应该可以自己理解参数的用法。最后，让我们尝试从此更新的代码构建镜像。</p>
<pre><code>docker image build --tag custom-nginx:built .

# Step 1/9 : FROM ubuntu:latest
#  ---&gt; d70eaf7277ea
# Step 2/9 : RUN apt-get update &amp;&amp;     apt-get install build-essential                    libpcre3                     libpcre3-dev                     zlib1g                     zlib1g-dev                     libssl-dev                     -y &amp;&amp;     apt-get clean &amp;&amp; rm -rf /var/lib/apt/lists/*
#  ---&gt; cbe1ced3da11
### LONG INSTALLATION STUFF GOES HERE ###
# Step 3/9 : ARG FILENAME="nginx-1.19.2"
#  ---&gt; Running in 33b62a0e9ffb
# Removing intermediate container 33b62a0e9ffb
#  ---&gt; fafc0aceb9c8
# Step 4/9 : ARG EXTENSION="tar.gz"
#  ---&gt; Running in 5c32eeb1bb11
# Removing intermediate container 5c32eeb1bb11
#  ---&gt; 36efdf6efacc
# Step 5/9 : ADD https://nginx.org/download/${FILENAME}.${EXTENSION} .
# Downloading [==================================================&gt;]  1.049MB/1.049MB
#  ---&gt; dba252f8d609
# Step 6/9 : RUN tar -xvf ${FILENAME}.${EXTENSION} &amp;&amp; rm ${FILENAME}.${EXTENSION}
#  ---&gt; Running in 2f5b091b2125
### LONG EXTRACTION STUFF GOES HERE ###
# Removing intermediate container 2f5b091b2125
#  ---&gt; 2c9a325d74f1
# Step 7/9 : RUN cd ${FILENAME} &amp;&amp;     ./configure         --sbin-path=/usr/bin/nginx         --conf-path=/etc/nginx/nginx.conf         --error-log-path=/var/log/nginx/error.log         --http-log-path=/var/log/nginx/access.log         --with-pcre         --pid-path=/var/run/nginx.pid         --with-http_ssl_module &amp;&amp;     make &amp;&amp; make install
#  ---&gt; Running in 11cc82dd5186
### LONG CONFIGURATION AND BUILD STUFF GOES HERE ###
# Removing intermediate container 11cc82dd5186
#  ---&gt; 6c122e485ec8
# Step 8/9 : RUN rm -rf /${FILENAME}}
#  ---&gt; Running in 04102366960b
# Removing intermediate container 04102366960b
#  ---&gt; 6bfa35420a73
# Step 9/9 : CMD ["nginx", "-g", "daemon off;"]
#  ---&gt; Running in 63ee44b571bb
# Removing intermediate container 63ee44b571bb
#  ---&gt; 4ce79556db1b
# Successfully built 4ce79556db1b
# Successfully tagged custom-nginx:built
</code></pre>
<p>现在可以使用 <code>custom-nginx:built</code> 镜像来运行容器了。</p>
<pre><code>docker container run --rm --detach --name custom-nginx-built --publish 8080:80 custom-nginx:built

# 90ccdbc0b598dddc4199451b2f30a942249d85a8ed21da3c8d14612f17eed0aa

docker container ls

# CONTAINER ID        IMAGE                COMMAND                  CREATED             STATUS              PORTS                  NAMES
# 90ccdbc0b598        custom-nginx:built   "nginx -g 'daemon of…"   2 minutes ago       Up 2 minutes        0.0.0.0:8080-&gt;80/tcp   custom-nginx-built
</code></pre>
<p>使用 <code>custom-nginx:built-v2</code> 映像的容器已成功运行。 现在可以从 <code>http://127.0.0.1:8080</code> 访问该容器。<br>
<img src="https://www.freecodecamp.org/news/content/images/2021/01/nginx-default.png" alt="nginx-default" width="600" height="400" loading="lazy"></p>
<p>这是 NGINX 的默认响应页面。可以访问<a href="https://docs.docker.com/engine/reference/builder/">官方参考</a>网站，以了解有关可用指令的更多信息。</p>
<h3 id="docker">怎样优化 Docker 镜像</h3>
<p>在上一个小节中构建的镜像具有功能，但是没有经过优化。为了证明我的观点，让我们使用  <code>image ls</code> 命令来查看镜像的大小：</p>
<pre><code>docker image ls

# REPOSITORY         TAG       IMAGE ID       CREATED          SIZE
# custom-nginx       built     1f3aaf40bb54   16 minutes ago   343MB
</code></pre>
<p>对于仅包含 NGINX 的镜像，这太大了。 如果拉取官方镜像并检查其大小，会看到它很小：</p>
<pre><code>docker image pull nginx:stable

# stable: Pulling from library/nginx
# a076a628af6f: Pull complete 
# 45d7b5d3927d: Pull complete 
# 5e326fece82e: Pull complete 
# 30c386181b68: Pull complete 
# b15158e9ebbe: Pull complete 
# Digest: sha256:ebd0fd56eb30543a9195280eb81af2a9a8e6143496accd6a217c14b06acd1419
# Status: Downloaded newer image for nginx:stable
# docker.io/library/nginx:stable

docker image ls

# REPOSITORY         TAG       IMAGE ID       CREATED          SIZE
# custom-nginx       built     1f3aaf40bb54   25 minutes ago   343MB
# nginx              stable    b9e1dc12387a   11 days ago      133MB
</code></pre>
<p>为了找出根本原因，让我们首先看一下 <code>Dockerfile</code>：</p>
<pre><code>FROM ubuntu:latest

RUN apt-get update &amp;&amp; \
    apt-get install build-essential\ 
                    libpcre3 \
                    libpcre3-dev \
                    zlib1g \
                    zlib1g-dev \
                    libssl-dev \
                    -y &amp;&amp; \
    apt-get clean &amp;&amp; rm -rf /var/lib/apt/lists/*

ARG FILENAME="nginx-1.19.2"
ARG EXTENSION="tar.gz"

ADD https://nginx.org/download/${FILENAME}.${EXTENSION} .

RUN tar -xvf ${FILENAME}.${EXTENSION} &amp;&amp; rm ${FILENAME}.${EXTENSION}

RUN cd ${FILENAME} &amp;&amp; \
    ./configure \
        --sbin-path=/usr/bin/nginx \
        --conf-path=/etc/nginx/nginx.conf \
        --error-log-path=/var/log/nginx/error.log \
        --http-log-path=/var/log/nginx/access.log \
        --with-pcre \
        --pid-path=/var/run/nginx.pid \
        --with-http_ssl_module &amp;&amp; \
    make &amp;&amp; make install

RUN rm -rf /${FILENAME}}

CMD ["nginx", "-g", "daemon off;"]
</code></pre>
<p>正如在第 3 行看到的那样，<code>RUN</code> 指令安装了很多东西。尽管这些软件包对于从源代码构建 NGINX 是必需的，但对于运行它而言则不是必需的。</p>
<p>在安装的 6 个软件包中，只有两个是运行 NGINX 所必需的，即 <code>libpcre3</code> 和 <code>zlib1g</code>。 因此，一个更好的主意是在构建过程完成后，卸载其他软件包。</p>
<p>为此，请按如下所示更新的 <code>Dockerfile</code> ：</p>
<pre><code>FROM ubuntu:latest

EXPOSE 80

ARG FILENAME="nginx-1.19.2"
ARG EXTENSION="tar.gz"

ADD https://nginx.org/download/${FILENAME}.${EXTENSION} .

RUN apt-get update &amp;&amp; \
    apt-get install build-essential \ 
                    libpcre3 \
                    libpcre3-dev \
                    zlib1g \
                    zlib1g-dev \
                    libssl-dev \
                    -y &amp;&amp; \
    tar -xvf ${FILENAME}.${EXTENSION} &amp;&amp; rm ${FILENAME}.${EXTENSION} &amp;&amp; \
    cd ${FILENAME} &amp;&amp; \
    ./configure \
        --sbin-path=/usr/bin/nginx \
        --conf-path=/etc/nginx/nginx.conf \
        --error-log-path=/var/log/nginx/error.log \
        --http-log-path=/var/log/nginx/access.log \
        --with-pcre \
        --pid-path=/var/run/nginx.pid \
        --with-http_ssl_module &amp;&amp; \
    make &amp;&amp; make install &amp;&amp; \
    cd / &amp;&amp; rm -rfv /${FILENAME} &amp;&amp; \
    apt-get remove build-essential \ 
                    libpcre3-dev \
                    zlib1g-dev \
                    libssl-dev \
                    -y &amp;&amp; \
    apt-get autoremove -y &amp;&amp; \
    apt-get clean &amp;&amp; rm -rf /var/lib/apt/lists/*

CMD ["nginx", "-g", "daemon off;"]
</code></pre>
<p>如你所见，在第 10 行上，一条 <code>RUN</code>  指令正在执行所有必要的核心操作。确切的事件链如下：</p>
<ul>
<li>从第 10 行到第 17 行，安装所有必需的软件包。</li>
<li>在第 18 行，将提取源代码，并删除下载的存档。</li>
<li>从第 19 行到第 28 行，NGINX 在系统上配置，构建和安装。</li>
<li>在第 29 行，从下载的档案中提取的文件将被删除。</li>
<li>从第 30 行到第 36 行，所有不必要的软件包都将被卸载并清除缓存。运行 NGINX 需要 <code>libpcre3</code> 和 <code>zlib1g</code> 包，因此我们保留了它们。</li>
</ul>
<p>你可能会问，为什么我要在一条  <code>RUN</code> 指令中做这么多工作，而不是像我们之前那样将它们很好地拆分成多个指令。 好吧，将它们拆分会是一个错误。</p>
<p>如果安装了软件包，然后按照单独的 <code>RUN</code> 说明将其删除，则它们将位于镜像的不同层中。尽管最终镜像不会包含已删除的包，但是由于它们存在于组成该图像的一层之一中，因此它们的大小仍将添加到最终镜像中。因此，请确保在单层上进行了此类更改。</p>
<p>让我们使用此 <code>Dockerfile</code> 来构建映像，并查看它们之间的区别。</p>
<pre><code>docker image build --tag custom-nginx:built .

# Sending build context to Docker daemon  1.057MB
# Step 1/7 : FROM ubuntu:latest
#  ---&gt; f63181f19b2f
# Step 2/7 : EXPOSE 80
#  ---&gt; Running in 006f39b75964
# Removing intermediate container 006f39b75964
#  ---&gt; 6943f7ef9376
# Step 3/7 : ARG FILENAME="nginx-1.19.2"
#  ---&gt; Running in ffaf89078594
# Removing intermediate container ffaf89078594
#  ---&gt; 91b5cdb6dabe
# Step 4/7 : ARG EXTENSION="tar.gz"
#  ---&gt; Running in d0f5188444b6
# Removing intermediate container d0f5188444b6
#  ---&gt; 9626f941ccb2
# Step 5/7 : ADD https://nginx.org/download/${FILENAME}.${EXTENSION} .
# Downloading [==================================================&gt;]  1.049MB/1.049MB
#  ---&gt; a8e8dcca1be8
# Step 6/7 : RUN apt-get update &amp;&amp;     apt-get install build-essential                     libpcre3                     libpcre3-dev                     zlib1g                     zlib1g-dev                     libssl-dev                     -y &amp;&amp;     tar -xvf ${FILENAME}.${EXTENSION} &amp;&amp; rm ${FILENAME}.${EXTENSION} &amp;&amp;     cd ${FILENAME} &amp;&amp;     ./configure         --sbin-path=/usr/bin/nginx         --conf-path=/etc/nginx/nginx.conf         --error-log-path=/var/log/nginx/error.log         --http-log-path=/var/log/nginx/access.log         --with-pcre         --pid-path=/var/run/nginx.pid         --with-http_ssl_module &amp;&amp;     make &amp;&amp; make install &amp;&amp;     cd / &amp;&amp; rm -rfv /${FILENAME} &amp;&amp;     apt-get remove build-essential                     libpcre3-dev                     zlib1g-dev                     libssl-dev                     -y &amp;&amp;     apt-get autoremove -y &amp;&amp;     apt-get clean &amp;&amp; rm -rf /var/lib/apt/lists/*
#  ---&gt; Running in e5675cad1260
### LONG INSTALLATION AND BUILD STUFF GOES HERE ###
# Removing intermediate container e5675cad1260
#  ---&gt; dc7e4161f975
# Step 7/7 : CMD ["nginx", "-g", "daemon off;"]
#  ---&gt; Running in b579e4600247
# Removing intermediate container b579e4600247
#  ---&gt; 512aa6a95a93
# Successfully built 512aa6a95a93
# Successfully tagged custom-nginx:built

docker image ls

# REPOSITORY         TAG       IMAGE ID       CREATED              SIZE
# custom-nginx       built     512aa6a95a93   About a minute ago   81.6MB
# nginx              stable    b9e1dc12387a   11 days ago          133MB
</code></pre>
<p>如你所见，镜像大小从 343MB 变为 81.6MB。官方镜像是 133MB。这是一个非常优化的构建，我们可以在下一部分中进一步介绍。</p>
<h3 id="alpinelinux">拥抱 Alpine Linux</h3>
<p>如果之前了解过 Docker，可能已经听说了 <a href="https://alpinelinux.org/">Alpine Linux</a>。 这是功能齐全的 <a href="https://en.wikipedia.org/wiki/Linux">Linux</a> 发行版，就像 <a href="https://ubuntu.com/">Ubuntu</a>、<a href="https://www.debian.org/">Debian</a> 或 <a href="https://getfedora.org/">Fedora</a>。</p>
<p>但是 Alpine 的好处是它是基于 <code>musl</code>，<code>libc</code> 和 <code>busybox</code> 构建的，并且是轻量级的。最新的 <a href="https://hub.docker.com/_/ubuntu">ubuntu</a> 镜像大约为 28MB，而 <a href="https://hub.docker.com/_/alpine">alpine</a> 仅为 2.8MB。</p>
<p>除了轻量之外，Alpine 还很安全，比其他发行版更适合创建容器。</p>
<p>尽管不像其他商业发行版那样用户友好，但是向 Alpine 的过渡仍然非常简单。在本小节中，将学习有关以 Alpine 镜像为基础重新创建 <code>custom-nginx</code> 镜像的信息。</p>
<p>打开的 <code>Dockerfile</code> 并更新其内容，如下所示：</p>
<pre><code>FROM alpine:latest

EXPOSE 80

ARG FILENAME="nginx-1.19.2"
ARG EXTENSION="tar.gz"

ADD https://nginx.org/download/${FILENAME}.${EXTENSION} .

RUN apk add --no-cache pcre zlib &amp;&amp; \
    apk add --no-cache \
            --virtual .build-deps \
            build-base \ 
            pcre-dev \
            zlib-dev \
            openssl-dev &amp;&amp; \
    tar -xvf ${FILENAME}.${EXTENSION} &amp;&amp; rm ${FILENAME}.${EXTENSION} &amp;&amp; \
    cd ${FILENAME} &amp;&amp; \
    ./configure \
        --sbin-path=/usr/bin/nginx \
        --conf-path=/etc/nginx/nginx.conf \
        --error-log-path=/var/log/nginx/error.log \
        --http-log-path=/var/log/nginx/access.log \
        --with-pcre \
        --pid-path=/var/run/nginx.pid \
        --with-http_ssl_module &amp;&amp; \
    make &amp;&amp; make install &amp;&amp; \
    cd / &amp;&amp; rm -rfv /${FILENAME} &amp;&amp; \
    apk del .build-deps

CMD ["nginx", "-g", "daemon off;"]
</code></pre>
<p>除了几处更改外，代码几乎完全相同。我将列出更改并在进行过程中进行解释：</p>
<ul>
<li>我们不使用 <code>apt-get install</code> 来安装软件包，而是使用 <code>apk add</code>。<code>--no-cache</code> 选项意味着下载的软件包将不会被缓存。同样，我们将使用 <code>apk del</code> 代替 <code>apt-get remove</code> 来卸载软件包。</li>
<li><code>apk add</code> 命令的 <code>--virtual</code> 选项用于将一堆软件包捆绑到单个虚拟软件包中，以便于管理。仅用于构建程序所需的软件包被标记为  <code>.build-deps</code>，然后通过执行 <code>apk del .build-deps</code> 命令在第 29 行将其删除。可以在官方文档中了解有关 <a href="https://docs.alpinelinux.org/user-handbook/0.1a/Working/apk.html#_virtuals">virtuals</a> 的更多信息。</li>
<li>软件包名称在这里有些不同。通常，每个 Linux 发行版都有其软件包仓库，可供在其中搜索软件包的每个人使用。如果你知道某项任务所需的软件包，则可以直接转到指定发行版的仓库的并进行搜索。可以 <a href="https://pkgs.alpinelinux.org/packages">在此处了解 Alpine Linux软件包</a>。</li>
</ul>
<p>现在使用此 <code>Dockerfile</code> 构建一个新镜像，并查看文件大小的差异：</p>
<pre><code>docker image build --tag custom-nginx:built .

# Sending build context to Docker daemon  1.055MB
# Step 1/7 : FROM alpine:latest
#  ---&gt; 7731472c3f2a
# Step 2/7 : EXPOSE 80
#  ---&gt; Running in 8336cfaaa48d
# Removing intermediate container 8336cfaaa48d
#  ---&gt; d448a9049d01
# Step 3/7 : ARG FILENAME="nginx-1.19.2"
#  ---&gt; Running in bb8b2eae9d74
# Removing intermediate container bb8b2eae9d74
#  ---&gt; 87ca74f32fbe
# Step 4/7 : ARG EXTENSION="tar.gz"
#  ---&gt; Running in aa09627fe48c
# Removing intermediate container aa09627fe48c
#  ---&gt; 70cb557adb10
# Step 5/7 : ADD https://nginx.org/download/${FILENAME}.${EXTENSION} .
# Downloading [==================================================&gt;]  1.049MB/1.049MB
#  ---&gt; b9790ce0c4d6
# Step 6/7 : RUN apk add --no-cache pcre zlib &amp;&amp;     apk add --no-cache             --virtual .build-deps             build-base             pcre-dev             zlib-dev             openssl-dev &amp;&amp;     tar -xvf ${FILENAME}.${EXTENSION} &amp;&amp; rm ${FILENAME}.${EXTENSION} &amp;&amp;     cd ${FILENAME} &amp;&amp;     ./configure         --sbin-path=/usr/bin/nginx         --conf-path=/etc/nginx/nginx.conf         --error-log-path=/var/log/nginx/error.log         --http-log-path=/var/log/nginx/access.log         --with-pcre         --pid-path=/var/run/nginx.pid         --with-http_ssl_module &amp;&amp;     make &amp;&amp; make install &amp;&amp;     cd / &amp;&amp; rm -rfv /${FILENAME} &amp;&amp;     apk del .build-deps
#  ---&gt; Running in 0b301f64ffc1
### LONG INSTALLATION AND BUILD STUFF GOES HERE ###
# Removing intermediate container 0b301f64ffc1
#  ---&gt; dc7e4161f975
# Step 7/7 : CMD ["nginx", "-g", "daemon off;"]
#  ---&gt; Running in b579e4600247
# Removing intermediate container b579e4600247
#  ---&gt; 3e186a3c6830
# Successfully built 3e186a3c6830
# Successfully tagged custom-nginx:built

docker image ls

# REPOSITORY         TAG       IMAGE ID       CREATED         SIZE
# custom-nginx       built     3e186a3c6830   8 seconds ago   12.8MB
</code></pre>
<p>ubuntu 版本为 81.6MB，而 alpine 版本已降至 12.8MB，这是一个巨大的进步。除了 <code>apk</code> 软件包管理器外，Alpine 和 Ubuntu 还有一些其他的区别，但是没什么大不了的。遇到困难，可以搜索互联网。</p>
<h3 id="docker">怎样创建可执行 Docker 镜像</h3>
<p>在上一节中，使用了 <a href="https://hub.docker.com/r/fhsinchy/rmbyext">fhsinchy/rmbyext</a> 镜像。在本节中，将学习如何制作这样的可执行镜像。</p>
<p>首先，打开本书随附仓库的目录。<code>rmbyext</code> 应用程序的代码位于同名子目录中。</p>
<p>在开始使用 <code>Dockerfile</code> 之前，请花一点时间来规划最终的输出。我认为应该是这样的：</p>
<ul>
<li>该镜像应预安装 Python。</li>
<li>它应该包含 <code>rmbyext</code> 脚本的副本。</li>
<li>应该在将要执行脚本的地方设置一个工作目录。</li>
<li>应该将 <code>rmbyext</code> 脚本设置为入口点，以便镜像可以将扩展名用作参数。</li>
</ul>
<p>要构建上面提到的镜像，请执行以下步骤：</p>
<ul>
<li>获得可以运行 Python 脚本基础镜像，例如 <a href="https://hub.docker.com/_/python">python</a>。</li>
<li>将工作目录设置为易于访问的目录。</li>
<li>安装 Git，以便可以从我的 GitHub 仓库中安装脚本。</li>
<li>使用 Git 和 pip 安装脚本。</li>
<li>删除不必要的构建软件包。</li>
<li>将 <code>rmbyext</code> 设置为该图像的入口点。</li>
</ul>
<p>现在在 <code>rmbyext</code> 目录中创建一个新的 <code>Dockerfile</code>，并将以下代码放入其中：</p>
<pre><code>FROM python:3-alpine

WORKDIR /zone

RUN apk add --no-cache git &amp;&amp; \
    pip install git+https://github.com/fhsinchy/rmbyext.git
    apk del git

ENTRYPOINT [ "rmbyext" ]
</code></pre>
<p>该文件中的指令说明如下：</p>
<ul>
<li><code>FROM</code> 指令将 <a href="https://hub.docker.com/_/python">python</a> 设置为基本镜像，从而为运行 Python 脚本提供了理想的环境。<code>3-alpine</code> 标记表示需要 Python 3 的 Alpine 版本。</li>
<li>这里的 <code>WORKDIR</code> 指令将默认工作目录设置为 <code>/zone</code>。这里的工作目录名称完全是随机的。我发现 zone 是一个合适的名称，你也可以换成任何你想要的名称。</li>
<li>假设从 GitHub 安装了 <code>rmbyext</code> 脚本，则 <code>git</code> 是安装时的依赖项。第 5 行的 <code>RUN</code> 指令先安装 <code>git</code>，然后使用 Git 和 pip 安装  <code>rmbyext</code> 脚本。之后也删除了<code>git</code>。</li>
<li>最后，在第 9 行，<code>ENTRYPOINT</code> 指令将 <code>rmbyext</code> 脚本设置为该镜像的入口点。</li>
</ul>
<p>在整个文件中，第 9 行是将这个看似正常的镜像转换为可执行镜像的关键。现在要构建镜像，可以执行以下命令：</p>
<pre><code>docker image build --tag rmbyext .

# Sending build context to Docker daemon  2.048kB
# Step 1/4 : FROM python:3-alpine
# 3-alpine: Pulling from library/python
# 801bfaa63ef2: Already exists 
# 8723b2b92bec: Already exists 
# 4e07029ccd64: Already exists 
# 594990504179: Already exists 
# 140d7fec7322: Already exists 
# Digest: sha256:7492c1f615e3651629bd6c61777e9660caa3819cf3561a47d1d526dfeee02cf6
# Status: Downloaded newer image for python:3-alpine
#  ---&gt; d4d4f50f871a
# Step 2/4 : WORKDIR /zone
#  ---&gt; Running in 454374612a91
# Removing intermediate container 454374612a91
#  ---&gt; 7f7e49bc98d2
# Step 3/4 : RUN apk add --no-cache git &amp;&amp;     pip install git+https://github.com/fhsinchy/rmbyext.git#egg=rmbyext &amp;&amp;     apk del git
#  ---&gt; Running in 27e2e96dc95a
### LONG INSTALLATION STUFF GOES HERE ###
# Removing intermediate container 27e2e96dc95a
#  ---&gt; 3c7389432e36
# Step 4/4 : ENTRYPOINT [ "rmbyext" ]
#  ---&gt; Running in f239bbea1ca6
# Removing intermediate container f239bbea1ca6
#  ---&gt; 1746b0cedbc7
# Successfully built 1746b0cedbc7
# Successfully tagged rmbyext:latest

docker image ls

# REPOSITORY         TAG        IMAGE ID       CREATED         SIZE
# rmbyext            latest     1746b0cedbc7   4 minutes ago   50.9MB
</code></pre>
<p>这里在镜像名称之后没有提供任何标签，因此默认情况下该镜像已被标记为 <code>latest</code>。 应该能够像在上一节中看到的那样运行该镜像。请记住，参考你设置的实际镜像名称，而不是这里的 <code>fhsinchy/rmbyext</code>。</p>
<p>现在知道如何制作镜像了，是时候与全世界分享它们了。在线共享镜像很容易。所需要做的就是在任何在线仓库中注册一个帐户。在此处我将使用 <a href="https://hub.docker.com/">Docker Hub</a>。</p>
<p>导航到 <a href="https://hub.docker.com/signup">Sign Up</a> 页面并创建一个免费帐户。一个免费帐户可托管无限的公共仓库和一个私有仓库。</p>
<p>创建帐户后，需要使用 Docker CLI 登录。打开终端并执行以下命令：</p>
<pre><code>docker login

# Login with your Docker ID to push and pull images from Docker Hub. If you don't have a Docker ID, head over to https://hub.docker.com to create one.
# Username: fhsinchy
# Password: 
# WARNING! Your password will be stored unencrypted in /home/fhsinchy/.docker/config.json.
# Configure a credential helper to remove this warning. See
# https://docs.docker.com/engine/reference/commandline/login/#credentials-store
#
# Login Succeeded
</code></pre>
<p>系统将提示输入用户名和密码。如果输入正确，则应该成功登录到你的帐户。</p>
<p>为了在线共享镜像，必须对镜像进行标记。已经在上一小节中学习了有关标记的信息。只是为了加深记忆，<code>--tag</code> 或 <code>-t</code> 选项的通用语法如下：</p>
<pre><code>--tag &lt;image repository&gt;:&lt;image tag&gt;
</code></pre>
<p>例如，让我们在线共享 <code>custom-nginx</code> 图像。 为此，请在 <code>custom-nginx</code> 项目目录中打开一个新的终端窗口。</p>
<p>要在线共享镜像，必须使用 <code>&lt;docker hub username&gt;/&lt;image name&gt;:&lt;image tag&gt;</code> 语法对其进行标记。我的用户名是 <code>fhsinchy</code>，因此命令如下所示：</p>
<pre><code>docker image build --tag fhsinchy/custom-nginx:latest --file Dockerfile.built .

# Step 1/9 : FROM ubuntu:latest
#  ---&gt; d70eaf7277ea
# Step 2/9 : RUN apt-get update &amp;&amp;     apt-get install build-essential                    libpcre3                     libpcre3-dev                     zlib1g                     zlib1g-dev                     libssl-dev                     -y &amp;&amp;     apt-get clean &amp;&amp; rm -rf /var/lib/apt/lists/*
#  ---&gt; cbe1ced3da11
### LONG INSTALLATION STUFF GOES HERE ###
# Step 3/9 : ARG FILENAME="nginx-1.19.2"
#  ---&gt; Running in 33b62a0e9ffb
# Removing intermediate container 33b62a0e9ffb
#  ---&gt; fafc0aceb9c8
# Step 4/9 : ARG EXTENSION="tar.gz"
#  ---&gt; Running in 5c32eeb1bb11
# Removing intermediate container 5c32eeb1bb11
#  ---&gt; 36efdf6efacc
# Step 5/9 : ADD https://nginx.org/download/${FILENAME}.${EXTENSION} .
# Downloading [==================================================&gt;]  1.049MB/1.049MB
#  ---&gt; dba252f8d609
# Step 6/9 : RUN tar -xvf ${FILENAME}.${EXTENSION} &amp;&amp; rm ${FILENAME}.${EXTENSION}
#  ---&gt; Running in 2f5b091b2125
### LONG EXTRACTION STUFF GOES HERE ###
# Removing intermediate container 2f5b091b2125
#  ---&gt; 2c9a325d74f1
# Step 7/9 : RUN cd ${FILENAME} &amp;&amp;     ./configure         --sbin-path=/usr/bin/nginx         --conf-path=/etc/nginx/nginx.conf         --error-log-path=/var/log/nginx/error.log         --http-log-path=/var/log/nginx/access.log         --with-pcre         --pid-path=/var/run/nginx.pid         --with-http_ssl_module &amp;&amp;     make &amp;&amp; make install
#  ---&gt; Running in 11cc82dd5186
### LONG CONFIGURATION AND BUILD STUFF GOES HERE ###
# Removing intermediate container 11cc82dd5186
#  ---&gt; 6c122e485ec8
# Step 8/9 : RUN rm -rf /${FILENAME}}
#  ---&gt; Running in 04102366960b
# Removing intermediate container 04102366960b
#  ---&gt; 6bfa35420a73
# Step 9/9 : CMD ["nginx", "-g", "daemon off;"]
#  ---&gt; Running in 63ee44b571bb
# Removing intermediate container 63ee44b571bb
#  ---&gt; 4ce79556db1b
# Successfully built 4ce79556db1b
# Successfully tagged fhsinchy/custom-nginx:latest
</code></pre>
<p>在此命令中，<code>fhsinchy/custom-nginx</code> 是镜像仓库，而 <code>latest</code> 是标签。镜像名称可以是任何名称，上传镜像后即无法更改。可以随时更改标签，该标签通常反映软件的版本或其他类型的内部版本。</p>
<p>以 <code>node</code> 镜像为例。<code>node:lts</code> 镜像是指 Node.js 的长期支持版本，而 <code>node:lts-alpine</code> 版本是指为 Alpine Linux 构建的 Node.js 版本，它比常规版本小得多。</p>
<p>如果你给镜像添加任何标签，则会将其自动标记为 <code>latest</code>。但这并不意味着 <code>latest</code> 标签将始终引用最新版本。如果出于某种原因，将镜像的较旧版本明确标记为 <code>latest</code>，则 Docker 将不会做出任何额外的工作来进行交叉检查。</p>
<p>生成镜像后，可以通过执行以下命令来上传镜像：</p>
<pre><code>docker image push &lt;image repository&gt;:&lt;image tag&gt;
</code></pre>
<p>因此，在我这里，命令如下所示：</p>
<pre><code>docker image push fhsinchy/custom-nginx:latest

# The push refers to repository [docker.io/fhsinchy/custom-nginx]
# 4352b1b1d9f5: Pushed 
# a4518dd720bd: Pushed 
# 1d756dc4e694: Pushed 
# d7a7e2b6321a: Pushed 
# f6253634dc78: Mounted from library/ubuntu 
# 9069f84dbbe9: Mounted from library/ubuntu 
# bacd3af13903: Mounted from library/ubuntu 
# latest: digest: sha256:ffe93440256c9edb2ed67bf3bba3c204fec3a46a36ac53358899ce1a9eee497a size: 1788
</code></pre>
<p>根据镜像大小，上传可能需要一些时间。完成后，应该可以在中心配置文件页面中找到该镜像。</p>
<h2 id="javascript">如何容器化 JavaScript 应用程序</h2>
<p>现在，已经了解了创建镜像的知识，是时候做一些更相关的工作了。</p>
<p>在本小节中，将使用在之前小节上使用的 <a href="https://hub.docker.com/r/fhsinchy/hello-dock">fhsinchy/hello-dock</a> 镜像的源代码。在容器化这个非常简单的应用的过程中，介绍了 volumes  和多阶段构建，这是 Docker 中两个很重要的概念。</p>
<h3 id="dockerfile">如何编写开发 Dockerfile</h3>
<p>首先，打开用来克隆本书随附仓库的目录。<code>hello-dock</code> 应用程序的代码位于具有相同名称的子目录中。</p>
<p>这是一个非常简单的 JavaScript 项目，由 <a href="https://github.com/vitejs/vite">vitejs/vite</a> 项目构建。不过，请不要担心，无需了解 JavaScript 或 vite 即可学习本小节。了解 <a href="https://nodejs.org/">Node.js</a> 和 <a href="https://www.npmjs.com/">npm</a> 就足够了。</p>
<p>与上一部分中完成的其他项目一样，将从制定如何运行该应用程序的规划开始。如下：</p>
<ul>
<li>获得可以运行 JavaScript 应用程序的基础镜像，例如 <a href="https://hub.docker.com/_/node">node</a>。</li>
<li>在镜像内设置默认的工作目录。</li>
<li>将  <code>package.json</code> 文件复制到镜像中。</li>
<li>安装必要的依赖项。</li>
<li>复制其余的项目文件。</li>
<li>通过执行 <code>npm run dev</code> 命令来启动 <code>vite</code> 开发服务。</li>
</ul>
<p>该规划应始终来由应用程序的开发人员制定。如果你是开发人员，那么应该已经对如何运行此应用程序有正确的了解。</p>
<p>现在，如果将上述计划放入 <code>Dockerfile.dev</code> 中，则该文件应如下所示：</p>
<pre><code>FROM node:lts-alpine

EXPOSE 3000

USER node

RUN mkdir -p /home/node/app

WORKDIR /home/node/app

COPY ./package.json .
RUN npm install

COPY . .

CMD [ "npm", "run", "dev" ]
</code></pre>
<p>此代码的说明如下：</p>
<ul>
<li>这里的 <code>FROM</code> 指令将官方的 Node.js 镜像设置为基础镜像，从而可以运行 JavaScript 应用。<code>lts-alpine</code> 标签代表镜像要使用针对 Alpine 的长期支持版本。 可以在 <a href="https://hub.docker.com/_/node">node</a> 页面上找到该镜像的所有标签和其它必要的文档。</li>
<li><code>USER</code> 指令将镜像的默认用户设置为 <code>node</code>。 默认情况下，Docker 以 root 用户身份运行容器。 但是根据 <a href="https://github.com/nodejs/docker-node/blob/master/docs/BestPractices.md">Docker and Node.js Best Practices</a>，这有安全隐患。因此，最好是尽可能以非 root 用户身份运行。node 镜像附带一个名为 <code>node</code> 的非 root 用户，可以使用 <code>USER</code> 指令将其设置为默认用户。</li>
<li><code>RUN mkdir -p /home/node/app</code> 指令在 <code>node</code>  用户的主目录内创建一个名为 <code>app</code> 的目录。默认情况下，Linux 中任何非 root 用户的主目录通常是 <code>/home/&lt;user name&gt;</code>。</li>
<li>然后，<code>WORKDIR</code> 指令将默认工作目录设置为新创建的 <code>/home/node/app</code> 目录。 默认情况下，任何镜像的工作目录都是根目录。如果不希望在根目录中放置不必要的文件，可以将默认工作目录更改为更合理的目录，例如 <code>/home/node/app</code> 或你喜欢的任何目录。该工作目录将适用于任何连续的 <code>COPY</code>、<code>ADD</code>、<code>RUN</code> 和 <code>CMD</code> 指令。</li>
<li>此处的 <code>COPY</code> 指令复制了 <code>package.json</code>文件，该文件包含有关此应用程序所有必需依赖项的信息。<code>RUN</code> 指令执行 <code>npm install</code> 命令，这是在 Node.js 项目中使用 <code>package.json</code> 文件安装依赖项的默认命令。 最后的  <code>.</code>  代表工作目录。</li>
<li>第二条 <code>COPY</code> 指令将其余内容从主机文件系统的当前目录（<code>.</code>）复制到镜像内的工作目录（<code>.</code>）。</li>
<li>最后，这里的 <code>CMD</code> 指令为该镜像设置了默认命令，即以 <code>exec</code> 形式编写的 <code>npm run dev</code>。</li>
<li>默认情况下，<code>vite</code> 开发服务器在端口 <code>3000</code> 上运行，最好添加一个 <code>EXPOSE</code> 命令。</li>
</ul>
<p>现在，要由此  <code>Dockerfile.dev</code> 构建像镜，可以执行以下命令：</p>
<pre><code>docker image build --file Dockerfile.dev --tag hello-dock:dev .

# Step 1/7 : FROM node:lts
#  ---&gt; b90fa0d7cbd1
# Step 2/7 : EXPOSE 3000
#  ---&gt; Running in 722d639badc7
# Removing intermediate container 722d639badc7
#  ---&gt; e2a8aa88790e
# Step 3/7 : WORKDIR /app
#  ---&gt; Running in 998e254b4d22
# Removing intermediate container 998e254b4d22
#  ---&gt; 6bd4c42892a4
# Step 4/7 : COPY ./package.json .
#  ---&gt; 24fc5164a1dc
# Step 5/7 : RUN npm install
#  ---&gt; Running in 23b4de3f930b
### LONG INSTALLATION STUFF GOES HERE ###
# Removing intermediate container 23b4de3f930b
#  ---&gt; c17ecb19a210
# Step 6/7 : COPY . .
#  ---&gt; afb6d9a1bc76
# Step 7/7 : CMD [ "npm", "run", "dev" ]
#  ---&gt; Running in a7ff529c28fe
# Removing intermediate container a7ff529c28fe
#  ---&gt; 1792250adb79
# Successfully built 1792250adb79
# Successfully tagged hello-dock:dev
</code></pre>
<p>如果文件名不是  <code>Dockerfile</code>，则必须使用 <code>--file</code> 选项显式传递文件名。 通过执行以下命令，可以使用此镜像运行容器：</p>
<pre><code>docker container run \
    --rm \
    --detach \
    --publish 3000:3000 \
    --name hello-dock-dev \
    hello-dock:dev

# 21b9b1499d195d85e81f0e8bce08f43a64b63d589c5f15cbbd0b9c0cb07ae268
</code></pre>
<p>现在访问 <code>http://127.0.0.1:3000</code>，可以看到 <code>hello-dock</code> 应用程序。</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2021/01/hello-dock-dev.png" alt="hello-dock-dev" width="600" height="400" loading="lazy"></p>
<p>恭喜你在容器内运行了你的第一个实际应用程序。刚刚编写的代码还可以，但是它存在一个大问题，可以在某些地方进行改进。让我们先从问题开始。</p>
<h3 id="dockerbindmounts">如何在 Docker 中使用 Bind Mounts</h3>
<p>如果你以前使用过任何前端 JavaScript 框架，则应该知道这些框架中的开发服务器通常带有热重载功能。也就是说，如果对代码进行更改，服务器将重新加载，并自动反映立即进行的所有更改。</p>
<p>但是，如果现在对代码进行任何更改，将不会在浏览器中运行任何应用程序。这是因为正在更改本地文件系统中的代码，但是在浏览器中看到的应用程序位于容器文件系统中。</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2021/01/local-vs-container-file-system.svg" alt="local-vs-container-file-system" width="600" height="400" loading="lazy"></p>
<p>要解决此问题，可以再次使用 <a href="https://docs.docker.com/storage/bind-mounts/">绑定挂载</a>。 使用绑定挂载，可以轻松地在容器内安装本地文件系统目录。绑定挂载可以直接从容器内部引用本地文件系统，而无需复制本地文件。</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2021/01/bind-mounts.svg" alt="bind-mounts" width="600" height="400" loading="lazy"></p>
<p>这样，对本地源代码所做的任何更改都会及时反映在容器内部，从而触发 <code>vite</code> 开发服务器的热重载功能。对容器内部文件系统所做的更改也将反映在本地文件系统上。</p>
<p>已经在<a href="https://www.freecodecamp.org/news/the-docker-handbook/#working-with-executable-images">使用可执行镜像</a>小节中学习到，可以对  <code>container run</code> 或 <code>container start</code> 命令使用 <code>--volume</code> 或 <code>-v</code> 选项创建绑定挂载。 回顾一下，通用语法如下：</p>
<pre><code>--volume &lt;local file system directory absolute path&gt;:&lt;container file system directory absolute path&gt;:&lt;read write access&gt;
</code></pre>
<p>停止先前启动的 <code>hello-dock-dev</code> 容器，并通过执行以下命令来启动新的容器：</p>
<pre><code>docker container run \
    --rm \
    --publish 3000:3000 \
    --name hello-dock-dev \
    --volume $(pwd):/home/node/app \
    hello-dock:dev

# sh: 1: vite: not found
# npm ERR! code ELIFECYCLE
# npm ERR! syscall spawn
# npm ERR! file sh
# npm ERR! errno ENOENT
# npm ERR! hello-dock@0.0.0 dev: `vite`
# npm ERR! spawn ENOENT
# npm ERR!
# npm ERR! Failed at the hello-dock@0.0.0 dev script.
# npm ERR! This is probably not a problem with npm. There is likely additional logging output above.
# npm WARN Local package.json exists, but node_modules missing, did you mean to install?
</code></pre>
<p>请记住，我省略了 <code>--detach</code> 选项，这只是说明一个非常重要的观点。如你所见，该应用程序现在根本没有运行。</p>
<p>这是因为尽管 volume 解决了热重载的问题，但它引入了另一个问题。如果你以前有过使用 Node.js 的经验，你可能会知道 Node.js 项目的依赖项位于项目根目录的 <code>node_modules</code> 目录中。</p>
<p>现在，将项目根目录作为容器中的 volume  安装在本地文件系统上，容器中的内容将被包含所有依赖项的 <code>node_modules</code> 目录替换。 这意味着 <code>vite</code> 软件包不见了。</p>
<h3 id="docker">如何在 Docker 中使用匿名卷</h3>
<p>可以使用匿名卷解决此问题。匿名卷除了无需在此处指定源目录之外，与绑定挂载相同。 创建匿名卷的通用语法如下：</p>
<pre><code>--volume &lt;container file system directory absolute path&gt;:&lt;read write access&gt;
</code></pre>
<p>因此，用两个卷启动 <code>hello-dock</code> 容器的最终命令应如下：</p>
<pre><code>docker container run \
    --rm \
    --detach \
    --publish 3000:3000 \
    --name hello-dock-dev \
    --volume $(pwd):/home/node/app \
    --volume /home/node/app/node_modules \
    hello-dock:dev

# 53d1cfdb3ef148eb6370e338749836160f75f076d0fbec3c2a9b059a8992de8b
</code></pre>
<p>在这里，Docker 将从容器内部获取整个 <code>node_modules</code> 目录，并将其存放在主机文件系统上由 Docker 守护程序管理的其他目录中，并将该目录作为 <code>node_modules</code> 挂载在容器中。</p>
<h3 id="docker">如何在 Docker 中执行多阶段构建</h3>
<p>到目前为止，在本节中，已经构建了用于在开发模式下运行 JavaScript 应用程序的镜像。现在，如果要在生产模式下构建镜像，会有一些新的挑战。</p>
<p>在开发模式下，<code>npm run serve</code> 命令启动一个开发服务器，该服务器将应用程序提供给用户。该服务器不仅提供文件，还提供热重载功能。</p>
<p>在生产模式下，<code>npm run build</code> 命令将所有 JavaScript 代码编译为一些静态 HTML、CSS 和 JavaScript 文件。要运行这些文件，不需要 node 或任何其他运行时依赖项。只需要一个像 <code>nginx</code>  这样的服务器。</p>
<p>要在应用程序以生产模式运行时创建镜像，可以执行以下步骤：</p>
<ul>
<li>使用 <code>node</code> 作为基础镜像并构建应用程序。</li>
<li>在 node 镜像中安装 <code>nginx</code> 并使用它来提供静态文件。</li>
</ul>
<p>这种方法是完全有效的。但是问题在于，<code>node</code> 镜像很大，并且它所承载的大多数内容对于静态文件服务而言都是不必要的。解决此问题的更好方法如下：</p>
<ul>
<li>使用<code>node</code>图像作为基础并构建应用程序。</li>
<li>将使用 <code>node</code> 镜像创建的文件复制到 <code>nginx</code> 映像。</li>
<li>根据 <code>nginx</code> 创建最终镜像，并丢弃所有与 <code>node</code> 相关的东西。</li>
</ul>
<p>这样，镜像仅包含所需的文件，变得非常方便。</p>
<p>这种方法是一个多阶段构建。要执行这样的构建，在 <code>hello-dock</code> 项目目录中创建一个新的 <code>Dockerfile</code>，并将以下内容放入其中：</p>
<pre><code>FROM node:lts-alpine as builder

WORKDIR /app

COPY ./package.json ./
RUN npm install

COPY . .
RUN npm run build

FROM nginx:stable-alpine

EXPOSE 80

COPY --from=builder /app/dist /usr/share/nginx/html
</code></pre>
<p>如你所见，<code>Dockerfile</code> 看起来很像以前的 Dockerfile，但有一些不同之处。该文件的解释如下：</p>
<ul>
<li>第 1 行使用 <code>node:lts-alpine</code> 作为基础镜像开始构建的第一阶段。<code>as builder</code> 语法为此阶段分配一个名称，以便以后可以引用。</li>
<li>从第 3 行到第 13 行，以前已经看过很多次了。实际上，<code>RUN npm run build</code> 命令会编译整个应用程序，并将其存放在 <code>/app/dist</code> 目录中，其中 <code>/app</code> 是工作目录，<code>/dist</code>  是 <code>vite</code> 应用程序的默认输出目录。</li>
<li>第 15 行使用 <code>nginx:stable-alpine</code> 作为基础镜像开始构建的第二阶段。</li>
<li>NGINX 服务器默认在端口 80 上运行，因此添加了 <code>EXPOSE 80</code> 行。</li>
<li>最后一行是 <code>COPY</code> 指令。<code>--from=builder</code> 部分表示要从 <code>builder</code> 阶段复制一些文件。之后，这是一条标准的复制指令，其中 <code>/app/dist</code> 是 source，而 <code>/usr/share/nginx/html</code>  是 destination。 这里使用的 destination 是 NGINX 的默认站点路径，因此放置在其中的任何静态文件都将自动提供。</li>
</ul>
<p>如你所见，生成的镜像是基于 <code>nginx</code> 的镜像，仅包含运行应用程序所需的文件。要构建此镜像，请执行以下命令：</p>
<pre><code>docker image build --tag hello-dock:prod .

# Step 1/9 : FROM node:lts-alpine as builder
#  ---&gt; 72aaced1868f
# Step 2/9 : WORKDIR /app
#  ---&gt; Running in e361c5c866dd
# Removing intermediate container e361c5c866dd
#  ---&gt; 241b4b97b34c
# Step 3/9 : COPY ./package.json ./
#  ---&gt; 6c594c5d2300
# Step 4/9 : RUN npm install
#  ---&gt; Running in 6dfabf0ee9f8
# npm WARN deprecated fsevents@2.1.3: Please update to v 2.2.x
#
# &gt; esbuild@0.8.29 postinstall /app/node_modules/esbuild
# &gt; node install.js
#
# npm notice created a lockfile as package-lock.json. You should commit this file.
# npm WARN optional SKIPPING OPTIONAL DEPENDENCY: fsevents@~2.1.2 (node_modules/chokidar/node_modules/fsevents):
# npm WARN notsup SKIPPING OPTIONAL DEPENDENCY: Unsupported platform for fsevents@2.1.3: wanted {"os":"darwin","arch":"any"} (current: {"os":"linux","arch":"x64"})
# npm WARN hello-dock@0.0.0 No description
# npm WARN hello-dock@0.0.0 No repository field.
# npm WARN hello-dock@0.0.0 No license field.
#
# added 327 packages from 301 contributors and audited 329 packages in 35.971s
#
# 26 packages are looking for funding
#   run `npm fund` for details
#
# found 0 vulnerabilities
#
# Removing intermediate container 6dfabf0ee9f8
#  ---&gt; 21fd1b065314
# Step 5/9 : COPY . .
#  ---&gt; 43243f95bff7
# Step 6/9 : RUN npm run build
#  ---&gt; Running in 4d918cf18584
#
# &gt; hello-dock@0.0.0 build /app
# &gt; vite build
#
# - Building production bundle...
#
# [write] dist/index.html 0.39kb, brotli: 0.15kb
# [write] dist/_assets/docker-handbook-github.3adb4865.webp 12.32kb
# [write] dist/_assets/index.eabcae90.js 42.56kb, brotli: 15.40kb
# [write] dist/_assets/style.0637ccc5.css 0.16kb, brotli: 0.10kb
# - Building production bundle...
#
# Build completed in 1.71s.
#
# Removing intermediate container 4d918cf18584
#  ---&gt; 187fb3e82d0d
# Step 7/9 : EXPOSE 80
#  ---&gt; Running in b3aab5cf5975
# Removing intermediate container b3aab5cf5975
#  ---&gt; d6fcc058cfda
# Step 8/9 : FROM nginx:stable-alpine
# stable: Pulling from library/nginx
# 6ec7b7d162b2: Already exists 
# 43876acb2da3: Pull complete 
# 7a79edd1e27b: Pull complete 
# eea03077c87e: Pull complete 
# eba7631b45c5: Pull complete 
# Digest: sha256:2eea9f5d6fff078ad6cc6c961ab11b8314efd91fb8480b5d054c7057a619e0c3
# Status: Downloaded newer image for nginx:stable
#  ---&gt; 05f64a802c26
# Step 9/9 : COPY --from=builder /app/dist /usr/share/nginx/html
#  ---&gt; 8c6dfc34a10d
# Successfully built 8c6dfc34a10d
# Successfully tagged hello-dock:prod
</code></pre>
<p>生成镜像后，可以通过执行以下命令来运行新容器：</p>
<pre><code>docker container run \
    --rm \
    --detach \
    --name hello-dock-prod \
    --publish 8080:80 \
    hello-dock:prod

# 224aaba432bb09aca518fdd0365875895c2f5121eb668b2e7b2d5a99c019b953
</code></pre>
<p>正在运行的应用程序应位于 <code>http://127.0.0.1:8080</code> 上：</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2021/01/hello-dock.png" alt="hello-dock" width="600" height="400" loading="lazy"></p>
<p>在这里，可以看到我所有的 <code>hello-dock</code> 应用程序。 如果要构建具有大量依赖关系的大型应用程序，那么多阶段构建可能会非常有用。如果配置正确，则可以很好地优化和压缩分多个阶段构建的镜像。</p>
<h3 id="">如何忽略不必要的文件</h3>
<p>如果了解 <code>git</code>，你可能会知道项目中的 <code>.gitignore</code> 文件。 这些文件包含要从仓库中排除的文件和目录的列表。</p>
<p>嗯，Docker 也有类似的概念。<code>.dockerignore</code> 文件包含要从镜像构建中排除的文件和目录的列表。可以在 <code>hello-dock</code> 目录中有一个预先创建的 <code>.dockerignore</code> 文件。</p>
<pre><code>.git
*Dockerfile*
*docker-compose*
node_modules
</code></pre>
<p>该 <code>.dockerignore</code> 文件必须位于构建上下文中。这里提到的文件和目录将被 <code>COPY</code> 指令忽略。但是，如果执行绑定挂载，则 <code>.dockerignore</code> 文件将对此无效。我已经在项目仓库中的必要位置添加了 <code>.dockerignore</code> 文件。</p>
<h2 id="docker">Docker 中的网络操作基础知识</h2>
<p>到目前为止，在本书中，仅处理了单个容器项目。但是在实际应用中，多数项目都具有多个容器。老实说，如果不了解容器隔离的细微差别，使用一堆容器可能会有些困难。</p>
<p>因此，在本书的这一部分中，将介绍 Docker 的基本网络，并涉及一个小型的多容器项目。</p>
<p>好了，已经在上一节中了解到容器是隔离的环境。现在考虑一个场景，其中有一个基于 <a href="https://expressjs.com/">Express.js</a>  <code>notes-api</code> 应用程序和一个 <a href="https://www.postgresql.org/">PostgreSQL</a> 数据库服务，他们在两个单独的容器中运行。</p>
<p>这两个容器彼此完全隔离，并且彼此无关。<strong>那么如何连接两者？ 将是一个挑战。</strong></p>
<p>你可能会想到针对此问题的两种可能的解决方案。 它们如下：</p>
<ul>
<li>使用暴露的端口访问数据库服务。</li>
<li>使用其 IP 地址和默认端口访问数据库服务。</li>
</ul>
<p>第一个涉及从 <code>postgres</code> 容器暴露一个端口，<code>notes-api</code> 将通过该端口进行连接。假设来自 <code>postgres</code> 容器的暴露端口为 5432。现在，如果尝试从 <code>notes-api</code> 容器内部连接到 <code>127.0.0.1:5432</code>，会发现 <code>notes-api</code> 根本找不到数据库服务。</p>
<p>原因是，在 <code>notes-api</code> 容器内的 <code>127.0.0.1</code> 时，只是代表当前容器的 <code>localhost</code> 。<code>postgres</code> 服务根本不存在。结果是，<code>notes-api</code> 应用程序无法连接。</p>
<p>你可能想到的第二个解决方案找到  <code>postgres</code> 容器的确切 IP 地址，使用 <code>container inspect</code> 命令并将其与端口一起使用。 假设 <code>postgres</code>  容器的名称为 <code>notes-api-db-server</code> ，则可以通过执行以下命令轻松获得 IP 地址：</p>
<pre><code>docker container inspect --format='{{range .NetworkSettings.Networks}} {{.IPAddress}} {{end}}' notes-api-db-server

#  172.17.0.2
</code></pre>
<p>现在假设 <code>postgres</code> 的默认端口是 <code>5432</code>，可以通过从 <code>notes-api</code> 容器连接到 <code>172.17.0.2:5432</code> 来非常容易地访问数据库服务。</p>
<p>这种方法也存在问题。 不建议使用 IP 地址来引用容器。另外，如果容器被破坏并重新创建，则 IP 地址可能会更改。跟踪这些不断变化的 IP 地址可能非常麻烦。</p>
<p>现在，我已经排除了对原始问题的可能错误答案，正确的答案是，<strong>将它们放置在用户定义的桥接网络下即可将它们连接起来。</strong></p>
<h3 id="docker">Docker 网络基础</h3>
<p>Docker 中的网络是另一个逻辑对象，和容器和镜像一样。就像其他两个一样，在 <code>docker network</code> 组下有很多用于操纵网络的命令。</p>
<p>要列出系统中的网络，请执行以下命令：</p>
<pre><code>docker network ls

# NETWORK ID     NAME      DRIVER    SCOPE
# c2e59f2b96bd   bridge    bridge    local
# 124dccee067f   host      host      local
# 506e3822bf1f   none      null      local
</code></pre>
<p>你应该在系统中看到三个网络。现在在这里查看表的 <code>DRIVER</code> 列。这些 drivers 可以视为网络类型。</p>
<p>默认情况下，Docker 具有五类网络驱动。它们如下：</p>
<ul>
<li><code>bridge</code> - Docker 中的默认网络驱动程序。当多个容器以标准模式运行并且需要相互通信时，可以使用此方法。</li>
<li><code>host</code> -  完全消除网络隔离。在 <code>host</code>  网络下运行的任何容器基本上都连接到主机系统的网络。</li>
<li><code>none</code> - 此驱动程序完全禁用容器的联网。 我还没有找到其应用场景。</li>
<li><code>overlay</code> - 这用于跨计算机连接多个 Docker 守护程序，这超出了本书的范围。</li>
<li><code>macvlan</code> - 允许将 MAC 地址分配给容器，使它们的功能类似于网络中的物理设备。</li>
</ul>
<p>也有第三方插件，可让你将 Docker 与专用网络堆栈集成。在上述五种方法中，本书仅使用 <code>bridge</code> 网络驱动程序。</p>
<h3 id="docker">如何在 Docker 中创建用户定义的桥接网络</h3>
<p>在开始创建自己的桥接网络之前，我想花一些时间来讨论 Docker 随附的默认桥接网络。让我们首先列出系统上的所有网络：</p>
<pre><code>docker network ls

# NETWORK ID     NAME      DRIVER    SCOPE
# c2e59f2b96bd   bridge    bridge    local
# 124dccee067f   host      host      local
# 506e3822bf1f   none      null      local
</code></pre>
<p>如你所见，Docker 随附了一个名为 <code>bridge</code> 的默认桥接网络。 运行的任何容器都将自动连接到此网桥网络：</p>
<pre><code>docker container run --rm --detach --name hello-dock --publish 8080:80 fhsinchy/hello-dock
# a37f723dad3ae793ce40f97eb6bb236761baa92d72a2c27c24fc7fda0756657d

docker network inspect --format='{{range .Containers}}{{.Name}}{{end}}' bridge
# hello-dock
</code></pre>
<p>连接到默认桥接网络的容器可以使用我在上一小节中不鼓励使用的 IP 地址相互通信。</p>
<p>但是，用户定义的桥接网络比默认桥接网络多一些额外的功能。根据有关此主题的官方 <a href="https://docs.docker.com/network/bridge/#differences-between-user-defined-bridges-and-the-default-bridge">docs</a>，一些值得注意的额外功能如下：</p>
<ul>
<li><strong>用户定义的网桥可在容器之间提供自动 DNS 解析：</strong> 这意味着连接到同一网络的容器可以使用容器名称相互通信。 因此，如果你有两个名为 <code>notes-api</code> 和 <code>notes-db</code> 的容器，则 API 容器将能够使用 <code>notes-db</code> 名称连接到数据库容器。</li>
<li><strong>用户定义的网桥提供更好的隔离性：</strong> 默认情况下，所有容器都连接到默认桥接网络，这可能会导致它们之间的冲突。将容器连接到用户定义的桥可以确保更好的隔离。</li>
<li><strong>容器可以即时与用户定义的网络连接和分离：</strong> 在容器的生命周期内，可以即时将其与用户定义的网络连接或断开连接。要从默认网桥网络中删除容器，需要停止容器并使用其他网络选项重新创建它。</li>
</ul>
<p>既然已经了解了很多有关用户定义的网络的知识，那么现在该为自己创建一个了。可以使用 <code>network create</code> 命令创建网络。该命令的通用语法如下：</p>
<pre><code>docker network create &lt;network name&gt;
</code></pre>
<p>要创建名称为 <code>skynet</code> 的网络，请执行以下命令：</p>
<pre><code>docker network create skynet

# 7bd5f351aa892ac6ec15fed8619fc3bbb95a7dcdd58980c28304627c8f7eb070

docker network ls

# NETWORK ID     NAME     DRIVER    SCOPE
# be0cab667c4b   bridge   bridge    local
# 124dccee067f   host     host      local
# 506e3822bf1f   none     null      local
# 7bd5f351aa89   skynet   bridge    local
</code></pre>
<p>如你所见，已经使用给定名称创建了一个新网络。当前没有容器连接到该网络。在下一小节中，将学习有关将容器连接到网络的信息。</p>
<h3 id="docker">如何在 Docker 中将容器连接到网络</h3>
<p>将容器连接到网络的方式主要有两种。首先，可以使用 network connect 命令将容器连接到网络。该命令的通用语法如下：</p>
<pre><code>docker network connect &lt;network identifier&gt; &lt;container identifier&gt;
</code></pre>
<p>要将 <code>hello-dock</code> 容器连接到 <code>skynet</code> 网络，可以执行以下命令：</p>
<pre><code>docker network connect skynet hello-dock

docker network inspect --format='{{range .Containers}} {{.Name}} {{end}}' skynet

#  hello-dock

docker network inspect --format='{{range .Containers}} {{.Name}} {{end}}' bridge

#  hello-dock
</code></pre>
<p>从两个  <code>network inspect</code>  命令的输出中可以看到，<code>hello-dock</code> 容器现在已连接到 <code>skynet</code> 和默认的 <code>bridge</code> 网络。</p>
<p>将容器连接到网络的第二种方法是对 <code>container run</code>  或 <code>container create</code> 命令使用 <code>--network</code> 选项。 该选项的通用语法如下：</p>
<pre><code>--network &lt;network identifier&gt;
</code></pre>
<p>要运行连接到同一网络的另一个 <code>hello-dock</code> 容器，可以执行以下命令：</p>
<pre><code>docker container run --network skynet --rm --name alpine-box -it alpine sh

# lands you into alpine linux shell

/ # ping hello-dock

# PING hello-dock (172.18.0.2): 56 data bytes
# 64 bytes from 172.18.0.2: seq=0 ttl=64 time=0.191 ms
# 64 bytes from 172.18.0.2: seq=1 ttl=64 time=0.103 ms
# 64 bytes from 172.18.0.2: seq=2 ttl=64 time=0.139 ms
# 64 bytes from 172.18.0.2: seq=3 ttl=64 time=0.142 ms
# 64 bytes from 172.18.0.2: seq=4 ttl=64 time=0.146 ms
# 64 bytes from 172.18.0.2: seq=5 ttl=64 time=0.095 ms
# 64 bytes from 172.18.0.2: seq=6 ttl=64 time=0.181 ms
# 64 bytes from 172.18.0.2: seq=7 ttl=64 time=0.138 ms
# 64 bytes from 172.18.0.2: seq=8 ttl=64 time=0.158 ms
# 64 bytes from 172.18.0.2: seq=9 ttl=64 time=0.137 ms
# 64 bytes from 172.18.0.2: seq=10 ttl=64 time=0.145 ms
# 64 bytes from 172.18.0.2: seq=11 ttl=64 time=0.138 ms
# 64 bytes from 172.18.0.2: seq=12 ttl=64 time=0.085 ms

--- hello-dock ping statistics ---
13 packets transmitted, 13 packets received, 0% packet loss
round-trip min/avg/max = 0.085/0.138/0.191 ms
</code></pre>
<p>如你所见，从 <code>alpine-box</code> 容器内部运行 <code>ping hello-dock</code> 是可行的，因为这两个容器都在同一用户定义的网桥网络下，并且自动 DNS 解析有效。</p>
<p>但是请记住，为了使自动 DNS 解析正常工作，必须为容器分配自定义名称。使用随机生成的名称将不起作用。</p>
<h3 id="docker">如何在 Docker 中从网络分离容器</h3>
<p>在上一小节中，了解了有关将容器连接到网络的信息。在本小节中，将学习如何分离它们。</p>
<p>可以使用 <code>network disconnect</code> 命令来执行此任务。该命令的通用语法如下：</p>
<pre><code>docker network disconnect &lt;network identifier&gt; &lt;container identifier&gt;
</code></pre>
<p>要从 <code>skynet</code> 网络分离 <code>hello-dock</code> 容器，可以执行以下命令：</p>
<pre><code>docker network disconnect skynet hello-dock
</code></pre>
<p>就像 <code>network connect</code> 命令一样，<code>network disconnect</code>  命令也不给出任何输出。</p>
<h3 id="docker">如何删除 Docker 中的网络</h3>
<p>就像 Docker 中的其他逻辑对象一样，可以使用 <code>network rm</code> 命令删除网络。该命令的通用语法如下：</p>
<pre><code>docker network rm &lt;network identifier&gt;
</code></pre>
<p>要从系统中删除 <code>skynet</code> 网络，可以执行以下命令：</p>
<pre><code>docker network rm skynet
</code></pre>
<p>也可以使用 <code>network prune</code> 命令从系统中删除所有未使用的网络。该命令还具有 <code>-f</code> 或 <code>--force</code> 和 <code>-a</code> 或 <code>--all</code> 选项。</p>
<h2 id="javascript">如何容器化多容器 JavaScript 应用程序</h2>
<p>既然已经对 Docker 中的网络有了足够的了解，那么在本节中，将学习如何将成熟的多容器项目容器化。涉及的项目是一个基于 Express.js 和 PostgreSQL 的简单 <code>notes-api</code>。</p>
<p>在此项目中，需要使用网络连接两个容器。除此之外，还将学习诸如环境变量和命名卷之类的概念。因此，事不宜迟，让我们直接开始。</p>
<h3 id="">如何运行数据库服务</h3>
<p>该项目中的数据库服务器是一个简单的 PostgreSQL 服务，使用官方的 <a href="https://hub.docker.com/_/postgres">postgres</a> 镜像。</p>
<p>根据官方文档，为了使用此镜像运行容器，必须提供 <code>POSTGRES_PASSWORD</code> 环境变量。除此之外，还将使用 <code>POSTGRES_DB</code> 环境变量为默认数据库提供一个名称。默认情况下，PostgreSQL 监听 <code>5432</code> 端口，因此也需要公开它。</p>
<p>要运行数据库服务，可以执行以下命令：</p>
<pre><code>docker container run \
    --detach \
    --name=notes-db \
    --env POSTGRES_DB=notesdb \
    --env POSTGRES_PASSWORD=secret \
    --network=notes-api-network \
    postgres:12

# a7b287d34d96c8e81a63949c57b83d7c1d71b5660c87f5172f074bd1606196dc

docker container ls

# CONTAINER ID   IMAGE         COMMAND                  CREATED              STATUS              PORTS      NAMES
# a7b287d34d96   postgres:12   "docker-entrypoint.s…"   About a minute ago   Up About a minute   5432/tcp   notes-db
</code></pre>
<p><code>container run</code>  和 <code>container create</code> 命令的 <code>--env</code> 选项可用于向容器提供环境变量。如你所见，数据库容器已成功创建并且正在运行。</p>
<p>尽管容器正在运行，但是存在一个小问题。 PostgreSQL、MongoDB 和 MySQL 等数据库将其数据保留在目录中。 PostgreSQL使用容器内的 <code>/var/lib/postgresql/data</code> 目录来持久化数据。</p>
<p>现在，如果容器由于某种原因被破坏怎么办？ 将丢失所有数据。为了解决此问题，可以使用命名卷。</p>
<h3 id="docker">如何在 Docker 中使用命名卷</h3>
<p>之前，已经使用了绑定挂载和匿名卷。命名卷与匿名卷非常相似，不同之处在于可以使用其名称引用命名卷。</p>
<p>卷也是 Docker 中的逻辑对象，可以使用命令行进行操作。<code>volume create</code> 命令可用于创建命名卷。</p>
<p>该命令的通用语法如下：</p>
<pre><code>docker volume create &lt;volume name&gt;
</code></pre>
<p>要创建一个名为 <code>notes-db-data</code> 的卷，可以执行以下命令：</p>
<pre><code>docker volume create notes-db-data

# notes-db-data

docker volume ls

# DRIVER    VOLUME NAME
# local     notes-db-data
</code></pre>
<p>这个卷现在可以被安装到 <code>notes-db</code> 容器中的 <code>/var/lib/postgresql/data</code> 中。为此，请停止并删除 <code>notes-db</code> 容器：</p>
<pre><code>docker container stop notes-db

# notes-db

docker container rm notes-db

# notes-db
</code></pre>
<p>现在运行一个新容器，并使用 <code>--volume</code> 或 <code>-v</code> 选项分配卷。</p>
<pre><code>docker container run \
    --detach \
    --volume notes-db-data:/var/lib/postgresql/data \
    --name=notes-db \
    --env POSTGRES_DB=notesdb \
    --env POSTGRES_PASSWORD=secret \
    --network=notes-api-network \
    postgres:12

# 37755e86d62794ed3e67c19d0cd1eba431e26ab56099b92a3456908c1d346791
</code></pre>
<p>现在检查 <code>notes-db</code> 容器以确保安装成功：</p>
<pre><code>docker container inspect --format='{{range .Mounts}} {{ .Name }} {{end}}' notes-db

#  notes-db-data
</code></pre>
<p>现在，这些数据将安全地存储在 <code>notes-db-data</code> 卷中，并且将来可以重复使用。在这里也可以使用绑定挂载代替命名卷，但是在这种情况下，我更喜欢使用命名卷。</p>
<h3 id="docker">如何从 Docker 中的容器访问日志</h3>
<p>为了查看来自容器的日志，可以使用 <code>container logs</code>  命令。 该命令的通用语法如下：</p>
<pre><code>docker container logs &lt;container identifier&gt;
</code></pre>
<p>要从 <code>notes-db</code> 容器访问日志，可以执行以下命令：</p>
<pre><code>docker container logs notes-db

# The files belonging to this database system will be owned by user "postgres".
# This user must also own the server process.

# The database cluster will be initialized with locale "en_US.utf8".
# The default database encoding has accordingly been set to "UTF8".
# The default text search configuration will be set to "english".
#
# Data page checksums are disabled.
#
# fixing permissions on existing directory /var/lib/postgresql/data ... ok
# creating subdirectories ... ok
# selecting dynamic shared memory implementation ... posix
# selecting default max_connections ... 100
# selecting default shared_buffers ... 128MB
# selecting default time zone ... Etc/UTC
# creating configuration files ... ok
# running bootstrap script ... ok
# performing post-bootstrap initialization ... ok
# syncing data to disk ... ok
#
#
# Success. You can now start the database server using:
#
#     pg_ctl -D /var/lib/postgresql/data -l logfile start
#
# initdb: warning: enabling "trust" authentication for local connections
# You can change this by editing pg_hba.conf or using the option -A, or
# --auth-local and --auth-host, the next time you run initdb.
# waiting for server to start....2021-01-25 13:39:21.613 UTC [47] LOG:  starting PostgreSQL 12.5 (Debian 12.5-1.pgdg100+1) on x86_64-pc-linux-gnu, compiled by gcc (Debian 8.3.0-6) 8.3.0, 64-bit
# 2021-01-25 13:39:21.621 UTC [47] LOG:  listening on Unix socket "/var/run/postgresql/.s.PGSQL.5432"
# 2021-01-25 13:39:21.675 UTC [48] LOG:  database system was shut down at 2021-01-25 13:39:21 UTC
# 2021-01-25 13:39:21.685 UTC [47] LOG:  database system is ready to accept connections
#  done
# server started
# CREATE DATABASE
#
#
# /usr/local/bin/docker-entrypoint.sh: ignoring /docker-entrypoint-initdb.d/*
#
# 2021-01-25 13:39:22.008 UTC [47] LOG:  received fast shutdown request
# waiting for server to shut down....2021-01-25 13:39:22.015 UTC [47] LOG:  aborting any active transactions
# 2021-01-25 13:39:22.017 UTC [47] LOG:  background worker "logical replication launcher" (PID 54) exited with exit code 1
# 2021-01-25 13:39:22.017 UTC [49] LOG:  shutting down
# 2021-01-25 13:39:22.056 UTC [47] LOG:  database system is shut down
#  done
# server stopped
#
# PostgreSQL init process complete; ready for start up.
#
# 2021-01-25 13:39:22.135 UTC [1] LOG:  starting PostgreSQL 12.5 (Debian 12.5-1.pgdg100+1) on x86_64-pc-linux-gnu, compiled by gcc (Debian 8.3.0-6) 8.3.0, 64-bit
# 2021-01-25 13:39:22.136 UTC [1] LOG:  listening on IPv4 address "0.0.0.0", port 5432
# 2021-01-25 13:39:22.136 UTC [1] LOG:  listening on IPv6 address "::", port 5432
# 2021-01-25 13:39:22.147 UTC [1] LOG:  listening on Unix socket "/var/run/postgresql/.s.PGSQL.5432"
# 2021-01-25 13:39:22.177 UTC [75] LOG:  database system was shut down at 2021-01-25 13:39:22 UTC
# 2021-01-25 13:39:22.190 UTC [1] LOG:  database system is ready to accept connections
</code></pre>
<p>从第 57 行的文本可以看出，数据库已启动，并准备接受来自外部的连接。该命令还有 <code>--follow</code> 或 <code>-f</code> 选项，使可以将控制台连接到日志输出并获得连续的文本流。</p>
<h3 id="docker">如何在 Docker 中创建网络并连接数据库服务</h3>
<p>如在上一节中所学，容器必须连接到用户定义的桥接网络，才能使用容器名称相互通信。为此，请在系统中创建一个名为 <code>notes-api-network</code> 的网络：</p>
<pre><code>docker network create notes-api-network
</code></pre>
<p>现在，通过执行以下命令，将 <code>notes-db</code> 容器连接到该网络：</p>
<pre><code>docker network connect notes-api-network notes-db
</code></pre>
<h3 id="dockerfile">如何编写 Dockerfile</h3>
<p>转到克隆项目代码的目录。在其中，进入 <code>notes-api/api</code> 目录，并创建一个新的 <code>Dockerfile</code>。 将以下代码放入文件中：</p>
<pre><code># stage one
FROM node:lts-alpine as builder

# install dependencies for node-gyp
RUN apk add --no-cache python make g++

WORKDIR /app

COPY ./package.json .
RUN npm install --only=prod

# stage two
FROM node:lts-alpine

EXPOSE 3000
ENV NODE_ENV=production

USER node
RUN mkdir -p /home/node/app
WORKDIR /home/node/app

COPY . .
COPY --from=builder /app/node_modules  /home/node/app/node_modules

CMD [ "node", "bin/www" ]
</code></pre>
<p>这是一个多阶段构建。第一阶段用于使用 <code>node-gyp</code> 构建和安装依赖项，第二阶段用于运行应用程序。我将简要介绍以下步骤：</p>
<ul>
<li>阶段1使用 <code>node：lts-alpine</code> 作为基础，并使用 <code>builder</code> 作为阶段名称。</li>
<li>在第 5 行，安装了  <code>python</code>、<code>make</code> 和 <code>g++</code>。<code>node-gyp</code> 工具需要这三个软件包才能运行。</li>
<li>在第 7 行，我们将 <code>/app</code>  目录设置为 <code>WORKDIR</code>。</li>
<li>在第 9 和 10 行，将 <code>package.json</code> 文件复制到 <code>WORKDIR</code> 并安装所有依赖项。</li>
<li>第 2 阶段还使用 <code>node-lts：alpine</code> 作为基础镜像。</li>
<li>在第 16 行，将环境变量 <code>NODE_ENV</code> 设置为 <code>production</code>。 这对于 API 正常运行很重要。</li>
<li>从第 18 行到第 20 行，将默认用户设置为 <code>node</code>，创建 <code>/home/node/app</code> 目录，并将其设置为 <code>WORKDIR</code>。</li>
<li>在第 22 行，复制了所有项目文件，在第 23 行，从 <code>builder</code>  阶段复制了 <code>node_modules</code> 目录。此目录包含运行应用程序所需的所有已构建依赖关系。</li>
<li>在第 25 行，设置了默认命令。</li>
</ul>
<p>要从此 <code>Dockerfile</code> 构建镜像，可以执行以下命令：</p>
<pre><code>docker image build --tag notes-api .

# Sending build context to Docker daemon  37.38kB
# Step 1/14 : FROM node:lts-alpine as builder
#  ---&gt; 471e8b4eb0b2
# Step 2/14 : RUN apk add --no-cache python make g++
#  ---&gt; Running in 5f20a0ecc04b
# fetch http://dl-cdn.alpinelinux.org/alpine/v3.11/main/x86_64/APKINDEX.tar.gz
# fetch http://dl-cdn.alpinelinux.org/alpine/v3.11/community/x86_64/APKINDEX.tar.gz
# (1/21) Installing binutils (2.33.1-r0)
# (2/21) Installing gmp (6.1.2-r1)
# (3/21) Installing isl (0.18-r0)
# (4/21) Installing libgomp (9.3.0-r0)
# (5/21) Installing libatomic (9.3.0-r0)
# (6/21) Installing mpfr4 (4.0.2-r1)
# (7/21) Installing mpc1 (1.1.0-r1)
# (8/21) Installing gcc (9.3.0-r0)
# (9/21) Installing musl-dev (1.1.24-r3)
# (10/21) Installing libc-dev (0.7.2-r0)
# (11/21) Installing g++ (9.3.0-r0)
# (12/21) Installing make (4.2.1-r2)
# (13/21) Installing libbz2 (1.0.8-r1)
# (14/21) Installing expat (2.2.9-r1)
# (15/21) Installing libffi (3.2.1-r6)
# (16/21) Installing gdbm (1.13-r1)
# (17/21) Installing ncurses-terminfo-base (6.1_p20200118-r4)
# (18/21) Installing ncurses-libs (6.1_p20200118-r4)
# (19/21) Installing readline (8.0.1-r0)
# (20/21) Installing sqlite-libs (3.30.1-r2)
# (21/21) Installing python2 (2.7.18-r0)
# Executing busybox-1.31.1-r9.trigger
# OK: 212 MiB in 37 packages
# Removing intermediate container 5f20a0ecc04b
#  ---&gt; 637ca797d709
# Step 3/14 : WORKDIR /app
#  ---&gt; Running in 846361b57599
# Removing intermediate container 846361b57599
#  ---&gt; 3d58a482896e
# Step 4/14 : COPY ./package.json .
#  ---&gt; 11b387794039
# Step 5/14 : RUN npm install --only=prod
#  ---&gt; Running in 2e27e33f935d
#  added 269 packages from 220 contributors and audited 1137 packages in 140.322s
#
# 4 packages are looking for funding
#   run `npm fund` for details
#
# found 0 vulnerabilities
#
# Removing intermediate container 2e27e33f935d
#  ---&gt; eb7cb2cb0b20
# Step 6/14 : FROM node:lts-alpine
#  ---&gt; 471e8b4eb0b2
# Step 7/14 : EXPOSE 3000
#  ---&gt; Running in 4ea24f871747
# Removing intermediate container 4ea24f871747
#  ---&gt; 1f0206f2f050
# Step 8/14 : ENV NODE_ENV=production
#  ---&gt; Running in 5d40d6ac3b7e
# Removing intermediate container 5d40d6ac3b7e
#  ---&gt; 31f62da17929
# Step 9/14 : USER node
#  ---&gt; Running in 0963e1fb19a0
# Removing intermediate container 0963e1fb19a0
#  ---&gt; 0f4045152b1c
# Step 10/14 : RUN mkdir -p /home/node/app
#  ---&gt; Running in 0ac591b3adbd
# Removing intermediate container 0ac591b3adbd
#  ---&gt; 5908373dfc75
# Step 11/14 : WORKDIR /home/node/app
#  ---&gt; Running in 55253b62ff57
# Removing intermediate container 55253b62ff57
#  ---&gt; 2883cdb7c77a
# Step 12/14 : COPY . .
#  ---&gt; 8e60893a7142
# Step 13/14 : COPY --from=builder /app/node_modules  /home/node/app/node_modules
#  ---&gt; 27a85faa4342
# Step 14/14 : CMD [ "node", "bin/www" ]
#  ---&gt; Running in 349c8ca6dd3e
# Removing intermediate container 349c8ca6dd3e
#  ---&gt; 9ea100571585
# Successfully built 9ea100571585
# Successfully tagged notes-api:latest
</code></pre>
<p>在使用该镜像运行容器之前，请确保数据库容器正在运行，并且已附加到 <code>notes-api-network</code> 上。</p>
<pre><code>docker container inspect notes-db

# [
#     {
#         ...
#         "State": {
#             "Status": "running",
#             "Running": true,
#             "Paused": false,
#             "Restarting": false,
#             "OOMKilled": false,
#             "Dead": false,
#             "Pid": 11521,
#             "ExitCode": 0,
#             "Error": "",
#             "StartedAt": "2021-01-26T06:55:44.928510218Z",
#             "FinishedAt": "2021-01-25T14:19:31.316854657Z"
#         },
#         ...
#         "Mounts": [
#             {
#                 "Type": "volume",
#                 "Name": "notes-db-data",
#                 "Source": "/var/lib/docker/volumes/notes-db-data/_data",
#                 "Destination": "/var/lib/postgresql/data",
#                 "Driver": "local",
#                 "Mode": "z",
#                 "RW": true,
#                 "Propagation": ""
#             }
#         ],
#         ...
#         "NetworkSettings": {
#             ...
#             "Networks": {
#                 "bridge": {
#                     "IPAMConfig": null,
#                     "Links": null,
#                     "Aliases": null,
#                     "NetworkID": "e4c7ce50a5a2a49672155ff498597db336ecc2e3bbb6ee8baeebcf9fcfa0e1ab",
#                     "EndpointID": "2a2587f8285fa020878dd38bdc630cdfca0d769f76fc143d1b554237ce907371",
#                     "Gateway": "172.17.0.1",
#                     "IPAddress": "172.17.0.2",
#                     "IPPrefixLen": 16,
#                     "IPv6Gateway": "",
#                     "GlobalIPv6Address": "",
#                     "GlobalIPv6PrefixLen": 0,
#                     "MacAddress": "02:42:ac:11:00:02",
#                     "DriverOpts": null
#                 },
#                 "notes-api-network": {
#                     "IPAMConfig": {},
#                     "Links": null,
#                     "Aliases": [
#                         "37755e86d627"
#                     ],
#                     "NetworkID": "06579ad9f93d59fc3866ac628ed258dfac2ed7bc1a9cd6fe6e67220b15d203ea",
#                     "EndpointID": "5b8f8718ec9a5ec53e7a13cce3cb540fdf3556fb34242362a8da4cc08d37223c",
#                     "Gateway": "172.18.0.1",
#                     "IPAddress": "172.18.0.2",
#                     "IPPrefixLen": 16,
#                     "IPv6Gateway": "",
#                     "GlobalIPv6Address": "",
#                     "GlobalIPv6PrefixLen": 0,
#                     "MacAddress": "02:42:ac:12:00:02",
#                     "DriverOpts": {}
#                 }
#             }
#         }
#     }
# ]
</code></pre>
<p>已经缩短了输出，以便于在此处查看。在我的系统上，<code>notes-db</code> 容器正在运行，使用 <code>notes-db-data</code> 卷，并连接到 <code>notes-api-network</code> 桥接网络。</p>
<p>一旦确定一切就绪，就可以通过执行以下命令来运行新容器：</p>
<pre><code>docker container run \
    --detach \
    --name=notes-api \
    --env DB_HOST=notes-db \
    --env DB_DATABASE=notesdb \
    --env DB_PASSWORD=secret \
    --publish=3000:3000 \
    --network=notes-api-network \
    notes-api
    
# f9ece420872de99a060b954e3c236cbb1e23d468feffa7fed1e06985d99fb919
</code></pre>
<p>应该理解这个长命令，因此只简要介绍下环境变量。</p>
<p><code>notes-api</code> 应用程序需要设置三个环境变量。 它们如下：</p>
<ul>
<li><code>DB_HOST</code> - 这是数据库服务的主机。假定数据库服务和 API 都连接到同一用户定义的桥接网络，则可以使用其容器名称（在这种情况下为 <code>notes-db</code>）引用数据库服务。</li>
<li><code>DB_DATABASE</code> - 此API将使用的数据库。 在<a href="https://www.freecodecamp.org/news/@fhsinchy/s/the-docker-handbook/~/drafts/-MS2MtB5zjVVjK3Ujaz4/containerizing-a-multi-container-javascript-application#running-the-database-server">运行数据库服务</a>小节，使用环境变量 <code>POSTGRES_DB</code> 将默认数据库名称设置为 <code>notesdb</code>。 将在这里使用它。</li>
<li><code>DB_PASSWORD</code> - 连接数据库的密码。 这也在运行数据库服务小节涉及，使用环境变量<code>POSTGRES_PASSWORD</code>。</li>
</ul>
<p>要检查容器是否正常运行，可以使用 <code>container ls</code> 命令：</p>
<pre><code>docker container ls

# CONTAINER ID   IMAGE         COMMAND                  CREATED          STATUS          PORTS                    NAMES
# f9ece420872d   notes-api     "docker-entrypoint.s…"   12 minutes ago   Up 12 minutes   0.0.0.0:3000-&gt;3000/tcp   notes-api
# 37755e86d627   postgres:12   "docker-entrypoint.s…"   17 hours ago     Up 14 minutes   5432/tcp                 notes-db
</code></pre>
<p>容器正在运行。 可以访问 <code>http://127.0.0.1:3000/</code> 来查看正在使用的API。</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2021/01/bonjour-mon-ami.png" alt="bonjour-mon-ami" width="600" height="400" loading="lazy"></p>
<p>该 API 总共有五个路由，可以在 <code>/notes/api/api/api/routes/notes.js</code>  文件中看到。 它是用我的一个<a href="https://github.com/fhsinchy/create-node-rocket-api">开源项目</a>引导的。</p>
<p>尽管容器正在运行，但是在开始使用它之前，还有最后一件事要做。必须运行设置数据库表所必需的数据库迁移，并且可以通过在容器内执行 <code>npm run db:migrate</code> 命令来执行此操作。</p>
<h3 id="">如何在正在运行的容器中执行命令</h3>
<p>已经了解了在停止的容器中执行命令的知识。另一种情况是在正在运行的容器内执行命令。</p>
<p>为此，必须使用 <code>exec</code> 命令在正在运行的容器内执行自定义命令。</p>
<p><code>exec</code> 命令的通用语法如下：</p>
<pre><code>docker container exec &lt;container identifier&gt; &lt;command&gt;
</code></pre>
<p>要执行 <code>notes-api</code> 容器内的 <code>npm run db:migrate</code>，可以执行以下命令：</p>
<pre><code>docker container exec notes-api npm run db:migrate

# &gt; notes-api@ db:migrate /home/node/app
# &gt; knex migrate:latest
#
# Using environment: production
# Batch 1 run: 1 migrations
</code></pre>
<p>如果要在正在运行的容器中运行交互式命令，则必须使用 <code>-it</code>  标志。例如，如果要访问在 <code>notes-api</code> 容器中运行的 shell，可以执行以下命令：</p>
<pre><code>docker container exec -it notes-api sh

# / # uname -a
# Linux b5b1367d6b31 5.10.9-201.fc33.x86_64 #1 SMP Wed Jan 20 16:56:23 UTC 2021 x86_64 Linux
</code></pre>
<h3 id="docker">如何在 Docker 中编写管理脚本</h3>
<p>管理多容器项目以及网络，卷和内容意味着编写大量命令。为了简化过程，我通常会从简单的 <a href="https://opensource.com/article/17/1/getting-started-shell-scripting">shell脚本</a>和 <a href="https://opensource.com/article/18/8/what-how-makefile">Makefile</a> 来提高效率。</p>
<p>可以在 <code>notes-api</code> 目录中找到四个 shell 脚本。 它们如下：</p>
<ul>
<li><code>boot.sh</code> - 用于启动容器（如果已存在）。</li>
<li><code>build.sh</code> - 创建并运行容器。如果需要，它还会创建镜像，卷和网络。</li>
<li><code>destroy.sh</code> - 删除与此项目关联的所有容器，卷和网络。</li>
<li><code>stop.sh</code> - 停止所有正在运行的容器。</li>
</ul>
<p>还有一个 <code>Makefile</code>，其中包含名为<code>start</code>、<code>stop</code>、<code>build</code>和 <code>destroy</code> 的四个目标，每个目标都调用前面提到的 shell 脚本。</p>
<p>如果容器在系统中处于运行状态，执行 <code>make stop</code>  将停止所有容器。 执行 <code>make destroy</code> 应该停止容器并删除所有东西。 确保正在 <code>notes-api</code> 目录中运行脚本：</p>
<pre><code>make destroy

# ./shutdown.sh
# stopping api container ---&gt;
# notes-api
# api container stopped ---&gt;

# stopping db container ---&gt;
# notes-db
# db container stopped ---&gt;

# shutdown script finished

# ./destroy.sh
# removing api container ---&gt;
# notes-api
# api container removed ---&gt;

# removing db container ---&gt;
# notes-db
# db container removed ---&gt;

# removing db data volume ---&gt;
# notes-db-data
# db data volume removed ---&gt;

# removing network ---&gt;
# notes-api-network
# network removed ---&gt;

# destroy script finished
</code></pre>
<p>如果遇到权限拒绝错误，请在脚本上执行 <code>chmod + x</code>：</p>
<pre><code>chmod +x boot.sh build.sh destroy.sh shutdown.sh
</code></pre>
<p>这里不解释这些脚本，因为它们是简单的 <code>if-else</code>  语句以及一些已经看过很多次的  Docker 命令。如果对 Linux Shell 有所了解，那么也应该能够理解这些脚本。</p>
<h2 id="dockercompose">如何使用 Docker-Compose 组合项目</h2>
<p>在上一节中，了解了有关管理多容器项目的困难。除了编写许多命令之外，还有一种更简单的方法来管理多容器项目，该工具叫作<a href="https://docs.docker.com/compose/">Docker Compose</a>。</p>
<p>根据 Docker 的<a href="https://docs.docker.com/compose/">文档</a> -</p>
<blockquote>
<p>Compose 是用于定义和运行多容器 Docker 应用程序的工具。通过 Compose，可以使用 YAML 文件来配置应用程序的服务。然后，使用一个命令，就可以从配置中创建并启动所有服务。</p>
</blockquote>
<p>尽管 Compose 可在所有环境中使用，但它更专注于开发和测试。完全不建议在生产环境上使用 Compose。</p>
<h3 id="dockercompose">Docker Compose 基础</h3>
<p>转至用来克隆本书随附仓库的目录。进入 <code>notes-api/api</code> 目录并创建 <code>Dockerfile.dev</code> 文件。将以下代码放入其中：</p>
<pre><code>
FROM node:lts-alpine as builder


RUN apk add --no-cache python make g++

WORKDIR /app

COPY ./package.json .
RUN npm install


FROM node:lts-alpine

ENV NODE_ENV=development

USER node
RUN mkdir -p /home/node/app
WORKDIR /home/node/app

COPY . .
COPY --from=builder /app/node_modules /home/node/app/node_modules

CMD [ "./node_modules/.bin/nodemon", "--config", "nodemon.json", "bin/www" ]
</code></pre>
<p>该代码与上一小节中使用的 <code>Dockerfile</code> 几乎相同。 此文件中的三个区别如下：</p>
<ul>
<li>在第 10 行中，执行 <code>npm install</code> 而不是 <code>npm run install --only = prod</code>，因为还需要开发依赖项。</li>
<li>在第 15 行，将环境变量 <code>NODE_ENV</code> 设置为  <code>development</code> 而不是 <code>production</code>。</li>
<li>在第 24 行，使用名为 <a href="https://nodemon.io/">nodemon</a> 的工具来获取 API 的热重载功能。</li>
</ul>
<p>已经知道该项目有两个容器：</p>
<ul>
<li><code>notes-db</code> - 一个基于 PostgreSQL 的数据库服务</li>
<li><code>notes-api</code> - 一个基于 Express.js 的 REST API</li>
</ul>
<p>在 Compose 的世界中，组成应用程序的每个容器都叫作服务。组合多容器项目的第一步就是定义这些服务。</p>
<p>就像 Docker 守护进程使用 <code>Dockerfile</code> 构建映像一样，Docker Compose 使用 <code>docker-compose.yaml</code> 文件从中读取服务定义。</p>
<p>转到 <code>notes-api</code> 目录并创建一个新的 <code>docker-compose.yaml</code> 文件。 将以下代码放入新创建的文件中：</p>
<pre><code>version: "3.8"

services: 
    db:
        image: postgres:12
        container_name: notes-db-dev
        volumes: 
            - notes-db-dev-data:/var/lib/postgresql/data
        environment:
            POSTGRES_DB: notesdb
            POSTGRES_PASSWORD: secret
    api:
        build:
            context: ./api
            dockerfile: Dockerfile.dev
        image: notes-api:dev
        container_name: notes-api-dev
        environment: 
            DB_HOST: db 
            DB_DATABASE: notesdb
            DB_PASSWORD: secret
        volumes: 
            - /home/node/app/node_modules
            - ./api:/home/node/app
        ports: 
            - 3000:3000

volumes:
    notes-db-dev-data:
        name: notes-db-dev-data
</code></pre>
<p>每个有效的 <code>docker-compose.yaml</code> 文件均通过定义文件版本开始。在撰写本文时，<code>3.8</code> 是最新版本。可以在<a href="https://docs.docker.com/compose/compose-file/">此处</a>查找最新版本。</p>
<p>YAML 文件中的块由缩进定义。将仔细介绍每个块，并解释它们的作用。</p>
<ul>
<li><code>services</code> 块包含应用程序中每个服务或容器的定义。<code>db</code> 和 <code>api</code> 是构成该项目的两个服务。</li>
<li><code>db</code>  块在应用程序中定义了一个新服务，并保存了启动容器所需的信息。每个服务都需要一个预先构建的镜像或一个 <code>Dockerfile</code> 来运行容器。 对于 <code>db</code> 服务，我们使用的是官方 PostgreSQL 镜像。</li>
<li>与 <code>db</code> 服务不同的是，不存在 <code>api</code> 服务的预构建镜像。因此，将使用 <code>Dockerfile.dev</code> 文件。</li>
<li><code>volumes</code>  块定义了任何服务所需的任何名称卷。当时，它仅登记 <code>db</code> 服务使用的是 <code>notes-db-dev-data</code> 卷。</li>
</ul>
<p>既然已经对 <code>docker-compose.yaml</code> 文件有了一个高层次的概述，那么让我们仔细看一下各个服务。</p>
<p><code>db</code> 服务的定义代码如下：</p>
<pre><code>db:
    image: postgres:12
    container_name: notes-db-dev
    volumes: 
        - db-data:/var/lib/postgresql/data
    environment:
        POSTGRES_DB: notesdb
        POSTGRES_PASSWORD: secret
</code></pre>
<ul>
<li><code>image</code> 键保存用于此容器的镜像仓库和标签。使用 <code>postgres:12</code> 镜像来运行数据库容器。</li>
<li><code>container_name</code> 指示容器的名称。默认情况下，容器使用 <code>&lt;project directory name&gt;_&lt;service name&gt;</code> 语法命名。可以使用 <code>container_name</code> 覆盖它。</li>
<li><code>volumes</code> 数组保存该服务的卷映射，并支持命名卷，匿名卷和绑定挂载。 语法 <code>&lt;source&gt;:&lt;destination&gt;</code> 与之前相同。</li>
<li><code>environment</code> map 包含服务所需的各种环境变量的值。</li>
</ul>
<p><code>api</code> 服务的定义代码如下：</p>
<pre><code>api:
    build:
        context: ./api
        dockerfile: Dockerfile.dev
    image: notes-api:dev
    container_name: notes-api-dev
    environment: 
        DB_HOST: db 
        DB_DATABASE: notesdb
        DB_PASSWORD: secret
    volumes: 
        - /home/node/app/node_modules
        - ./api:/home/node/app
    ports: 
        - 3000:3000
</code></pre>
<ul>
<li><code>api</code> 服务没有预构建的镜像。相反，它具有构建配置。在 <code>build</code> 块下，定义了用于构建镜像的上下文和 Dockerfile 的名称。到目前为止，应该已经了解了上下文和 Dockerfile，因此我不会花时间解释它们。</li>
<li><code>image</code> 键保存要构建的镜像的名称。如果未分配，则将使用 <code>&lt;project directory name&gt;_&lt;service name&gt;</code> 语法来命名镜像。</li>
<li>在 <code>environment</code> map 内部，<code>DB_HOST</code> 变量演示了 Compose 的功能。即，可以使用其名称引用同一应用程序中的另一服务。因此，此处的  <code>db</code>  将被 <code>api</code> 服务容器的 IP 地址代替。<code>DB_DATABASE</code> 和 <code>DB_PASSWORD</code>  变量必须分别与 <code>db</code> 服务定义中的 <code>POSTGRES_DB</code> 和 <code>POSTGRES_PASSWORD</code> 匹配。</li>
<li>在 <code>volumes</code> map 中，可以看到一个匿名卷和一个绑定挂载。语法与上一节中看到的相同。</li>
<li><code>ports</code> 映射定义了端口映射。 语法 <code>&lt;host port&gt;:&lt;container port&gt;</code> 与以前使用的 <code>--publish</code> 选项相同。</li>
</ul>
<p>最后，<code>volumes</code> 的代码如下：</p>
<pre><code>volumes:
    db-data:
        name: notes-db-dev-data
</code></pre>
<p>在此处定义服务中使用的命名卷。如果未定义名称，则将使用 <code>&lt;project directory name&gt;_&lt;volume key&gt;</code> 命名该卷，此处的密钥为 <code>db-data</code>。</p>
<p>可以在官方<a href="https://docs.docker.com/compose/compose-file/compose-file-v3/#volumes">文档</a>中了解有关卷配置的更多选项。</p>
<h3 id="dockercompose">如何在 Docker Compose 中启动服务</h3>
<p>有几种启动 YAML 文件中定义的服务的方法。将了解的第一个命令是 <code>up</code> 命令。<code>up</code> 命令可以构建所有丢失的镜像，创建容器，然后一次性启动它们。</p>
<p>在执行命令之前，请确保已在 <code>docker-compose.yaml</code> 文件所在的目录中打开了终端。 这对于执行的每个 <code>docker-compose</code> 命令都非常重要。</p>
<pre><code>docker-compose --file docker-compose.yaml up --detach

# Creating network "notes-api_default" with the default driver
# Creating volume "notes-db-dev-data" with default driver
# Building api
# Sending build context to Docker daemon  37.38kB
#
# Step 1/13 : FROM node:lts-alpine as builder
#  ---&gt; 471e8b4eb0b2
# Step 2/13 : RUN apk add --no-cache python make g++
#  ---&gt; Running in 197056ec1964
### LONG INSTALLATION STUFF GOES HERE ###
# Removing intermediate container 197056ec1964
#  ---&gt; 6609935fe50b
# Step 3/13 : WORKDIR /app
#  ---&gt; Running in 17010f65c5e7
# Removing intermediate container 17010f65c5e7
#  ---&gt; b10d12e676ad
# Step 4/13 : COPY ./package.json .
#  ---&gt; 600d31d9362e
# Step 5/13 : RUN npm install
#  ---&gt; Running in a14afc8c0743
### LONG INSTALLATION STUFF GOES HERE ###
#  Removing intermediate container a14afc8c0743
#  ---&gt; 952d5d86e361
# Step 6/13 : FROM node:lts-alpine
#  ---&gt; 471e8b4eb0b2
# Step 7/13 : ENV NODE_ENV=development
#  ---&gt; Running in 0d5376a9e78a
# Removing intermediate container 0d5376a9e78a
#  ---&gt; 910c081ce5f5
# Step 8/13 : USER node
#  ---&gt; Running in cfaefceb1eff
# Removing intermediate container cfaefceb1eff
#  ---&gt; 1480176a1058
# Step 9/13 : RUN mkdir -p /home/node/app
#  ---&gt; Running in 3ae30e6fb8b8
# Removing intermediate container 3ae30e6fb8b8
#  ---&gt; c391cee4b92c
# Step 10/13 : WORKDIR /home/node/app
#  ---&gt; Running in 6aa27f6b50c1
# Removing intermediate container 6aa27f6b50c1
#  ---&gt; 761a7435dbca
# Step 11/13 : COPY . .
#  ---&gt; b5d5c5bdf3a6
# Step 12/13 : COPY --from=builder /app/node_modules /home/node/app/node_modules
#  ---&gt; 9e1a19960420
# Step 13/13 : CMD [ "./node_modules/.bin/nodemon", "--config", "nodemon.json", "bin/www" ]
#  ---&gt; Running in 5bdd62236994
# Removing intermediate container 5bdd62236994
#  ---&gt; 548e178f1386
# Successfully built 548e178f1386
# Successfully tagged notes-api:dev
# Creating notes-api-dev ... done
# Creating notes-db-dev  ... done
</code></pre>
<p>这里的  <code>--detach</code> 或 <code>-d</code> 选项的功能与之前相同。仅当 YAML 文件未命名为 <code>docker-compose.yaml</code> 时才需要使用  <code>--file</code> 或  <code>-f</code> 选项（但我已在此处用于演示目的）。</p>
<p>除了 <code>up</code> 命令外，还有 <code>start</code> 命令。两者之间的主要区别在于，<code>start</code> 命令不会创建丢失的容器，而只会启动现有的容器。基本上与 <code>container start</code> 命令相同。</p>
<p><code>up</code> 命令的 <code>--build</code> 选项强制重建镜像。可以在官方<a href="https://docs.docker.com/compose/reference/up/">文档</a>中查阅 <code>up</code> 命令的更多选项。</p>
<h3 id="dockercompose">如何在 Docker Compose 中列表展示服务</h3>
<p>尽管可以使用 <code>container ls</code> 命令列出由 Compose 启动的服务容器，但是还可以用  <code>ps</code>  命令列出仅在 YAML 中定义的容器。</p>
<pre><code>docker-compose ps

#     Name                   Command               State           Ports         
# -------------------------------------------------------------------------------
# notes-api-dev   docker-entrypoint.sh ./nod ...   Up      0.0.0.0:3000-&gt;3000/tcp
# notes-db-dev    docker-entrypoint.sh postgres    Up      5432/tcp
</code></pre>
<p>它不如 <code>container ls</code> 输出的信息丰富，但是当同时运行大量容器时，它很有用。</p>
<h3 id="dockercompose">如何在 Docker Compose 正在运行的服务中执行命令</h3>
<p>我希望你记得上一部分，必须运行一些迁移脚本来为此 API 创建数据库表。</p>
<p>就像 <code>container exec</code> 命令一样，<code>docker-compose</code> 也有 <code>exec</code> 命令。该命令的通用语法如下：</p>
<pre><code>docker-compose exec &lt;service name&gt; &lt;command&gt;
</code></pre>
<p>要在 <code>api</code> 服务中执行 <code>npm run db:migrate</code> 命令，可以执行以下命令：</p>
<pre><code>docker-compose exec api npm run db:migrate

# &gt; notes-api@ db:migrate /home/node/app
# &gt; knex migrate:latest
# 
# Using environment: development
# Batch 1 run: 1 migrations
</code></pre>
<p>与 <code>container exec</code> 命令不同，不需要为交互式会话传递 <code>-it</code> 标志。<code>docker-compose</code> 是自动完成的。</p>
<h3 id="dockercompose">如何访问 Docker Compose 中正在运行的服务日志</h3>
<p>也可以使用 <code>logs</code> 命令从正在运行的服务中检索日志。该命令的通用语法如下：</p>
<pre><code>docker-compose logs &lt;service name&gt;
</code></pre>
<p>要从 <code>api</code> 服务访问日志，请执行以下命令：</p>
<pre><code>docker-compose logs api

# Attaching to notes-api-dev
# notes-api-dev | [nodemon] 2.0.7
# notes-api-dev | [nodemon] reading config ./nodemon.json
# notes-api-dev | [nodemon] to restart at any time, enter `rs`
# notes-api-dev | [nodemon] or send SIGHUP to 1 to restart
# notes-api-dev | [nodemon] ignoring: *.test.js
# notes-api-dev | [nodemon] watching path(s): *.*
# notes-api-dev | [nodemon] watching extensions: js,mjs,json
# notes-api-dev | [nodemon] starting `node bin/www`
# notes-api-dev | [nodemon] forking
# notes-api-dev | [nodemon] child pid: 19
# notes-api-dev | [nodemon] watching 18 files
# notes-api-dev | app running -&gt; http://127.0.0.1:3000
</code></pre>
<p>这只是日志输出的一部分。可以使用 <code>-f</code> 或 <code>--follow</code> 选项来钩住服务的输出流并实时获取日志。只要不按 <code>ctrl + c</code> 或关闭窗口退出，任何以后的日志都会立即显示在终端中。即使退出日志窗口，该容器也将继续运行。</p>
<h3 id="dockercompose">如何在 Docker Compose 中停止服务</h3>
<p>要停止服务，可以采用两种方法。第一个是 <code>down</code> 命令。<code>down</code> 命令将停止所有正在运行的容器并将其从系统中删除。它还会删除所有网络：</p>
<pre><code>docker-compose down --volumes

# Stopping notes-api-dev ... done
# Stopping notes-db-dev  ... done
# Removing notes-api-dev ... done
# Removing notes-db-dev  ... done
# Removing network notes-api_default
# Removing volume notes-db-dev-data
</code></pre>
<p><code>--volumes</code> 选项表示要删除 <code>volumes</code> 块中定义的所有命名卷。可以在官方<a href="https://docs.docker.com/compose/reference/down/">文档</a> 中查阅有关 <code>down</code> 命令的更多选用法。</p>
<p>另一个停止服务的命令是 <code>stop</code> 命令，其功能与  <code>container stop</code>  命令相同。它停止应用程序的所有容器并保留它们。这些容器可以稍后使用 <code>start</code> 或 <code>up</code> 命令启动。</p>
<h3 id="dockercompose">如何在 Docker Compose 中编写全栈应用程序</h3>
<p>在本小节中，我们将在 Notes API 中添加一个前端，并将其转变为一个完整的全栈应用程序。在本小节中，我将不解释 <code>Dockerfile.dev</code> 文件内容（除了关于 <code>nginx</code> 服务的部分），因为它们与上一小节中已经看到的其他文件相同。</p>
<p>如果已经克隆了项目代码仓库，进入 <code>fullstack-notes-application</code> 目录。项目根目录下的每个目录都包含每个服务的代码和相应的 <code>Dockerfile</code>。</p>
<p>在开始使用 <code>docker-compose.yaml</code> 文件之前，让我们看一下该应用程序的流程图：</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2021/01/fullstack-application-design.svg" alt="fullstack-application-design" width="600" height="400" loading="lazy"></p>
<p>与其像以前那样直接接受请求，在此应用程序中，所有请求都将首先由 NGINX（我们称其为路由器）服务接收。</p>
<p>然后，路由器将查看所请求的路径中是否包含 <code>/api</code>。如果是，则路由器会将请求路由到后端，否则，路由器会将请求路由到前端。</p>
<p>这样做是因为在运行前端应用程序时，它不会在容器中运行。 它在浏览器上运行，并通过容器提供服务。结果，Compose 网络无法按预期工作，并且前端应用程序无法找到 <code>api</code> 服务。</p>
<p>另一方面，NGINX 在容器内运行，并且可以与整个应用程序中的不同服务进行通信。</p>
<p>在这里介绍不 NGINX 的配置。该主题有点超出了本书的范围。如果你想了解，请继续阅读 <code>/notes-api/nginx/development.conf</code> 和 <code>/notes-api/nginx/production.conf</code> 文件。<code>/notes-api/nginx/Deockerfile.dev</code> 的代码如下：</p>
<pre><code>FROM nginx:stable-alpine

COPY ./development.conf /etc/nginx/conf.d/default.conf
</code></pre>
<p>它所做的只是将配置文件复制到容器内的 <code>/etc/nginx/conf.d/default.conf</code> 中。</p>
<p>让我们开始编写  <code>docker-compose.yaml</code> 文件。除了 <code>api</code> 和 <code>db</code> 服务之外，还有<code>client</code> 和 <code>nginx</code> 服务。还将很快介绍一些网络定义。</p>
<pre><code>version: "3.8"

services: 
    db:
        image: postgres:12
        container_name: notes-db-dev
        volumes: 
            - db-data:/var/lib/postgresql/data
        environment:
            POSTGRES_DB: notesdb
            POSTGRES_PASSWORD: secret
        networks:
            - backend
    api:
        build: 
            context: ./api
            dockerfile: Dockerfile.dev
        image: notes-api:dev
        container_name: notes-api-dev
        volumes: 
            - /home/node/app/node_modules
            - ./api:/home/node/app
        environment: 
            DB_HOST: db 
            DB_PORT: 5432
            DB_USER: postgres
            DB_DATABASE: notesdb
            DB_PASSWORD: secret
        networks:
            - backend
    client:
        build:
            context: ./client
            dockerfile: Dockerfile.dev
        image: notes-client:dev
        container_name: notes-client-dev
        volumes: 
            - /home/node/app/node_modules
            - ./client:/home/node/app
        networks:
            - frontend
    nginx:
        build:
            context: ./nginx
            dockerfile: Dockerfile.dev
        image: notes-router:dev
        container_name: notes-router-dev
        restart: unless-stopped
        ports: 
            - 8080:80
        networks:
            - backend
            - frontend

volumes:
    db-data:
        name: notes-db-dev-data

networks: 
    frontend:
        name: fullstack-notes-application-network-frontend
        driver: bridge
    backend:
        name: fullstack-notes-application-network-backend
        driver: bridge
</code></pre>
<p>该文件与之前用到的文件几乎相同。唯一需要说明的是网络配置。<code>networks</code> 块的代码如下：</p>
<pre><code>networks: 
    frontend:
        name: fullstack-notes-application-network-frontend
        driver: bridge
    backend:
        name: fullstack-notes-application-network-backend
        driver: bridge
</code></pre>
<p>我定义了两个桥接网络。默认情况下，Compose 创建一个桥接网络并将所有容器连接到该网络。但是，在这个项目中，我想要适当的网络隔离。 因此，我定义了两个网络，一个用于前端服务，一个用于后端服务。</p>
<p>我还在每个服务定义中添加了  <code>networks</code>  块。 这样，<code>api</code> 和 <code>db</code> 服务将被附加到同一个网络，而 <code>client</code> 服务将被附加到一个单独的网络。 但是 <code>nginx</code> 服务将同时连接到两个网络，因此它可以充当前端和后端服务之间的路由器。</p>
<p>通过执行以下命令来启动所有服务：</p>
<pre><code>docker-compose --file docker-compose.yaml up --detach

# Creating network "fullstack-notes-application-network-backend" with driver "bridge"
# Creating network "fullstack-notes-application-network-frontend" with driver "bridge"
# Creating volume "notes-db-dev-data" with default driver
# Building api
# Sending build context to Docker daemon  37.38kB
# 
# Step 1/13 : FROM node:lts-alpine as builder
#  ---&gt; 471e8b4eb0b2
# Step 2/13 : RUN apk add --no-cache python make g++
#  ---&gt; Running in 8a4485388fd3
### LONG INSTALLATION STUFF GOES HERE ###
# Removing intermediate container 8a4485388fd3
#  ---&gt; 47fb1ab07cc0
# Step 3/13 : WORKDIR /app
#  ---&gt; Running in bc76cc41f1da
# Removing intermediate container bc76cc41f1da
#  ---&gt; 8c03fdb920f9
# Step 4/13 : COPY ./package.json .
#  ---&gt; a1d5715db999
# Step 5/13 : RUN npm install
#  ---&gt; Running in fabd33cc0986
### LONG INSTALLATION STUFF GOES HERE ###
# Removing intermediate container fabd33cc0986
#  ---&gt; e09913debbd1
# Step 6/13 : FROM node:lts-alpine
#  ---&gt; 471e8b4eb0b2
# Step 7/13 : ENV NODE_ENV=development
#  ---&gt; Using cache
#  ---&gt; b7c12361b3e5
# Step 8/13 : USER node
#  ---&gt; Using cache
#  ---&gt; f5ac66ca07a4
# Step 9/13 : RUN mkdir -p /home/node/app
#  ---&gt; Using cache
#  ---&gt; 60094b9a6183
# Step 10/13 : WORKDIR /home/node/app
#  ---&gt; Using cache
#  ---&gt; 316a252e6e3e
# Step 11/13 : COPY . .
#  ---&gt; Using cache
#  ---&gt; 3a083622b753
# Step 12/13 : COPY --from=builder /app/node_modules /home/node/app/node_modules
#  ---&gt; Using cache
#  ---&gt; 707979b3371c
# Step 13/13 : CMD [ "./node_modules/.bin/nodemon", "--config", "nodemon.json", "bin/www" ]
#  ---&gt; Using cache
#  ---&gt; f2da08a5f59b
# Successfully built f2da08a5f59b
# Successfully tagged notes-api:dev
# Building client
# Sending build context to Docker daemon  43.01kB
# 
# Step 1/7 : FROM node:lts-alpine
#  ---&gt; 471e8b4eb0b2
# Step 2/7 : USER node
#  ---&gt; Using cache
#  ---&gt; 4be5fb31f862
# Step 3/7 : RUN mkdir -p /home/node/app
#  ---&gt; Using cache
#  ---&gt; 1fefc7412723
# Step 4/7 : WORKDIR /home/node/app
#  ---&gt; Using cache
#  ---&gt; d1470d878aa7
# Step 5/7 : COPY ./package.json .
#  ---&gt; Using cache
#  ---&gt; bbcc49475077
# Step 6/7 : RUN npm install
#  ---&gt; Using cache
#  ---&gt; 860a4a2af447
# Step 7/7 : CMD [ "npm", "run", "serve" ]
#  ---&gt; Using cache
#  ---&gt; 11db51d5bee7
# Successfully built 11db51d5bee7
# Successfully tagged notes-client:dev
# Building nginx
# Sending build context to Docker daemon   5.12kB
# 
# Step 1/2 : FROM nginx:stable-alpine
#  ---&gt; f2343e2e2507
# Step 2/2 : COPY ./development.conf /etc/nginx/conf.d/default.conf
#  ---&gt; Using cache
#  ---&gt; 02a55d005a98
# Successfully built 02a55d005a98
# Successfully tagged notes-router:dev
# Creating notes-client-dev ... done
# Creating notes-api-dev    ... done
# Creating notes-router-dev ... done
# Creating notes-db-dev     ... done
</code></pre>
<p>现在访问 <code>http://localhost:8080</code>，瞧瞧！</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2021/01/notes-application.png" alt="notes-application" width="600" height="400" loading="lazy"></p>
<p>尝试添加和删除注释，以查看应用程序是否正常运行。该项目还带有 shell 脚本和<code>Makefile</code>。研究一下他们，以了解如何像上一节中那样在没有 <code>docker-compose</code> 的帮助下运行该项目。</p>
<h2 id="">结语</h2>
<p>衷心感谢你花了宝贵的时间阅读本书。我希望你喜欢它并学到 Docker 的相关知识。</p>
<p>如果你喜欢我的文笔，则可以在<a href="https://books.farhan.info/">这里找到更多的的书</a>，我偶尔也写一些<a href="https://www.farhan.info/">博客</a>。</p>
<p>可以在 Twitter <a href="https://twitter.com/frhnhsin">@frhnhsin</a> 上关注我，也可以在 LinkedIn <a href="https://www.linkedin.com/in/farhanhasin/">/in/farhanhasin</a> 联系我。</p>
<!--kg-card-end: markdown--><p>原文：<a href="https://www.freecodecamp.org/news/the-docker-handbook/#how-to-install-docker">The Docker Handbook – 2021 Edition</a>，作者：<a href="https://www.freecodecamp.org/news/author/farhanhasin/">Farhan Hasin Chowdhury</a></p> ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ 如何获取 Docker 容器的 IP 地址 ]]>
                </title>
                <description>
                    <![CDATA[ Docker 提供了在隔离环境（容器）中打包和运行应用的能力。 你一定会想 - 得了吧，解释 Docker 的文章随处可见。 别担心，我们会跳过基础知识。本文的目标人群需要对 Docker 和容器有一定的了解。 但是你是否也曾好奇过应该怎样获得 Docker 容器的 IP 地址呢？ Docker 网络解释 首先来了解一下 Docker 的网络是如何工作的。首先是默认的bridge  网络。当使用 Docker 时，如果没有指定其它驱动默认会使用桥接网络。 Docker 网络图摘自了解Docker网络驱动程序及其用例 [https://www.docker.com/blog/understanding-docker-networking-drivers-use-cases/] bridge  网络是主机内部的专用网络，容器可以通过它进行通信。也可暴漏端口在外部访问。 单独容器中的应用通过桥接网络互相通讯时。 在上图中 db  和 web  可以通过用户创建的桥接网络 mybridge  互相通讯。 如果还没有在 Docker 中添加网络过，可以看到类似下面的信息 ]]>
                </description>
                <link>https://www.freecodecamp.org/chinese/news/how-to-get-a-docker-container-ip-address-explained-with-examples/</link>
                <guid isPermaLink="false">6034753ec354c605689ea6c2</guid>
                
                    <category>
                        <![CDATA[ Docker ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ ZhichengChen ]]>
                </dc:creator>
                <pubDate>Tue, 23 Feb 2021 03:00:00 +0000</pubDate>
                <media:content url="https://chinese.freecodecamp.org/news/content/images/2021/02/photo-1545935950-b7a28791ad7a.jpeg" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>Docker 提供了在隔离环境（容器）中打包和运行应用的能力。</p>
<p>你一定会想 - 得了吧，解释 Docker 的文章随处可见。</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2020/06/docker-i-see.jpg" alt="docker-i-see" width="600" height="400" loading="lazy"></p>
<p>别担心，我们会跳过基础知识。本文的目标人群需要对 Docker 和容器有一定的了解。</p>
<p>但是你是否也曾好奇过应该怎样获得 Docker 容器的 IP 地址呢？</p>
<h2 id="docker">Docker 网络解释</h2>
<p>首先来了解一下 Docker 的网络是如何工作的。首先是默认的<code>bridge</code> 网络。当使用 Docker 时，如果没有指定其它驱动默认会使用桥接网络。</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2020/06/docker-network.png" alt="docker-network" width="600" height="400" loading="lazy"></p>
<p>Docker 网络图摘自<a href="https://www.docker.com/blog/understanding-docker-networking-drivers-use-cases/">了解Docker网络驱动程序及其用例</a></p>
<p><code>bridge</code> 网络是主机内部的专用网络，容器可以通过它进行通信。也可暴漏端口在外部访问。</p>
<p>单独容器中的应用通过桥接网络互相通讯时。</p>
<p>在上图中 <code>db</code>  和  <code>web</code> 可以通过用户创建的桥接网络 <code>mybridge</code> 互相通讯。</p>
<p>如果还没有在 Docker 中添加网络过，可以看到类似下面的信息：</p>
<pre><code class="language-bash">
$ docker network ls

NETWORK ID          NAME                  DRIVER              SCOPE
c3cd46f397ce        bridge                bridge              local
ad4e4c24568e        host                  host                local
1c69593fc6ac        none                  null                local


</code></pre>
<p>默认的  <code>bridge</code> 网络显示在列表中，下面还有 <code>host</code>  和  <code>none</code>。我们会暂时忽略这两个网络，在接下来的例子使用  <code>bridge</code> 网络。</p>
<h2 id="dockerip">Docker 容器的 IP 地址</h2>
<p>默认情况下，会为连接到容器的每个 Docker 网络分配一个IP地址，并为每个网络分配一个默认的子网掩码，用作稍后分配 IP的地址池。</p>
<p>通常 Docker 默认使用 <strong>172.17. 0.0/16</strong> 作为容器网络的子网。</p>
<p>为了便于理解，运行一个真实的用例。</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2020/06/flamenco-done.png" alt="drawing" width="600" height="400" loading="lazy"></p>
<h3 id="docker">Docker 例子</h3>
<p>作为说明，我们会使用 Hive 和 Hadoop 环境，一共 5 个 Docker 容器。</p>
<p>检查将要执行的 <code>docker-compose.yml</code> 如下文件：</p>
<pre><code>version: "3"
services:
  namenode:
    image: bde2020/hadoop-namenode:2.0.0-hadoop2.7.4-java8
    volumes:
      - namenode:/hadoop/dfs/name
    environment:
      - CLUSTER_NAME=test
    env_file:
      - ./hadoop-hive.env
    ports:
      - "50070:50070"
  datanode:
    image: bde2020/hadoop-datanode:2.0.0-hadoop2.7.4-java8
    volumes:
      - datanode:/hadoop/dfs/data
    env_file:
      - ./hadoop-hive.env
    environment:
      SERVICE_PRECONDITION: "namenode:50070"
    ports:
      - "50075:50075"
  hive-server:
    image: bde2020/hive:2.3.2-postgresql-metastore
    env_file:
      - ./hadoop-hive.env
    environment:
      HIVE_CORE_CONF_javax_jdo_option_ConnectionURL: "jdbc:postgresql://hive-metastore/metastore"
      SERVICE_PRECONDITION: "hive-metastore:9083"
    ports:
      - "10000:10000"
  hive-metastore:
    image: bde2020/hive:2.3.2-postgresql-metastore
    env_file:
      - ./hadoop-hive.env
    command: /opt/hive/bin/hive --service metastore
    environment:
      SERVICE_PRECONDITION: "namenode:50070 datanode:50075 hive-metastore-postgresql:5432"
    ports:
      - "9083:9083"
  hive-metastore-postgresql:
    image: bde2020/hive-metastore-postgresql:2.3.0

</code></pre>
<p><a href="https://github.com/mesmacosta/docker-hive">代码源自  <strong>docker-hive</strong>  GitHub</a></p>
<p>没人想要阅读<strong>这么长</strong>的配置文件对吗？这是图解：</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2020/06/Screen-Shot-2020-06-21-at-2.48.18-PM.png" alt="Screen-Shot-2020-06-21-at-2.48.18-PM" width="600" height="400" loading="lazy"></p>
<p>好多了，来启动这些容器吧：</p>
<pre><code class="language-bash">docker-compose up -d

</code></pre>
<p>可以看到 5 个容器：</p>
<pre><code class="language-bash">
$ docker ps --format \
"table {{.ID}}\t{{.Status}}\t{{.Names}}"

CONTAINER ID        STATUS                   NAMES
158741ba0339        Up 1 minutes             dockerhive_hive-metastore-postgresql
607b00c25f29        Up 1 minutes             dockerhive_namenode
2a2247e49046        Up 1 minutes             dockerhive_hive-metastore
7f653d83f5d0        Up 1 minutes (healthy)   dockerhive_hive-server
75000c343eb7        Up 1 minutes (healthy)   dockerhive_datanode

</code></pre>
<p>接下来检查一下 Docker 的网络：</p>
<pre><code class="language-bash">
$ docker network ls

NETWORK ID          NAME                  DRIVER              SCOPE
c3cd46f397ce        bridge                bridge              local
9f6bc3c15568        docker-hive_default   bridge              local
ad4e4c24568e        host                  host                local
1c69593fc6ac        none                  null                local

</code></pre>
<p>等一下，怎么有一个没见过的 <code>docker-hive_default</code> 网络。</p>
<p>默认情况下，docker compose 会为应用设置一个网络。 应用的网络会根据 “project name” 来命名，该名称源自其所在目录的名称。</p>
<p>由于项目目录是 <code>docker-hive</code>，这就解释了新出现的网络。</p>
<p>接下来的例子解释如何获取容器的 IP 地址。</p>
<h2 id="ip">如何获得容器的 IP 地址 - 例子</h2>
<p>接下来，就是见证奇迹的时刻。</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2020/06/bermuda-logged-out-1.png" alt="drawing" width="600" height="400" loading="lazy"></p>
<h3 id="1dockerinspect">1. 使用 Docker Inspect</h3>
<p>Docker inspect 是检索 Docker 对象底层信息的很棒的方式。可以以非常简单的方式在返回的 JSON 里找出想要的字段。</p>
<p>所以 <code>dockerhive_datanode</code> 里面有我们想要的 IP 地址吗？</p>
<pre><code class="language-bash">$ docker inspect -f '{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}' 75000c343eb7

172.18.0.5
</code></pre>
<p>之前不是说 Docker 使用容器网络默认的  <strong>172.17. 0.0/16</strong> 的子网吗？为什么和返回的 IP地址 <strong>172.18.0.5</strong> 并不在一个网段呢？</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2020/06/Screen-Shot-2020-06-22-at-3.25.07-PM.png" alt="Screen-Shot-2020-06-22-at-3.25.07-PM" width="600" height="400" loading="lazy"></p>
<p>图片截自 <a href="https://tehnoblog.org/ip-tools/ip-address-in-cidr-range/">IP address in CIDR range</a></p>
<p>需要查看一下网络设置才能解答它：</p>
<pre><code class="language-bash">$ docker network inspect -f '{{range .IPAM.Config}}{{.Subnet}}{{end}}'  9f6bc3c15568

172.18.0.0/16

</code></pre>
<p>我们在虚拟计算引擎里执行了这个例子，在这个测试里，docker 网络分配了一个不同的子网  <strong>172.18.0.0/16</strong>。原来如此。</p>
<p>另外，我们可以找到在 <code>docker-hive_default</code> 网络内所有的 IP 地址。</p>
<p>也就是不必去每个容器里找它的 IP：</p>
<pre><code class="language-bash">$ docker network inspect -f '{{json .Containers}}' 9f6bc3c15568 | jq '.[] | .Name + ":" + .IPv4Address'

"dockerhive_hive-metastore-postgresql:172.18.0.6/16"
"dockerhive_hive-metastore:172.18.0.2/16"
"dockerhive_namenode:172.18.0.3/16"
"dockerhive_datanode:172.18.0.5/16"
"dockerhive_hive-server:172.18.0.4/16"

</code></pre>
<p><img src="https://www.freecodecamp.org/news/content/images/2020/06/cherry-success.png" alt="drawing" width="600" height="400" loading="lazy"></p>
<p>上面使用了<a href="https://github.com/stedolan/jq"><strong>jq</strong></a> 来解析 <code>Containers</code> 映射对象。</p>
<h3 id="2dockerexec">2. 使用 Docker exec</h3>
<p>在接下来的例子里会用到 <code>dockerhive_namenode</code>。</p>
<pre><code class="language-bash">$ docker exec dockerhive_namenode cat /etc/hosts

127.0.0.1       localhost
::1     localhost ip6-localhost ip6-loopback
fe00::0 ip6-localnet
ff00::0 ip6-mcastprefix
ff02::1 ip6-allnodes
ff02::2 ip6-allrouters
172.18.0.3      607b00c25f29

</code></pre>
<h3 id="3dockercontainer">3.  在 Docker Container 内部</h3>
<pre><code class="language-bash">$ docker exec -it dockerhive_namenode /bin/bash
# running inside the dockerhive_namenode container
ip -4 -o

7: eth0    inet 172.18.0.3/16 brd 172.18.255.255 scope global eth0

</code></pre>
<p>我们甚至可以在容器内部找到同一网络下的其他容器的 IP 地址：</p>
<p><strong>Data node</strong></p>
<pre><code class="language-bash"># running inside the dockerhive_namenode container
ping dockerhive_datanode

PING dockerhive_datanode (172.18.0.5): 56 data bytes
64 bytes from 172.18.0.5: icmp_seq=0 ttl=64 time=0.092 ms

</code></pre>
<p><strong>Hive mestastore</strong></p>
<pre><code class="language-bash"># running inside the dockerhive_namenode container
ping dockerhive_hive-metastore

PING dockerhive_hive-metastore_1 (172.18.0.2): 56 data bytes
64 bytes from 172.18.0.2: icmp_seq=0 ttl=64 time=0.087 ms

</code></pre>
<p><strong>Hive server</strong></p>
<pre><code class="language-bash"># running inside the container
ping dockerhive_hive-server

PING dockerhive_hive-server (172.18.0.4): 56 data bytes
64 bytes from 172.18.0.4: icmp_seq=0 ttl=64 time=0.172 ms

</code></pre>
<h2 id=""><strong>拓展</strong></h2>
<p>所有的例子都是在 Linux 的VM 计算引擎下执行，如果你用的是 macOS 或者 Windows 例子里的命令可能会有些区别。</p>
<p>另外请记住，例子中所有的 IP 地址都是针对 <code>docker-hive_default</code> 网络内部的。如果需要在外部连接这些容器，则需要主机的外部 IP（假设容器正确暴漏了端口）。</p>
<p>或者你用的是 kubernetes 来管理 Docker 容器，可以让它来为你搞定 IP 地址  <a href="https://kubernetes.io/docs/tutorials/stateless-application/expose-external-ip-address/">kubernetes-expose-external-ip-address</a>。</p>
<p><strong>* 插图由  <a href="https://icons8.com/">icons8.com</a>  的  <a href="https://dribbble.com/muratkalkavan">Murat Kalkavan</a> 提供</strong></p>
<!--kg-card-end: markdown--><p>原文：<a href="https://www.freecodecamp.org/news/how-to-get-a-docker-container-ip-address-explained-with-examples/">How to Get A Docker Container IP Address - Explained with Examples</a>，作者：<a href="https://www.freecodecamp.org/news/author/mesmacosta/">Marcelo Costa</a></p> ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ Docker 教程——理解 Docker 镜像和容器的存储路径 ]]>
                </title>
                <description>
                    <![CDATA[ Docker 已经被广泛用在生产环境中运行和扩展应用程序。此外，它还支持通过一条命令快速启动应用。 许多公司投入越来越多的精力来优化本地和远程 Docker 容器中的开发流程，由此也带来了诸多好处。 执行以下命令可以查看 Docker 的配置信息： $ docker info ...  Storage Driver: overlay2  Docker Root Dir: /var/lib/docker ... 输出的内容包含了存储驱动和 docker 根目录的信息。 Docker 镜像和容器的存储路径 Docker 容器由网络文件、卷和镜像组成。Docker 文件的存储路径取决于你的操作系统。常用操作系统中的路径如下：  * Ubuntu: /var/lib/docker/  * Fedora: /var/lib/docker/  * Debian: /var/lib/docker/  * Windows: C:\ProgramData\DockerDesktop  * MacOS: ~/Library/Containers/com.docker.docker/Data ]]>
                </description>
                <link>https://www.freecodecamp.org/chinese/news/where-are-docker-images-stored-docker-container-paths-explained/</link>
                <guid isPermaLink="false">601e8b486183a705401563bc</guid>
                
                    <category>
                        <![CDATA[ Docker ]]>
                    </category>
                
                    <category>
                        <![CDATA[ 容器 ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ Humilitas ]]>
                </dc:creator>
                <pubDate>Fri, 05 Feb 2021 07:20:00 +0000</pubDate>
                <media:content url="https://chinese.freecodecamp.org/news/content/images/2021/02/example-of-examples-word-embeddings_grey.jpg" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>Docker 已经被广泛用在生产环境中运行和扩展应用程序。此外，它还支持通过一条命令快速启动应用。</p>
<p>许多公司投入越来越多的精力来优化本地和远程 Docker 容器中的开发流程，由此也带来了诸多好处。</p>
<p>执行以下命令可以查看 Docker 的配置信息：</p>
<pre><code class="language-shell">$ docker info

...
 Storage Driver: overlay2
 Docker Root Dir: /var/lib/docker
...
</code></pre>
<p>输出的内容包含了存储驱动和 docker 根目录的信息。</p>
<h2 id="docker">Docker 镜像和容器的存储路径</h2>
<p>Docker 容器由网络文件、卷和镜像组成。Docker 文件的存储路径取决于你的操作系统。常用操作系统中的路径如下：</p>
<ul>
<li>Ubuntu:  <code>/var/lib/docker/</code></li>
<li>Fedora:  <code>/var/lib/docker/</code></li>
<li>Debian:  <code>/var/lib/docker/</code></li>
<li>Windows:  <code>C:\ProgramData\DockerDesktop</code></li>
<li>MacOS:  <code>~/Library/Containers/com.docker.docker/Data/vms/0/~</code></li>
</ul>
<p>在 macOS 和 Windows 系统中，Docker 在一个虚拟机中运行 Linux 容器。关于这两种情况，你需要了解一些额外信息。</p>
<h3 id="macdocker">Mac 系统中的 Docker</h3>
<p>Docker 在 Mac 系统中并不是原生兼容的，所以需要使用 <a href="https://github.com/moby/hyperkit">Hyperkit</a> 来运行虚拟机。虚拟机数据存储在：</p>
<p><code>/Library/Containers/com.docker.docker/Data/vms/0</code></p>
<p>在虚拟机内部，Docker 的路径是默认的 <code>/var/lib/docker</code>。</p>
<p>在虚拟机中创建一个 shell 窗口来查看 Docker 根目录：</p>
<pre><code class="language-shell">$ screen ~/Library/Containers/com.docker.docker/Data/vms/0/tty 
</code></pre>
<p>可以按下 <strong>Ctrl+a</strong>，<strong>k</strong>，<strong>y</strong> 组合键来结束会话。</p>
<h3 id="windowsdocker">Windows 系统中的 Docker</h3>
<p>Windows 系统中，Docker 比较复杂，因为有类似于 Linux 容器的原生 Windows 容器，也有运行在基于 Hyper-V 的最小虚拟机中的 Linux 容器。</p>
<p>配置信息和运行 linux 镜像的虚拟机存储在默认的 Docker 根目录。</p>
<p><code>C:\ProgramData\DockerDesktop</code></p>
<p>查看常规镜像的信息，会得到 linux 系统中的路径，如：</p>
<pre><code class="language-shell">$ docker inspect nginx

...
"UpperDir": "/var/lib/docker/overlay2/585...9eb/diff"
...
</code></pre>
<p>连接镜像：</p>
<pre><code class="language-shell">docker run -it --privileged --pid=host debian nsenter -t 1 -m -u -i sh
</code></pre>
<p>现在，可以进入指定路径：</p>
<pre><code class="language-shell">$ cd /var/lib/docker/overlay2/585...9eb/
$ ls -lah

drwx------    4 root     root        4.0K Feb  6 06:56 .
drwx------   13 root     root        4.0K Feb  6 09:17 ..
drwxr-xr-x    3 root     root        4.0K Feb  6 06:56 diff
-rw-r--r--    1 root     root          26 Feb  6 06:56 link
-rw-r--r--    1 root     root          57 Feb  6 06:56 lower
drwx------    2 root     root        4.0K Feb  6 06:56 work
</code></pre>
<h2 id="docker">Docker 根目录的内部结构</h2>
<p><code>/var/lib/docker</code> 目录中保存着各种信息，例如：容器数据、卷、构建文件、网络文件和集群数据。</p>
<pre><code class="language-shell">$ ls -la /var/lib/docker

total 152
drwx--x--x   15 root     root          4096 Feb  1 13:09 .
drwxr-xr-x   13 root     root          4096 Aug  1  2019 ..
drwx------    2 root     root          4096 May 20  2019 builder
drwx------    4 root     root          4096 May 20  2019 buildkit
drwx------    3 root     root          4096 May 20  2019 containerd
drwx------    2 root     root         12288 Feb  3 19:35 containers
drwx------    3 root     root          4096 May 20  2019 image
drwxr-x---    3 root     root          4096 May 20  2019 network
drwx------    6 root     root         77824 Feb  3 19:37 overlay2
drwx------    4 root     root          4096 May 20  2019 plugins
drwx------    2 root     root          4096 Feb  1 13:09 runtimes
drwx------    2 root     root          4096 May 20  2019 swarm
drwx------    2 root     root          4096 Feb  3 19:37 tmp
drwx------    2 root     root          4096 May 20  2019 trust
drwx------   15 root     root         12288 Feb  3 19:35 volumes
</code></pre>
<h3 id="docker">Docker 镜像</h3>
<p>最大的文件通常是镜像。如果使用默认的 overlay2 存储驱动，Docker 镜像会保存在 <code>/var/lib/docker/overlay2</code> 目录。</p>
<p>通过一个示例来查看它的内容：</p>
<pre><code class="language-shell">$ docker image pull nginx
$ docker image inspect nginx

[
    {
        "Id": "sha256:207...6e1",
        "RepoTags": [
            "nginx:latest"
        ],
        "RepoDigests": [
            "nginx@sha256:ad5...c6f"
        ],
        "Parent": "",
 ...
        "Architecture": "amd64",
        "Os": "linux",
        "Size": 126698063,
        "VirtualSize": 126698063,
        "GraphDriver": {
            "Data": {
                "LowerDir": "/var/lib/docker/overlay2/585...9eb/diff:
                             /var/lib/docker/overlay2/585...9eb/diff",
                "MergedDir": "/var/lib/docker/overlay2/585...9eb/merged",
                "UpperDir": "/var/lib/docker/overlay2/585...9eb/diff",
                "WorkDir": "/var/lib/docker/overlay2/585...9eb/work"
            },
...
</code></pre>
<p><strong>LowerDir</strong> 包含镜像的只读层，表示变更的读写层包含在 <strong>UpperDir</strong> 中。示例中，NGINX 镜像的 <strong>UpperDir</strong> 文件夹包含以下文件：</p>
<pre><code class="language-shell">$ ls -la /var/lib/docker/overlay2/585...9eb/diff

total 8
drwxr-xr-x    2 root     root    4096 Feb  2 08:06 .
drwxr-xr-x    3 root     root    4096 Feb  2 08:06 ..
lrwxrwxrwx    1 root     root      11 Feb  2 08:06 access.log -&gt; /dev/stdout
lrwxrwxrwx    1 root     root      11 Feb  2 08:06 error.log -&gt; /dev/stderr
</code></pre>
<p><strong>MergedDir</strong> 表示 <strong>UpperDir</strong> 和 <strong>LowerDir</strong> 合并的结果，Docker 用它来运行容器。<strong>WorkDir</strong> 是 overlay2 的内部目录，应该是空的。</p>
<h3 id="dockervolumes">Docker 卷（Volumes）</h3>
<p>可以利用卷来持久化容器内的数据，容器和宿主机之间、容器和容器之间也可以通过共享卷来共享数据。使用 <strong>-v</strong> 选项可以让容器以挂载卷的方式启动：</p>
<pre><code class="language-shell">$ docker run --name nginx_container -v /var/log nginx
</code></pre>
<p>查看挂载的卷的位置：</p>
<pre><code class="language-shell">$ docker inspect nginx_container

...
"Mounts": [
            {
                "Type": "volume",
                "Name": "1e4...d9c",
                "Source": "/var/lib/docker/volumes/1e4...d9c/_data",
                "Destination": "/var/log",
                "Driver": "local",
                "Mode": "",
                "RW": true,
                "Propagation": ""
            }
        ],
...
</code></pre>
<p>相应目录中包含来自 NGINX 容器 <code>/var/log</code> 目录中的文件。</p>
<pre><code class="language-shell">$ ls -lah /var/lib/docker/volumes/1e4...d9c/_data

total 88
drwxr-xr-x    4 root     root        4.0K Feb  3 21:02 .
drwxr-xr-x    3 root     root        4.0K Feb  3 21:02 ..
drwxr-xr-x    2 root     root        4.0K Feb  3 21:02 apt
-rw-rw----    1 root     43             0 Jan 30 00:00 btmp
-rw-r--r--    1 root     root       34.7K Feb  2 08:06 dpkg.log
-rw-r--r--    1 root     root        3.2K Feb  2 08:06 faillog
-rw-rw-r--    1 root     43         29.1K Feb  2 08:06 lastlog
drwxr-xr-x    2 root     root        4.0K Feb  3 21:02 nginx
-rw-rw-r--    1 root     43             0 Jan 30 00:00 w
</code></pre>
<h2 id="docker">清理 Docker 使用的空间</h2>
<p>建议使用 Docker 命令来清理不再使用的容器。可以使用以下命令清理容器、网络文件、镜像和构建缓存：</p>
<pre><code class="language-shell">$ docker system prune -a
</code></pre>
<p>此外，也可以清除不再使用的卷：</p>
<pre><code class="language-shell">$ docker volumes prune
</code></pre>
<h2 id=""><strong>总结</strong></h2>
<p>Docker 是很多人开发环境和工具集的一部分，有时候，Docker 能够像魔法一样巧妙地解决许多问题，而用户完全不需要关心内部细节。不过，Docker 也是一个常规的工具，它将庞大的文件保存在用户可以打开和编辑的目录中。</p>
<p>有时候，它可能很快就把磁盘占满了，所以要经常检查它的根目录（的磁盘占用情况），但是不建议手动删除或编辑 Docker 文件，最好使用 prune 命令来释放磁盘空间。</p>
<hr>
<p>希望你喜欢这篇文章，欢迎关注我的 <a href="https://twitter.com/Journerist">Twitter</a> 以示鼓励。</p>
<p>我是革命性的旅游平台 <a href="https://www.urlaubsbaron.de/">Explore The World</a> 的联合创始人，我们是位于德国 Dresden 的一家初创公司，初期目标市场是德国。如果有任何反馈或疑问，欢迎与我联系。</p>
<p>Happy Docker exploring :)</p>
<hr>
<h2 id="">参考资料</h2>
<ul>
<li>Docker 存储驱动文档<br>
<a href="https://docs.docker.com/storage/storagedriver/">https://docs.docker.com/storage/storagedriver/</a></li>
<li>Overlay 文件系统文档<br>
<a href="https://www.kernel.org/doc/Documentation/filesystems/overlayfs.txt">https://www.kernel.org/doc/Documentation/filesystems/overlayfs.txt</a></li>
</ul>
<!--kg-card-end: markdown--><p>原文：<a href="https://www.freecodecamp.org/news/where-are-docker-images-stored-docker-container-paths-explained/">Where are Docker Images Stored? Docker Container Paths Explained</a>，作者：<a href="https://www.freecodecamp.org/news/author/sebarthel/">Sebastian Barthel</a></p> ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ 如何删除 Docker 镜像和容器 ]]>
                </title>
                <description>
                    <![CDATA[ Docker rmi docker rmi  通过镜像的 ID 删除镜像。 要删除镜像，首先需要列出所有镜像以获取镜像的 ID，镜像的名称和其他详细信息。 运行简单的命令 docker images -a  或 docker images。 之后，明确要删除哪个镜像，然后执行简单命令 docker rmi <your-image-id>。然后，列出所有镜像并检查，可以确认镜像是否已删除。 一次删除多张镜像 当你要一次删除多张镜像时，可以使用一种方法。首先只需列出镜像即可获取镜像的 ID，然后执行简单的命令： docker rmi <your-image-id> <your-image-id> ... 列出镜像的 ID，每个 ID 之间留一个空格。 一次删除所有镜像 要删除所有镜像，有一个简单的命令可以做到：docker rmi $(docker images -q)。 在上面的命令中，有两个命令，第一个在 $()  中执行的命令是 shell 语法，返回以该执行的结果。然后，-q-  是一个选项，用于返回唯一的 ID。$() 返回镜像 ]]>
                </description>
                <link>https://www.freecodecamp.org/chinese/news/how-to-remove-images-in-docker/</link>
                <guid isPermaLink="false">5ff51ab939641a0517d53451</guid>
                
                    <category>
                        <![CDATA[ Docker ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ Miya Liu ]]>
                </dc:creator>
                <pubDate>Wed, 06 Jan 2021 02:20:00 +0000</pubDate>
                <media:content url="https://chinese.freecodecamp.org/news/content/images/2021/01/photo-1548092372-0d1bd40894a3.jpeg" medium="image" />
                <content:encoded>
                    <![CDATA[ <h2 id="docker-rmi"><strong><strong><strong>Docker rmi</strong></strong></strong></h2><p><code>docker rmi</code> 通过镜像的 ID 删除镜像。</p><p>要删除镜像，首先需要列出所有镜像以获取镜像的 ID，镜像的名称和其他详细信息。 运行简单的命令 <code>docker images -a</code> 或 <code>docker images</code>。</p><p>之后，明确要删除哪个镜像，然后执行简单命令 <code>docker rmi &lt;your-image-id&gt;</code>。然后，列出所有镜像并检查，可以确认镜像是否已删除。</p><h3 id="-"><strong><strong><strong>一次删除多张</strong></strong></strong>镜像</h3><p>当你要一次删除多张镜像时，可以使用一种方法。首先只需列出镜像即可获取镜像的 ID，然后执行简单的命令：</p><p><code>docker rmi &lt;your-image-id&gt; &lt;your-image-id&gt; ...</code></p><p>列出镜像的 ID，每个 ID 之间留一个空格。</p><h3 id="--1"><strong><strong><strong>一次删除所有</strong></strong></strong>镜像</h3><p>要删除所有镜像，有一个简单的命令可以做到：<code>docker rmi $(docker images -q)</code>。</p><p>在上面的命令中，有两个命令，第一个在 <code>$()</code> 中执行的命令是 shell 语法，返回以该执行的结果。然后，<code>-q-</code> 是一个选项，用于返回唯一的 ID。$() 返回镜像 ID 的结果，然后 <code>docker rmi</code> 删除所有这些镜像。</p><h4 id="--2"><strong><strong><strong>更多信息</strong></strong>：</strong></h4><ul><li><a href="https://docs.docker.com/engine/reference/commandline/rm/">Docker CLI docs: rmi</a></li></ul><h2 id="docker-rm"><strong><strong><strong>Docker rm</strong></strong></strong></h2><p><code>docker rm</code> 根据容器的名称或者 ID 来删除容器。</p><p>如果 Docker 容器正在运行，你在删除它们之前需要先停止运行。</p><ul><li>停止所有容器运行：<code>docker stop $(docker ps -a -q)</code></li><li>删除所有停止运行的容器：<code>docker rm $(docker ps -a -q)</code></li></ul><h3 id="--3"><strong><strong><strong>删除多个容器</strong></strong></strong></h3><p>你可以通过向命令传递要删除的容器列表来停止和删除多个容器。shell 语法 <code>$()</code> 返回括号中执行的任何结果。因此，你可以在其中创建容器列表，以传递给 <code>stop</code> 和 <code>rm</code> 命令。</p><h3 id="docker-ps-a-q-"><strong>docker ps -a -q 分解</strong></h3><ul><li><code>docker ps</code> 列出容器。</li><li><code>-a</code> 这个选项用于列出所有容器，包括停止运行的。如果没有这个选项，则默认只列出在运行的容器。</li><li><code>-q</code> 这个选项列出容器的数字 ID，而不是容器的所有信息。</li></ul><h4 id="--4"><strong><strong><strong>更多信息</strong></strong>：</strong></h4><ul><li><a href="https://docs.docker.com/engine/reference/commandline/rm/">Docker CLI docs: rm</a></li></ul><h2 id="-docker-"><strong>关于 Docker </strong>镜像<strong>的更多信息：</strong></h2><ul><li><a href="https://www.freecodecamp.org/news/docker-image-guide-how-to-remove-and-delete-docker-images-stop-containers-and-remove-all-volumes/">Docker </a>镜像<a href="https://www.freecodecamp.org/news/docker-image-guide-how-to-remove-and-delete-docker-images-stop-containers-and-remove-all-volumes/">指南</a></li><li><a href="https://www.freecodecamp.org/news/where-are-docker-images-stored-docker-container-paths-explained/">Docker </a>镜像<a href="https://www.freecodecamp.org/news/where-are-docker-images-stored-docker-container-paths-explained/">存储在哪里</a></li></ul><h2 id="-docker--1"><strong>关于 Docker 容器的更多信息：</strong></h2><ul><li><a href="https://www.freecodecamp.org/news/automate-docker-container-deployment-via-maven-53a855e26d3e/">如何自动化部署 Docker 容器</a></li><li><a href="https://www.freecodecamp.org/news/how-to-find-and-fix-docker-container-vulnerabilities-in-2020/">如何修复 Docker 容器</a></li></ul><h2 id="-docker--2"><strong>关于 Docker 的更多信息：</strong></h2><ul><li><a href="https://www.freecodecamp.org/news/a-beginners-guide-to-docker-how-to-create-your-first-docker-application-cc03de9b639f/">Docker 入门指南</a></li><li><a href="https://www.freecodecamp.org/news/docker-devops-course/">Docker DevOps 课程（视频）</a></li><li><a href="https://www.freecodecamp.org/news/docker-101-creation-to-deployment/">Docker 101：从创建到部署</a></li></ul><p>原文：<a href="https://www.freecodecamp.org/news/how-to-remove-images-in-docker/">How to Remove Images and Containers in Docker</a></p> ]]>
                </content:encoded>
            </item>
        
    </channel>
</rss>
