docker学习5:提升Dockerfile水平的5个技巧

Docker 是一种容器技术,它可以在操作系统上创建多个相互隔离的容器。容器内独立安装软件、运行服务。

但是,这个容器和宿主机还是有关联的,比如可以把宿主机的端口映射到容器内的端口、宿主机某个目录挂载到容器内的目录。

比如映射了 3000 端口,那容器内 3000 端口的服务,就可以在宿主机的 3000 端口访问了。

比如挂载了 /aaa 到容器的 /bbb/ccc,那容器内读写 /bbb/ccc 目录的时候,改的就是宿主机的 /aaa 目录,反过来,改宿主机 /aaa 目录,容器内的 /bbb/ccc 也会改,这俩同一个。

这分别叫做端口映射、数据卷(volume)挂载。

这个容器是通过镜像起来的,通过 docker run image-name。

js 复制代码
docker run -d -p 3000:3000 -v /aaa:/bbb/ccc --name xxx-container xxx-image

通过 xxx-image 镜像跑起来一个叫做 xxx-container 的容器。

-p 指定端口映射,映射宿主机的 3000 到容器的 3000 端口。

-v 指定数据卷挂载,挂载宿主机的 /aaa 到容器的 /bbb/ccc 目录。

这个镜像是通过 Dockerfile 经过 build 产生的。

也就是这样的流程:

一般在项目里维护 Dockerfile ,然后执行 docker build 构建出镜像、push 到镜像仓库,部署的时候 pull 下来用 docker run 跑起来。

基本 CI/CD 也是这样的流程:

CI 的时候 git clone 项目,根据 dockerfile 构建出镜像,打上 tag,push 到仓库。

CD 的时候把打 tag 的镜像下下来,docker run 跑起来。

这个 Dockerfile 是在项目里维护的,虽然 CI/CD 流程不用自己搞,但是 Dockefile 还是要开发者自己写的。

比如创建一个 nest 项目:

js 复制代码
npx nest new dockerfile-test -p npm

然后执行 npm run build,之后把它跑起来:

js 复制代码
npm run build
node ./dist/main.js

这时候访问http://localhost:3000 ,可以看到 hello world,说明服务跑成功了。

那如何通过 Docker 部署这个服务呢?

我们来写下 Dockerfile:

js 复制代码
FROM node:18

WORKDIR /app

COPY package.json .

COPY *.lock .

RUN npm config set registry https://registry.npmmirror.com/

RUN npm install

COPY . .

RUN npm run build

EXPOSE 3000

CMD [ "node", "./app/main.js" ]

FROM node:18 是继承 node:18 基础镜像。

WORKDIR /app 是指定当前目录为 /app

COPY 复制宿主机的 package.json 和 lock 文件到容器的当前目录,也就是 /app 下

RUN 是执行命令,这里执行了 npm install。

然后再复制其余的文件到容器内。

EXPOSE 指定容器需要暴露的端口是 3000。

CMD 指定容器跑起来时执行的命令是 node ./app/main.js。

然后通过 docker build 把它构建成镜像:

js 复制代码
docker build -t dockerfile-test:first .

-t 是指定名字和标签,这里镜像名为 dockerfile-test 标签为 first。

然后在 docker desktop 的 images 里就可以看到这个镜像了:

就是现在镜像稍微大了点,有 1.45 G。

我们先跑起来看看:

js 复制代码
docker run -d -p 2333:3000 --name first-container dockerfile-test:first

现在就可以访问容器内跑的这个服务。

但是刚才也发现了,现在镜像太大了,有 1.45G 呢,怎么优化一下呢?

这就涉及到了第一个技巧:

使用 alpine 镜像,而不是默认的 linux 镜像

docker 容器内跑的是 linux 系统,各种镜像的 dockerfile 都会继承 linux 镜像作为基础镜像。

但其实这个 linux 镜像可以换成更小的版本,也就是 alpine。

它裁剪了很多不必要的 linux 功能,使得镜像体积大幅减小了。

alpine 是高山植物,就是很少的资源就能存活的意思。

我们改下 dockerfile,使用 alpine 的镜像:node:18-alpine3.14。

它是使用 18 版本的 node 镜像,它底层使用 alpine 3.14 的基础镜像。

好家伙,足足小了 900M。

然后再来看第二个技巧。

使用多阶段构建

