译者 | 陈峻
审校 | 孙淑娟
众所周知,作为一个文本文档,Dockerfile包含了用户创建镜像的所有命令和说明。Docker可以通过读取Dockerfile中指令的方式,去自动构建镜像。因此,大家往往认为编写Dockerfile理应非常简单,只需从互联网上选择一个示例,并通过自定义来满足实际需求即可。然而,事实并非如此。
由于生产环境有着严格的要求,特别是在安全方面,因此虽然有许多示例能够适合开发环境,但不一定在生产环境中也合适。另外,由于Docker也提供了一套编写Dockerfile的指导策略,这就导致了Dockerfile像编写代码那样,您可能知道了相关语法,却不一定能够用特定的编程语言写出干净、简洁的代码。下面,我将和您探讨7项在编写Dockerfile时,比较实用的优秀策略与理论实践。
首先,让我们来看一个典型的Dockerfile示例:
Dockerfile
FROM eclipse-temurin:17
RUN mkdir /opt/app
ARG JAR_FILE
ADD target/${JAR_FILE} /opt/app/app.jar
CMD ["java", "-jar", "/opt/app/app.jar"]根据其内容,该Dockerfile会执行以下操作:
可见,上述每个段落生成的Dockerfile,都可以在Git存储库的Dockerfiles目录下被找到。而且在每个段落的末尾处,相应的Dockerfile名称也会在适用的地方被提及。下面,我们将通过修改该Dockerfile来实现七种优秀实践。
在继续阅读下文之前,您需要具备的先决条件是:
为了展示各项优秀实践,我事先创建了一个包含Spring Web依赖项的基本Spring Boot应用。该应用可以通过在存储库的根目录中调用以下命令来运行:
Shell
$ MVN spring-boot:run而为了构建Docker镜像,我将使用Spotify的dockerfile-maven-plugin的一个分支。为此,我会将如下代码段添加到pom文件中。
XML
<plugin>
<groupId>com.xenoamess.docker</groupId>
<artifactId>dockerfile-maven-plugin</artifactId>
<version>1.4.25</version>
<configuration>
<repository>mydeveloperplanet/dockerbestpractices</repository>
<tag>${project.version}</tag>
<buildArgs>
<JAR_FILE>${project.build.finalName}.jar</JAR_FILE>
</buildArgs>
</configuration>
</plugin>使用该插件的好处在于,您可以轻松地重用配置。同时,为了实现通过Maven命令来创建Docker镜像,您可以通过调用如下命令来构建jar文件:
Shell
$ mvn clean verify接着,请通过调用如下命令来构建Docker镜像:
Shell
$ mvn dockerfile:build如下命令可让您运行Docker镜像:
Shell
$ docker run --name dockerbestpractices mydeveloperplanet/dockerbestpractices:0.0.1-SNAPSHOT然后,请通过如下代码来找到运行中的容器的IP地址:
Shell
$ docker inspect dockerbestpractices | grep IPAddress
"SecondaryIPAddresses": null,
"IPAddress": "172.17.0.3",
"IPAddress": "172.17.0.3"本例的IP地址为172.17.0.3。同时,该应用还包含一个只用来响应hello消息的HelloController。而且,Hello端点可以通过如下方式被调用:
Shell
$ curl http://172.17.0.3:8080/hello
Hello Docker!至此,一切就绪了。
在前文中,我们提到了本例Dockerfile中使用到的镜像是eclipse-temurin:17。下面,我们来看看该镜像是如何被构建的:
如果您仔细观察页面每个层的细节,并将其与标签17-JRE进行比较,就会注意到标签17包含了一个完整的JDK,而标签17-JRE只是包含了JRE。当然,后者对于运行Java应用来说已经足够了,毕竟在生产环境中运行各种应用是不需要整个JDK的。而且,由于开发工具可能会被滥用,因此JDK在使用中也带有一定的安全问题。此外,标签17的镜像在压缩后的尺寸为235MB,而17-jre的压缩后尺寸只有89MB。
为了进一步减小镜像的尺寸,我们可以使用经“瘦身”的镜像:17-jre-alpine。该镜像的压缩后尺寸为59MB,足足比17-jre减少了30MB,因此它更容易被分发。
值得注意到是,以上使用的标签均为通用标签,且指向的是最新版本。这对于开发环境来说可能没有问题,但是对于生产环境而言,您需要事先明确所使用的版本。本例中使用的标签便是17.0.5_8-jre-alpine。如您想进一步加固安全,则可以将SHA256散列添加到镜像的版本中。SHA256散列可以在包含了这些层的页面上找到。当SHA256的哈希值与Dockerfile中定义的哈希值无法对应时,构建Docker镜像的过程将会失败。
在本例中,Dockerfile的第一行为:
Dockerfile
FROM eclipse-temurin:17有了上面的知识,我们可以将该行更改为:
Dockerfile
FROM eclipse-temurin: 17.0.5_8-jre-alpine@sha256:02c04793fa49ad5cd193c961403223755f9209a67894622e05438598b32f210e如下代码所示,在Docker镜像完成构建后,您会注意到,(曾经未压缩的)镜像从以前的475MB缩小到现在的188MB。
Shell
$ docker images
REPOSITORY TAG IMAGE ID CREATED SIZE
mydeveloperplanet/dockerbestpractices 0.0.1-SNAPSHOT 0b8d89616602 3 seconds ago 188MB生成的Dockerfile在Git存储库中被命名为1-Dockerfile-specific-image。
默认情况下,应用程序在容器中会以Root用户身份运行。这显然会暴露许多漏洞风险,并且也不是必要的。对此,您应该为应用定义一个系统级用户。如下代码所示,在启动容器时的第一行日志中,您可以看到该应用是由Root启动的。
Shell
2022-11-26 09:03:41.210 INFO 1 --- [ main] m.MyDockerBestPracticesPlanetApplication : Starting MyDockerBestPracticesPlanetApplication v0.0.1-SNAPSHOT using Java 17.0.5 on 3b06feee6c65 with PID 1 (/opt/app/app.jar started by root in /)我们可以通过向Dockerfile中添加组javauser和用户javauser,来创建系统级用户。然后,通过向Dockerfile中添加如下指令来实现。其中,javauser是一个系统级用户,并不具备登录权限。注意,为了只创建一个层面,组和用户的创建步骤都被&符号组合到了一行之中。
Dockerfile
RUN addgroup——system javauser && adduser -S -S /usr/sbin/nologin -G javauser javauser下表列出了可用于adduser的完整参数集:
同时,您也可以通过添加如下一行,将目录/opt/apt的所有者更改为新的javauser,否则javauser将无法访问该目录:
Dockerfile
RUN chown -R javauser:javauser /opt/app最后,您需要确保通过USER命令在容器中实际使用了javauser。其对应的完整Dockerfile为:
Dockerfile
FROM eclipse-temurin:17.0.5_8-jre-alpine@sha256:02c04793fa49ad5cd193c961403223755f9209a67894622e05438598b32f210e
RUN mkdir /opt/app
RUN addgroup --system javauser && adduser -S -s /usr/sbin/nologin -G javauser javauser
ARG JAR_FILE
ADD target/${JAR_FILE} /opt/app/app.jar
RUN chown -R javauser:javauser /opt/app
USER javauser
CMD ["java", "-jar", "/opt/app/app.jar"]
为了测试这个新的镜像,您首先需要通过如下命令,停止并删除正在运行的容器。
Shell
$ docker stop dockerbestpractices
$ docker rm dockerbestpractices完成重新构建并再次运行容器后,如下代码所示,您可以在第一行日志中看到,该应用程序是由javauser启动的。
Shell
2022-11-26 09:06:45.227 INFO 1 --- [ main] m.MyDockerBestPracticesPlanetApplication : Starting MyDockerBestPracticesPlanetApplication v0.0.1-SNAPSHOT using Java 17.0.5 on ab1bcd38dff7 with PID 1 (/opt/app/app.jar started by javauser in /)同样,生成的Dockerfile在Git存储库中被命名为2-Dockerfile-do-not-run-as-root。
在您使用的Dockerfile中,目录/opt/app是被一次性创建的,毕竟这是您的工作目录。就算它并不存在,Docker也会帮您默认创建。因此,您不必每一次都重复这条路径。例如,您会看到Dockerfile的第二行包含了如下RUN指令:
Dockerfile
RUN mkdir /opt/app我们可以通过使用WORKDIR指令来稍作改变:
Dockerfile
WORKDIR /opt/app由于WORKDIR指令已经确保了您在该目录下,因此您完全可以删除每一个/opt/app的引用。因此,新的Dockerfile如下代码所示:
Dockerfile
FROM eclipse-temurin:17.0.5_8-jre-alpine@sha256:02c04793fa49ad5cd193c961403223755f9209a67894622e05438598b32f210e
WORKDIR /opt/app
RUN addgroup --system javauser && adduser -S -s /usr/sbin/nologin -G javauser javauser
ARG JAR_FILE
ADD target/${JAR_FILE} app.jar
RUN chown -R javauser:javauser .
USER javauser
CMD ["java", "-jar", "app.jar"]完成构建并重新运行容器后,您可以在如下日志中看到,jar文件仍然在/opt/app目录中被执行:
Shell
2022-11-26 16:07:18.503 INFO 1 --- [ main] m.MyDockerBestPracticesPlanetApplication : Starting MyDockerBestPracticesPlanetApplication v0.0.1-SNAPSHOT using Java 17.0.5 on fe5cf9223143 with PID 1 (/opt/app/app.jar started by javauser in /opt/app)同样,生成的Dockerfile在Git存储库中被命名为3-Dockerfile-use-workdir。
CMD指令和ENTRYPOINT指令之间是存在区别的。简而言之,两者的使用场景分别是:
ENTRYPOINT:当您总需要执行各种命令,才能构建出可执行的Docker镜像时,只要您愿意,完全可以将参数附加到命令中。
CMD:当您想提供一个默认的参数集,且允许在容器运行时被命令行覆盖时。
那么,在运行Java应用的情况下,请最好使用ENTRYPOINT。例如,原本Dockerfile的最后一行为:
Dockerfile
CMD ["java", "-jar", "app.jar"]现在可以变为:
Dockerfile
ENTRYPOINT ["java", "-jar", "app.jar"]完成构建并重新运行容器,您并不会注意到有任何特定的差异,容器仍然会照常运行。生成的Dockerfile在Git存储库中被命名为4-Dockerfile-use-entrypoint。
COPY和ADD指令也似乎比较类似。然而,COPY要比ADD更好,毕竟COPY只是复制文件到镜像,而ADD还有一些额外的特性,比如添加来自远程资源的文件。
Dockerfile中的ADD命令行为:
Dockerfile
ADD target/${JAR_FILE} app.jar如果改用COPY命令,则为:
Dockerfile
COPY target/${JAR_FILE} app.jar重新构建并运行容器,您同样看不出显著变化,除了在构建日志中显示的是COPY命令,而不是ADD命令。生成的Dockerfile在Git存储库中可用,名称为5-Dockerfile-use-copy-instead-of-add。
为了防止Docker镜像被意外地添加文件,您可以使用.dockerignore文件来指定哪些文件可以被发送到Docker守护进程中,或者是在镜像中被使用。一种值得推荐的方法是:忽略所有的文件,仅显式地添加那些您允许的文件。通过在.dockerignore文件中添加星号,我们可以排除所有的子目录和文件。当然,为了将jar文件放入构建的上下文,您也可以使用感叹号来避免忽略jar文件。如下dockerignore文件所示,我们可以将它添加到运行Docker命令的目录中。例如,在本例中,我们将其添加到Git存储库的根目录上。
Plain Text
**/**
!target/*.jar完成构建并重新运行容器后,其变化可能并不显著。但是当您使用npm开发时,由于node_modules目录不再被复制到Docker构建的上下文中,因此您能够明显地感受到创建Docker镜像的过程被缩短了。注意,您可以直接在Git存储库的Dockerfiles目录下找到dockerignore文件。
默认情况下,Docker守护进程是以Root身份运行的。通过前文的讨论,您一定觉察到了潜在的安全问题。庆幸的是,从Docker v20.10开始,我们可以non-root用户运行Docker守护进程了。
此外,您也可以利用无守护进程(daemonless)的容器引擎--Podman(https://podman.io/)。以默认non-root方式运行。虽然有人认为Podman是Docker的临时替代品,但是它们在容器中挂载卷的方面有所区别。
在上文中,我们介绍了7种编写Dockerfile和运行容器的最佳实践。虽然编写Dockerfile并不复杂,但是若想正确、规范地编写,还是需要您花些时间去研究和理解其使用说明的。
原文链接:https://dzone.com/articles/docker-best-practices
陈峻 (Julian Chen),51CTO社区编辑,具有十多年的IT项目实施经验,善于对内外部资源与风险实施管控,专注传播网络与信息安全知识与经验。
很好奇,就使用rubyonrails自动化单元测试而言,你们正在做什么?您是否创建了一个脚本来在cron中运行rake作业并将结果邮寄给您?git中的预提交Hook?只是手动调用?我完全理解测试,但想知道在错误发生之前捕获错误的最佳实践是什么。让我们理所当然地认为测试本身是完美无缺的,并且可以正常工作。下一步是什么以确保他们在正确的时间将可能有害的结果传达给您? 最佳答案 不确定您到底想听什么,但是有几个级别的自动代码库控制:在处理某项功能时,您可以使用类似autotest的内容获得关于哪些有效,哪些无效的即时反馈。要确保您的提
导读:随着叮咚买菜业务的发展,不同的业务场景对数据分析提出了不同的需求,他们希望引入一款实时OLAP数据库,构建一个灵活的多维实时查询和分析的平台,统一数据的接入和查询方案,解决各业务线对数据高效实时查询和精细化运营的需求。经过调研选型,最终引入ApacheDoris作为最终的OLAP分析引擎,Doris作为核心的OLAP引擎支持复杂地分析操作、提供多维的数据视图,在叮咚买菜数十个业务场景中广泛应用。作者|叮咚买菜资深数据工程师韩青叮咚买菜创立于2017年5月,是一家专注美好食物的创业公司。叮咚买菜专注吃的事业,为满足更多人“想吃什么”而努力,通过美好食材的供应、美好滋味的开发以及美食品牌的孵
1.错误信息:Errorresponsefromdaemon:Gethttps://registry-1.docker.io/v2/:net/http:requestcanceledwhilewaitingforconnection(Client.Timeoutexceededwhileawaitingheaders)或者:Errorresponsefromdaemon:Gethttps://registry-1.docker.io/v2/:net/http:TLShandshaketimeout2.报错原因:docker使用的镜像网址默认为国外,下载容易超时,需要修改成国内镜像地址(首先阿里
我认为我的问题最好用一个例子来描述。假设我有一个名为“Thing”的简单模型,它有一些简单数据类型的属性。像...Thing-foo:string-goo:string-bar:int这并不难。数据库表将包含具有这三个属性的三列,我可以使用@thing.foo或@thing.bar之类的东西访问它们。但我要解决的问题是当“foo”或“goo”不再包含在简单数据类型中时会发生什么?假设foo和goo代表相同类型的对象。也就是说,它们都是“Whazit”的实例,只是数据不同。所以现在事情可能看起来像这样......Thing-bar:int但是现在有一个新的模型叫做“Whazit”,看起来
我有一个要在我的Rails3项目中使用的数组扩展方法。它应该住在哪里?我有一个应用程序/类,我最初把它放在(array_extensions.rb)中,在我的config/application.rb中我加载路径:config.autoload_paths+=%W(#{Rails.root}/应用程序/类)。但是,当我转到railsconsole时,未加载扩展。是否有一个预定义的位置可以放置我的Rails3扩展方法?或者,一种预先定义的方式来添加它们?我知道Rails有自己的数组扩展方法。我应该将我的添加到active_support/core_ext/array/conversion
参见下面的示例,我想最好使用第二种方法,但第一种也可以。哪种方法最好,使用另一种的后果是什么?classTestdefstartp"started"endtest=Test.newtest.startendclassTest2defstartp"started"endendtest2=Test2.newtest2.start 最佳答案 我肯定会说第二种变体更有意义。第一个不会导致错误,但对象实例化完全过时且毫无意义。外部变量在类的范围内不可见:var="string"classAvar=A.newendputsvar#=>strin
如果我构建了一个应用程序来访问来自Gmail、Twitter和Facebook的一些数据,并且我希望用户只需输入一次他们的身份验证信息,并且在几天或几周后重置,那会怎样是在Ruby中动态执行此操作的最佳方法吗?我看到很多人只是拥有他们客户/用户凭证的配置文件,如下所示:gmail_account:username:myClientpassword:myClientsPassword这看起来a)非常不安全,b)如果我想为成千上万的用户存储此类信息,它就无法工作。推荐的方法是什么?我希望能够在这些服务之上构建一个界面,因此每次用户进行交易时都必须输入凭据是不可行的。
我正在尝试使用docker运行一个Rails应用程序。通过github的sshurl安装的gem很少,如下所示:Gemfilegem'swagger-docs',:git=>'git@github.com:xyz/swagger-docs.git',:branch=>'my_branch'我在docker中添加了keys,它能够克隆所需的repo并从git安装gem。DockerfileRUNmkdir-p/root/.sshCOPY./id_rsa/root/.ssh/id_rsaRUNchmod700/root/.ssh/id_rsaRUNssh-keygen-f/root/.ss
我正在使用Devise在Rails应用程序中,并希望通过API公开一些模型数据,但应该像应用程序一样限制对API的访问。$curlhttp://myapp.com/api/v1/sales/7.json{"error":"Youneedtosigninorsignupbeforecontinuing."}很明显。在这种情况下是否有访问API的最佳实践?我更喜欢一步验证+获取数据,但这只是为了让客户的工作更轻松。他们将使用JQuery在客户端提取数据。感谢您提供任何信息!凡妮莎 最佳答案 我建议您按照以下帖子中的选项2:使用APIke
我正在开发一个Rails2.3.1网站。在整个网站中,我需要一个用于在各种页面(主页、创建帖子页面、帖子列表页面、评论列表页面等)上创建帖子的表单——只要说这个表单需要在由各种Controller)。这些页面中的每一个都显示在相应的Controller/操作中检索到的各种其他信息。例如,主页列出了最新的10篇文章、从数据库中提取的内容等。因此,我已将帖子创建表单移动到它自己的部分中,并将该部分包含在所有必要的页面中。请注意,部分POST中的表单到/questions(路由到PostsController::create——这是默认的Rails行为)。我遇到的问题是当Posts表单没有正