加速开发流程的 Dockerfile 最佳实践

简介: 作为开发人员,我们希望将开发环境与生产环境尽可能地匹配,以确保我们构建的内容在部署时能够正常工作。

开发流程
作为开发人员,我们希望将开发环境与生产环境尽可能地匹配,以确保我们构建的内容在部署时能够正常工作。
我们还希望能够快速开发,这意味着我们希望构建速度要快,也希望可以使用调试器之类的开发工具。容器是整理我们的开发环境的一种好方法,但是我们需要正确定义 Dockerfile 以便能够与我们的容器快速交互。
增量构建
Dockerfile 是用于构建容器镜像的一个声明清单。Docker 构建器将每个步骤的结果作为镜像层进行缓存的同时,缓存可能会无效,从而导致使缓存无效的步骤以及所有后续步骤都需要重新运行,并重新生成相应的层。
当 COPY 或 ADD 引用构建上下文中的文件发生变化时,缓存会失效。所以构建步骤的顺序可能会对构建的性能产生非常大的影响。 让我们看一个在 Dockerfile 中构建 NodeJs 项目的示例。在这个项目中,在 package.json 文件中指定了一些依赖项,这些依赖项是在运行 npm ci 命令时获取的。
最简单的 Dockerfile 文件如下所示:
FROM node:lts

ENV CI=true
ENV PORT=3000

WORKDIR /code
COPY . /code
RUN npm ci

CMD [ "npm", "start" ]
复制代码
每当构建上下文中的文件发生变化时,我们按照上述结构构建 Dockerfile 都会导致在 COPY 这一行使得缓存失效。也就是说除了会花费很长时间得 package.json 文件以外的其他任何文件发生了变更得话,都将会重新获取依赖项放置到 node_modules 目录下面去。
为了避免这种情况发送,只在依赖项发生变更时(即,当 package.json 或 package-lock.json 更改时)才重新获取依赖,我们应该考虑将依赖项安装与应用程序的构建和运行分开。
优化后得 Dockerfile 如下所示:
FROM node:lts

ENV CI=true
ENV PORT=3000

WORKDIR /code
COPY package.json package-lock.json /code/
RUN npm ci
COPY src /code/src

CMD [ "npm", "start" ]
复制代码
使用这种分离的方式,如果 package.json 或 package-lock.json 文件没有变更,则缓存将用于 RUN npm ci 指令生成的这一层。这意味着,当我们编辑应用程序源代码并进行重建时,就不会重新下载依赖项,从而节省了很多时间🎉。
在主机和容器之间保持实时加载
该技巧和 Dockerfile 并不直接相关,但我们经常听到这样的问题:在容器中运行应用程序并在主机上从 IDE 修改源代码时,如何保持代码的热更新?
在我们这里的示例,我们需要将我们的项目目录挂载到容器中,并传递一个环境变量来启用 Chokidar,该项目封装了 NodeJS 文件的更改事件。运行命令如下所示:
dockerruneCHOKIDARUSEPOLLING=truev{PWD}/src/:/code/src/ -p 3000:3000 repository/image_name
复制代码
这里我们通过 -v 将宿主机上面的代码目录挂载到容器中,当宿主机上的代码有任何变更时都会在容器中进行实时加载更新。
构建一致性
Dockerfile 最重要的事情之一就是从相同的构建上下文(源,依赖项…)构建完全相同的镜像。
这里我们将继续改进上一部分中定义的 Dockerfile。
从源上进行一致构建
如上一节所述,我们可以通过在 Dockerfile 描述中添加源文件和依赖项并在其上运行命令来构建应用程序。
但是在前面的示例中,其实我们每次运行 docker build 时都无法确认生成的镜像是否相同,为什么呢?因为每次 NodeJS 发布后,lts 标签就会指向 NodeJS 镜像的最新 LTS 版本,该版本会随着时间的推移而变化,并可能带来重大变化。所以我们可以通过对基础映像使用确定的标签来轻松解决此问题。如下所示:
FROM node:13.12.0