看下这个 dockerfile,大家发现有啥问题没:

js 复制代码
FROM node:18

WORKDIR /app

COPY package.json .

COPY *.lock .

RUN npm config set registry https://registry.npmmirror.com/

RUN npm install

COPY . .

RUN npm run build

EXPOSE 3000

CMD [ "node", "./app/main.js" ]

有的同学可能会说:为什么先复制 package.json 进去,安装依赖之后再复制其他文件,直接全部复制进去不就行了?

不是的,这两种写法的效果不同。

docker 是分层存储的,dockerfile 里的每一行指令是一层,会做缓存。

每次 docker build 的时候,只会从变化的层开始重新构建,没变的层会直接复用。

也就说现在这种写法,如果 package.json 没变,那么就不会执行 npm install,直接复用之前的。

那如果一开始就把所有文件复制进去呢?

那不管 package.json 变没变,任何一个文件变了,都会重新 npm install,这样没法充分利用缓存,性能不好。

现在重新跑 docker build,不管跑多少次,速度都很快,因为文件没变,直接用了镜像缓存:

然后改下 package.json,重新跑:

时间明显多了很多,过程中你可以看到在 npm install 那层停留了很长时间。

这就是为什么要这样写。

大家还能发现有没有什么别的问题么?

问题就是源码和很多构建的依赖是不需要的,但是现在都保存在了镜像里。

实际上我们只需要构建出来的 ./dist 目录下的文件还有运行时的依赖。

那怎么办呢?

这时可以用多阶段构建:

js 复制代码
FROM node:18-alpine3.14 as build-stage

WORKDIR /app

COPY package.json .

RUN npm config set registry https://registry.npmmirror.com/

RUN npm install

COPY . .

RUN npm run build

# production stage
FROM node:18-alpine3.14 as production-stage

COPY --from=build-stage /app/dist /app
COPY --from=build-stage /app/package.json /app/package.json

WORKDIR /app

RUN npm config set registry https://registry.npmmirror.com/

RUN npm install --production

EXPOSE 3000

CMD ["node", "/app/main.js"]

FROM 后面添加一个 as 来指定当前构建阶段的名字。

通过 COPY --from=xxx 可以从上个阶段复制文件过来。

然后 npm install 的时候添加 --production,这样只会安装 dependencies 的依赖。

docker build 之后,只会留下最后一个阶段的镜像。

也就是说,最终构建出来的镜像里是没有源码的,有的只是 dist 的文件和运行时依赖。

这样镜像就会小很多。

使用 ARG 增加构建灵活性

我们写一个 test.js

js 复制代码
console.log(process.env.aaa);
console.log(process.env.bbb);

跑一下:

js 复制代码
export aaa=1 bbb=2
node ./test.js

然后我们写个 dockerfile:

js 复制代码
FROM node:18-alpine3.14

ARG aaa
ARG bbb

WORKDIR /app

COPY ./test.js .

ENV aaa=${aaa} \
    bbb=${bbb}

CMD ["node", "/app/test.js"]

使用 ARG 声明构建参数,使用 ${xxx} 来取

然后用 ENV 声明环境变量。

dockerfile 内换行使用 \

之后构建的时候传入构建参数:

js 复制代码
docker build --build-arg aaa=3 --build-arg bbb=4 -t arg-test  .

通过 --build-arg xxx=yyy 传入 ARG 参数的值。

点击查看镜像详情,可以看到 ARG 已经被替换为具体的值了:

可以看到容器内拿到的环境变量就是 ENV 设置的。

也就是说 ARG 是构建时的参数,ENV 时运行时的变量。

灵活使用 ARG,可以增加 dockerfile 的灵活性。

这就是第三个技巧。

CMD 结合 ENTRYPOINT

前面我们指定容器跑起来之后运行什么命令,用的是 CMD。

其实还可以写成 ENTRYPOINT。

这两种写法有什么区别么?

我们来试试:

js 复制代码
FROM node:18-alpine3.14

CMD ["echo", "光光", "到此一游"]

build之后run一下:

重点是用 CMD 的时候,启动命令是可以重写的:

js 复制代码
docker run cmd-test echo "东东"

可以替换成任何命令。

而用 ENTRYPOINT 就不会:

js 复制代码
FROM node:18-alpine3.14

ENTRYPOINT ["echo", "光光", "到此一游"]

