用于 JavaScript 应用程序的 Dockerfile 的范围可以很大------从两行到五十行。这是怎么回事?这种复杂性会让一些开发人员无法真正理解这个强大的工具,所以今天,我想通过解读一个用于 JavaScript 应用程序的示例Dockerfile 来揭开 Docker 的神秘面纱。无论您使用什么JS框架,这都应该是一个有用的资源。
虽然本文不会对该主题进行全面的教育,但在本文结束时,您将对编写和修改任何Dockerfile以满足应用程序需求的能力更加自信。
Docker 快速入门
一些专属名词:
-
Image(镜像) :这包含您的应用程序代码和运行它所需的一切。在大多数情况下,Docker 镜像(或者更具体地说,)只是文件系统层的堆栈。您可以使用
docker build
或 等其他工具构建新镜像。要了解更多信息,请查看 -
Container(容器) :这是实际运行你的映像并使你的应用程序变焦的东西。你可以使用
docker run
运行现有的Docker映像。设置容器通常是定义环境变量、公开哪些端口、允许哪些协议等的地方。 -
Instructions(指令):这些是ALLCAPS中每行开头的位,后跟任意数量的参数。您可能会听到人们称它们为命令或语句,但它们正式称为指令。
-
Layers(层):Dockerfile中的几乎每条指令行都变成了一个层。这些层是构成Docker映像的tarball。此外,这些层的顺序很重要,尤其是对于构建优化。
让工作更轻松:使用 Dockerfile 生成器
我要取巧一点,并提早告诉你,虽然对 Dockerfile 语法有更深入的理解非常有用,但有一个更简单的方法 (至少对JavaScript开发人员来说!),这就是使用 Fly.io的Dockerfile生成器。
bash
$ npx @flydotio/dockerfile@latest
$ npx dockerfile
该软件包可以与npm
和bun.sh/一起使用(您可以使用bunx
而不是npx
来运行带有bun
的脚本)。根据需要调整Dockerfile有额外的参数;请务必查看README以获取更多详细信息。
剖析 Dockerfile
今天我们将剖析Next. JS应用程序的Dockerfile,因为它涵盖了有效使用Dockerfile的所有不同方式。这将有助于解释Dockerfile的不同部分,即使您不使用Next.js应用程序。
这是我们将处理的Dockerfile。
dockerfile
# syntax = docker/dockerfile:1
# Adjust NODE_VERSION as desired
ARG NODE_VERSION=20.9.0
FROM node:${NODE_VERSION}-slim AS base
LABEL fly_launch_runtime="Next.js"
# Next.js app lives here
WORKDIR /app
# Set production environment
ENV NODE_ENV="production"
# Throw-away build stage to reduce size of final image
FROM base AS build
# Install packages needed to build node modules
RUN apt-get update -qq && \
apt-get install -y build-essential pkg-config python-is-python3
# Install node modules
COPY --link package-lock.json package.json ./
RUN npm ci --include=dev
# Copy application code
COPY --link . .
# Build application
RUN npm run build
# Remove development dependencies
RUN npm prune --omit=dev
# Final stage for app image
FROM base
# Copy built application
COPY --from=build /app /app
# Start the server by default, this can be overwritten at runtime
EXPOSE 3000
CMD [ "npm", "run", "start" ]
这可能看起来像是很多步骤。但是每一行都有原因,阅读到这篇文章的结尾,你就会明白每一行都做了什么以及为什么要使用它。
让我们从高级概述开始。我们的Dockerfile可以分为三个阶段:
-
设置我们的基本映像(剧透:它只是Node)
-
构建应用程序
-
将我们的应用程序代码添加到我们的原始基础镜像
您会注意到这些阶段中的每一个都以一个FROM
语句开始,该语句设置一个基本图像。FROM语句用于设置一个基本图像,关于它们有一些需要注意的事情:
-
每个Dockerfile必须至少包含_一个_
FROM
指令 -
可以有多个
FROM
指令 -
最后一个
FROM
指令总是获胜*-获胜,我的意思是在最终图像中使用。在它被扔掉之前的一切
您可能希望拥有多个FROM
语句的原因是为了优化最终图像的大小。这将我们带到Dockerfile构建的一个重要主题:多阶段构建。
*此行为可以被覆盖时,部署您的应用程序与fly deploy ---build-target=<specific target>
多阶段构建:它们是什么以及为什么使用它们
在Dockerfiles中使用多个FROM
语句允许我们将构建过程分解为块,并最终使最终映像尽可能小。
打个比方,让我们假设你在做一批蔬菜高汤。非常简单:把一堆蔬菜残渣放在水里炖一个小时左右,瞧!你有高汤了。现在,正如你所料,你必须在最后过滤蔬菜;否则,你只是做了奇怪的汤。但是!仅仅因为最终产品不含任何蔬菜并不意味着它们对这个过程不重要。
这就是多阶段Docker构建的主要优势。你从不同的图像中借用你需要的东西来做重要的工作,并在你的最终图像中使用结果,扔掉其余的。你的图像越小,你的应用程序启动得越快。
现在我们对Docker构建的整体流程有了一个高级概述,以下是每个阶段发生的事情。
第一阶段:Base
ARG
dockerfile
ARG NODE_VERSION=20.9.0
我们通过定义我们想要使用的Node(或Bun!或其他任何东西)版本来开始我们的文件。很简单,但是ARG
命令到底是什么?
在Dockerfiles中,有两种设置变量的方法:
-
ARG
-这些用于设置构建时变量。正如您所料,这些变量在构建时可用(但不是运行时) -
ENV
-这些变量在_构建_和_运行时都可用于您的应用_
**不要将敏感数据存储在Dockerfile中。**它们可以安全地用于NODE_VERSION
或NODE_ENV
之类的东西,但是像令牌、数据库URL或其他机密之类的东西应该以不同的方式处理。
要处理构建时机密信息 ,您需要使用RUN
指令挂载它们。您可以使用前面提到的@flydotio/dockerfile
包来执行此操作,然后,当您部署到Fly.io时,设置构建机密的值(第二个命令):
bash
$ npx dockerfile --mount-secret=MY_SECRET
$ fly deploy --build-secret MY_SECRET=<value>
为了处理运行时机密,这些机密应该远离您的Dockerfile,而是使用以下命令设置:
bash
$ fly secrets set SECRET_PASSWORD=<value>
当应用程序在生产环境中运行时,这些秘密会作为环境变量公开。
FROM
dockerfile
FROM node:${NODE_VERSION}-slim as base
在本例中,node
是图像的 名称FROM``:<version>-slim
是一个标记,用于表示基础图像的特定_版本_。
我如何知道我的应用需要使用什么Debian版本?
对于大多数使用节点或Bun的应用程序,**我们建议从-slim
变体开始。**这采用了Debian的最新版本,并删除了您不需要的所有内容。如果您确实需要一些被删除的内容,您可以稍后使用apt-get
将其添加回构建阶段(我们将在构建阶段部分中介绍)。
LABEL
dockerfile
LABEL fly_launch_runtime="Next.js"
LABEL
允许您为Docker图像和容器设置任意键值元信息。如果这些信息对您的自动化有用,它允许您注释任何您想要的信息。这是我们的框架团队用来跟踪常用框架的Fly.io特定标签/标记,因此我们知道在开发新功能时应该优先考虑什么。它在技术上是可选的,但它确实有助于我们满足您首选框架的需求!😄
WORKDIR
dockerfile
WORKDIR /app
该WORKDIR
指令设置任何后续RUN
、COPY
和ADD
语句的当前工作目录。这是构建应用程序代码的地方,也是部署到生产环境中的文件夹。
ENV
dockerfile
ENV NODE_ENV="production"
ENV
指令设置环境变量,这些变量在_构建时和运行时都可用。_
请记住,您的Dockerfile是源码。源码绝不能包含秘密信息。如果您需要将秘密信息设置为环境变量,请使用Fly.io secrets
:
bash
$ fly secrets set SECRET_KEY=<value>
第二阶段:build
正如我们前面所讨论的,每次遇到FROM
语句时,您都知道您已经到达了Docker构建的新阶段。让我们看看Dockerfile的第二阶段。
dockerfile
FROM base AS build
这个FROM...AS...
标志着我们第二阶段的开始。如果你还记得,我们的第一阶段始于node:<version>-slim
,我们_将其命名为_ base
。现在在第二阶段,我们正在复制base
并将其命名为build
。从现在开始,任何build
_都不会影响原始base
。_稍后你会看到,这允许我们只挑选我们想要保留的部分,然后扔掉其余的部分。build
。
RUN
dockerfile
RUN apt-get update -qq \
&& apt-get install -y build-essential pkg-config python-is-python3
现在我们已经建立了一个base
作为build
的副本,我们将开始做一些_实际_的构建工作。首先,让我们了解RUN
指令的用途,然后我们将讨论这个apt-get
命令。
RUN语句用于运行命令。令人震惊,我知道。但值得注意的是,这不是运行shell命令的唯一方法。实际上有三种常用指令用于此类任务:
-
RUN
:总是创建一个新层,因此最好将这些命令链接成一条指令,就像我们上面对多个apt-get
命令所做的那样。一般来说,RUN非常适合应用程序代码的_设置_。 -
ENTRYPOINT
:这将设置首先在容器中运行的进程。这通常不是您的Web服务器。默认切入点是/bin/sh -c
,它将启动shell进程,但可以使用ENTRYPOINT
指令进行自定义。然后,您使用CMD指令设置的任何内容都将作为参数传递给该shell进程(例如启动服务器的命令)。 -
CMD
:此指令设置传递到入口点的默认命令。这通常是您编写命令以启动Web服务器的地方,例如CMD ["npm", "run", "start"]
。
你可能需要知道,它们之前的区别:
dockerfile
<INSTRUCTION> npm install
dockerfile
<INSTRUCTION> ["npm", "install"]
不同之处在于,当您使用字符串参数参数时,Docker将在shell中运行您的命令(更具体地说,在/bin/sh -c
中)。如果您使用字符串数组,它将直接运行程序,而无需将其包装在shell中。在许多情况下,这并不重要,但有时在非常不常见的用例中可能很重要,例如当容器图像中没有操作系统时,或者当每千字节的RAM都很重要时。
apt-get 是什么,为什么需要它?
工具apt-get
用于安装和管理基于Debian的Linux包(想想NPM,但对于操作系统包;它类似于macOS上的homebrew
)。我们包含的包(build-essential
、pkg-config
和python-is-python3
)是许多JavaScript包的常见要求。可能需要实验来找到您的应用程序所需的确切依赖项集,但即使您认为您的应用程序不需要它们,也可以安全地保留这条线用于未来的开发,而不必担心膨胀,因为这些不会包含在您的最终图像中。
COPY
dockerfile
COPY --link package-lock.json package.json ./
安装任何Linux包要求后,我们终于可以开始复制部分应用程序代码了。COPY
指令将存储库中的本地文件复制到Docker图像上的某个位置。
什么是 --link
?
通常,如果COPY
之前的层有新的更改,则需要重新运行COPY
语句。请记住,几乎每条Docker指令都会创建一个新的层,而层就是构成Docker图像的tarball。因为这些层需要_分层_,所以会运行diff以查看前一层是否有任何可能影响当前层的更改。如果前一层_有_更改,它将使后续层无效。
但是,通过包含--link
,我们创建了一个新层 ,当对前面的图像进行更改时,它不会失效,从而允许我们缓存--link
层。
要了解更多信息,请查看这篇文章,其中包含一个非常有用的信息图来说明COPY
和COPY --link
之间的区别。
我们build
步骤的最后几行现在应该感觉更熟悉了。此时,我们只是简单地安装其余的依赖项,复制我们的应用程序代码,并去掉所有的devDependencies
,以便我们的代码可以投入生产。
dockerfile
RUN npm ci --include=dev
# Copy application code
COPY --link . .
# Build application
RUN npm run build
# Remove development dependencies
RUN npm prune --omit=dev
第三阶段:base
+ build
dockerfile
# Final stage for app image
FROM base AS runner
# Copy built application
COPY --from=build /app /app
我们在最后阶段!因为我们已经到达了文件中的最终FROM
,我们知道base
将是我们最终图像的目标。
复制 --from=<target>
您会注意到我们的COPY
语句包括--from=build
。**这就是使用多阶段构建的魔力。**在这里,我们从build
目标中挑选部分,并留下运行时不需要的任何东西。这使我们的最终图像尽可能小!
EXPOSE
dockerfile
EXPOSE 3000
CMD [ "npm", "run", "start" ]
我们已经完成了Dockerfile!我们在关于RUN
语句的章节中简要讨论了CMD
语句,现在我们可以看到它的实际应用。如前所述,在启动Web服务时,CMD
通常是用于指定启动过程的指令。
然而,就在我们的start命令之前,我们使用EXPOSE
指令指定我们希望Docker容器公开的内部端口。这里的关键词是_内部_------对于Web服务,指定的端口被映射到外部端口80或443以接受HTTP(S)请求。
部署到Fly.io时的注意事项:此内部端口也设置在fly.toml
中。此处指定的任何端口都将覆盖您在EXPOSE
中设置的端口:
lua
[http_service]
internal_port = 8080
结论
希望到现在,您开始对自己对Dockerfiles
的理解感到更深刻。