ENV CI=true
ENV PORT=3000

WORKDIR /code
COPY package.json package-lock.json /code/
RUN npm ci
COPY src /code/src

CMD [ "npm", "start" ]
复制代码
在下面我们还将看到使用特定标签的基础镜像还有其他优点。
多阶段和匹配合适的环境
我们针对开发构建保持一致,但是针对生产环境如何来做到这一点?
从 Docker 17.05 开始,我们可以使用多阶段构建来定义生成最终镜像的步骤。使用 Dockerfile 中的这种机制,我们可以将用于开发流程的镜像与用于生产环境的镜像分开,如下所示:
FROM node:13.12.0 AS development

ENV CI=true
ENV PORT=3000

WORKDIR /code
COPY package.json package-lock.json /code/
RUN npm ci
COPY src /code/src

CMD [ "npm", "start" ]

FROM development AS builder

RUN npm run build

FROM nginx:1.17.9 AS production

COPY –from=builder /code/build /usr/share/nginx/html
复制代码
当我们看到 FROM…… AS 这样的指令就可以知道是多构建阶段。我们现在有开发、构建和生产3个阶段。通过使用 --target 标记构建特定的开发阶段的镜像,我们可以继续将容器用于我们的开发流程。
dockerbuildtargetdevelopmenttrepository/imagename:development. docker run -e CHOKIDAR_USEPOLLING=true -v ${PWD}/src/:/code/src/ repository/image_name:development
复制代码
没有 --target 标志的 docker 构建将构建最终阶段,在我们这里就是生产镜像。我们的生产镜像只是一个 nginx 镜像,其中在前面的步骤中构建的文件被放置在了对应的位置。
生产准备
保持生产环境的镜像尽可能精简和安全是非常重要的。在生产中运行容器之前,需要检查以下几件事。
没有更多最新镜像版本
正如我们前面说的,使用特定的标签的构建步骤有助于使镜像的生成的唯一性。此外至少还有两个非常好的理由为镜像使用具体的标签:

可以很方便在容器编排系统(Swarm,Kubernetes…)中找到所有运行有镜像版本的容器。

Search in Docker engine containers using our repository/image_name:development image

dockerinspect(docker ps -q) | jq -c ‘.[] | select(.Config.Image == "repository/image_name:development") |"(.Id) (.State) (.Config)"‘