build之后执行:

js 复制代码
docker run cmd-test echo "东东"

可以看到,现在 dockerfile 里 ENTRYPOINT 的命令依然执行了。

docker run 传入的参数作为了 echo 的额外参数。

这就是 ENTRYPOINT 和 CMD 的区别。

一般还是 CMD 用的多点,可以灵活修改启动命令。

其实 ENTRYPOINT 和 CMD 是可以结合使用的。

js 复制代码
FROM node:18-alpine3.14

ENTRYPOINT ["echo", "光光"]

CMD ["到此一游"]

docker run:

js 复制代码
docker run cmd-test
docker run cmd-test 66666

当没传参数的时候,执行的是 ENTRYPOINT + CMD 组合的命令,而传入参数的时候,只有 CMD 部分会被覆盖。

这就起到了默认值的作用。

所以,用 ENTRYPOINT + CMD 的方式更加灵活。

这是第四个技巧。

COPY vs ADD

其实不只是 ENTRYPOINT 和 CMD 相似,dockerfile 里还有一对指令也比较相似,就是 ADD 和 COPY。

这俩都可以把宿主机的文件复制到容器内。

但有一点区别,就是对于 tar.gz 这种压缩文件的处理上:

我们创建一个 aaa 目录,下面添加两个文件

使用 tar 命令打包:

js 复制代码
tar -zcvf aaa.tar.gz ./aaa

编写一个dockerfile:

js 复制代码
FROM node:18-alpine3.14

ADD ./aaa.tar.gz /aaa

COPY ./aaa.tar.gz /bbb

可以看到,ADD 把 tar.gz 给解压然后复制到容器内了。

而 COPY 没有解压,它把文件整个复制过去了。

也就是说,ADD、COPY 都可以用于把目录下的文件复制到容器内的目录下。

但是 ADD 还可以解压 tar.gz 文件。

一般情况下,还是用 COPY 居多。

总结

Dockerfile 有挺多技巧:

  • 使用 alpine 的镜像,而不是默认的 linux 镜像,可以极大减小镜像体积,比如 node:18-alpine3.14 这种
  • 使用多阶段构建,比如一个阶段来执行 build,一个阶段把文件复制过去,跑起服务来,最后只保留最后一个阶段的镜像。这样使镜像内只保留运行需要的文件以及 dependencies。
  • 使用 ARG 增加构建灵活性,ARG 可以在 docker build 时通过 --build-arg xxx=yyy 传入,在 dockerfile 中生效,可以使构建过程更灵活。如果是想定义运行时可以访问的变量,可以通过 ENV 定义环境变量,值使用 ARG 传入。
  • CMD 和 ENTRYPOINT 都可以指定容器跑起来之后运行的命令,CMD 可以被覆盖,而 ENTRYPOINT 不可以,两者结合使用可以实现参数默认值的功能。
  • ADD 和 COPY 都可以复制文件到容器内,但是 ADD 处理 tar.gz 的时候,还会做一下解压。

灵活使用这些技巧,可以让你的 Dockerfile 更加灵活、性能更好。

相关推荐
问简4 小时前
docker 镜像相关
运维·docker·容器
Benszen5 小时前
Docker容器化技术实战指南
运维·docker·容器
Hommy885 小时前
【开源剪映小助手】Docker 部署
docker·容器·开源·github·aigc
斯普信云原生组7 小时前
Prometheus 环境监控虚机 Redis 方案(生产实操版)
运维·docker·容器
喵了几个咪7 小时前
如何在 Superset Docker 容器中安装 MySQL 驱动
mysql·docker·容器·superset
工具罗某人7 小时前
docker compose部署kafka集群搭建
docker·容器·kafka
sbjdhjd13 小时前
Docker | 核心概念科普 + 保姆级部署
linux·运维·服务器·docker·云原生·面试·eureka
摇滚侠13 小时前
Vmvare 虚拟机安装 Linux CentOS 7 操作系统 一键安装 Docker 1Panel 一键安装 MySQL Redis OpenClaw
linux·docker·centos
comedate14 小时前
【OpenClaw】 Open-WebUI Docker 部署连接本地 Ollama 技术文档
docker·ollama·openwebui·openclaw
川trans14 小时前
基于 Docker & K8s 的 MySQL 容器化部署与应用关联实践
mysql·docker·kubernetes