"89bf376620b0da039715988fba42e78d42c239446d8cfd79e4fbc9fbcc4fd897 {\"Status\":\"running\",\"Running\":true,\"Paused\":false,\"Restarting\":false,\"OOMKilled\":false,\"Dead\":false,\"Pid\":25463,\"ExitCode\":0,\"Error\":\"\",\"StartedAt\":\"2020-04-20T09:38:31.600777983Z\",\"FinishedAt\":\"0001-01-01T00:00:00Z\"}
{\"Hostname\":\"89bf376620b0\",\"Domainname\":\"\",\"User\":\"\",\"AttachStdin\":false,\"AttachStdout\":true,\"AttachStderr\":true,\"ExposedPorts\":{\"3000/tcp\":{}},\"Tty\":false,\"OpenStdin\":false,\"StdinOnce\":false,\"Env\":[\"CHOKIDAR_USEPOLLING=true\",\"PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin\",\"NODE_VERSION=12.16.2\",\"YARN_VERSION=1.22.4\",\"CI=true\",\"PORT=3000\"],\"Cmd\":[\"npm\",\"start\"],\"Image\":\"repository/image_name:development\",\"Volumes\":null,\"WorkingDir\":\"/code\",\"Entrypoint\":[\"docker-entrypoint.sh\"],\"OnBuild\":null,\"Labels\":{}}"

Search in k8s pods running a container with our repository/image_name:development image (using jq cli)

$ kubectl get pods –all-namespaces -o json | jq -c ‘.items[] | select(.spec.containers[].image == "repository/image_name:development")| .metadata’

{"creationTimestamp":"2020-04-10T09:41:55Z","generateName":"image_name-78f95d4f8c-","labels":{"com.docker.default-service-type":"","com.docker.deploy-namespace":"docker","com.docker.fry":"image_name","com.docker.image-tag":"development","pod-template-hash":"78f95d4f8c"},"name":"image_name-78f95d4f8c-gmlrz","namespace":"docker","ownerReferences":[{"apiVersion":"apps/v1″,"blockOwnerDeletion":true,"controller":true,"kind":"ReplicaSet","name":"image_name-78f95d4f8c","uid":"5ad21a59-e691-4873-a6f0-8dc51563de8d"}],"resourceVersion":"532″,"selfLink":"/api/v1/namespaces/docker/pods/image_name-78f95d4f8c-gmlrz","uid":"5c70f340-05f1-418f-9a05-84d0abe7009d"}
复制代码

对于 CVE(常见漏洞和披露),我们可以快速知道是否需要修补容器和镜像。在我们这里的示例,我们可以指定我们的开发和生产镜像使用 alpine 版本。

FROM node:13.12.0-alpine AS development

ENV CI=true
ENV PORT=3000

WORKDIR /code
COPY package.json package-lock.json /code/
RUN npm ci
COPY src /code/src

CMD [ "npm", "start" ]

FROM development AS builder

RUN npm run build

FROM nginx:1.17.9-alpine

COPY –from=builder /code/build /usr/share/nginx/html
复制代码
使用官方镜像
您可以使用 Docker Hub 搜索在 Dockerfile 中使用的基础镜像,其中一些是官方支持的镜像。我们强烈建议使用这些镜像:

他们的内容已经过验证
修复 CVE 后,它们会快速更新

Docker Hub 中的 nginx 官方镜像
您可以添加 image_filter 请求查询参数来获取正式版本的镜像:
https://hub.docker.com/search?q=nginx&type=image&image_filter=official
复制代码
上面我们使用的示例中均使用 NodeJS 和 NGINX 的官方镜像。
足够的权限!
无论是否在容器中运行的所有应用程序都应遵守最小特权原则,这意味着应用程序应仅访问其所需的资源。
如果出现恶意行为或错误,以太多特权运行的进程可能会在运行时对整个系统造成意外的后果。
用非特权用户身份来配置镜像本身也是非常简单的:
FROM maven:3.6.3-jdk-11 AS builder
WORKDIR /workdir/server
COPY pom.xml /workdir/server/pom.xml
RUN mvn dependency:go-offline

RUN mvn package

FROM openjdk:11-jre-slim
RUN addgroup -S java && adduser -S javauser -G java
USER javauser

EXPOSE 8080
COPY –from=builder /workdir/server/target/project-0.0.1-SNAPSHOT.jar /project-0.0.1-SNAPSHOT.jar

CMD ["java", "-Djava.security.egd=file:/dev/./urandom", "-jar", "/project-0.0.1-SNAPSHOT.jar"]
复制代码
只需创建一个新组,向其中添加一个用户,然后使用 USER 指令,我们就可以使用非 root 用户运行容器了。
结论
本文我们只是展示了通过制作 Dockerfile 来优化和保护 Docker 镜像的许多方法中的部分方法。

目录
打赏
0
1
0
0
191
分享
相关文章
快速上手 Elasticsearch:Docker Compose 部署详解
本文介绍了如何使用Docker Compose快速搭建Elasticsearch学习环境。Elasticsearch是一款用于实时搜索和分析的分布式中间件,适用于多种场景,如搜索、日志分析、机器学习等。首先,创建docker网络,拉取最新版8.12.2镜像。接着,编写docker-compose.yml文件,配置单节点集群,设置端口映射、内存限制及数据卷挂载。然后,创建并配置数据卷目录,允许远程访问和跨域。最后,启动服务并验证,通过浏览器访问确认服务运行正常。本文为初学者提供了一个简便的Elasticsearch部署方法。
1276 4
快速上手 Elasticsearch:Docker Compose 部署详解
docker centos镜像 npm安装包时报错“npm ERR! code ECONNRESET”
通过上述步骤,您可以有效解决在 Docker 中使用 CentOS 镜像安装 npm 包时遇到的 "npm ERR! code ECONNRESET" 错误。希望这些方法能帮助您顺利进行 npm 包的安装。
362 26
现货量化交易机器人系统开发策略逻辑及源码示例
现货量化交易机器人系统是一种基于计算机算法和数据分析的自动化交易工具。该系统通过制定交易策略、获取和处理数据、生成交易信号、执行交易操作和控制风险等环节,实现高效、精准的交易决策。系统架构可采用分布式或集中式,以满足不同需求。文中还提供了一个简单的双均线策略Python代码示例。
【MySQL基础篇】MySQL约束语法
文章介绍了MySQL中表的约束概念,包括非空、唯一、主键、默认和外键约束,以及如何在创建和修改表时指定这些约束。外键约束用于保持数据的一致性和完整性,文章通过示例展示了添加、删除外键的语法,并讨论了不同的删除/更新行为,如CASCADE和SETNULL。
【MySQL基础篇】MySQL约束语法
|
6月前
|
探索 Python 中链表的实现:从基础到高级
链表是一种由节点组成的基础数据结构,每个节点包含数据和指向下一个节点的引用。本文通过Python类实现单向链表,详细介绍了创建、插入、删除节点等操作,并提供示例代码帮助理解。链表在处理动态数据时具有高效性,适用于大量数据变动的场景。文章为初学者提供了全面的入门指南,助你掌握链表的核心概念与应用。
246 0
RabbitMQ之死信队列
【1月更文挑战第10天】先从概念解释上搞清楚这个定义,死信,顾名思义就是无法被消费的消息,字面意思可以这样理解,一般来说,producer 将消息投递到 broker 或者直接到 queue 里了,consumer 从 queue 取出消息进行消费,但某些时候由于特定的原因导致 queue 中的某些消息无法被消费,这样的消息如果没有后续的处理,就变成了死信,有死信自然就有了死信队列。 应用场景:为了保证订单业务的消息数据不丢失,需要使用到 RabbitMQ 的死信队列机制,当消息消费发生异常时,将消息投入死信队列中.还有比如说: 用户在商城下单成功并点击去支付后在指定时间未支付时自动失效。
609 76
String能存储多少个字符?
这篇内容讨论了Java中String的长度限制。编译时,如果字符串长度大于等于65535,编译器将报错。这是由于`CONSTANT_Utf8`常量池项的长度字段是16位无符号整数,最大值为65535。而在运行时,虽然理论上String的最大长度是2^31-1,但实际长度受限于JVM内存,最大可能占用约2GB内存,超过可能导致OutOfMemoryError。JDK9以后,对于LATIN1字符的字符串,存储优化使用byte数组,节省内存。
592 1
String能存储多少个字符?
深入浅出:使用Docker容器化改进Python应用部署
在快速演进的软件开发领域,持续集成和持续部署(CI/CD)已成为加速产品上市的关键。本文将探索如何利用Docker,一种流行的容器化技术,来容器化Python应用,实现高效、可靠的部署流程。我们将从Docker的基本概念入手,详细讨论如何创建轻量级、可移植的Python应用容器,并展示如何通过Docker Compose管理多容器应用。此外,文章还将介绍使用Docker的最佳实践,帮助开发者避免常见陷阱,优化部署策略。无论是初学者还是有经验的开发人员,本文都将提供有价值的见解,助力读者在自己的项目中实现容器化部署的转型。
【深度强化学习】DQN与倒立摆控制问题实战(图文解释 附源码)
【深度强化学习】DQN与倒立摆控制问题实战(图文解释 附源码)
764 1
AI助理

你好,我是AI助理

可以解答问题、推荐解决方案等

登录插画

登录以查看您的控制台资源

管理云资源
状态一览
快捷访问