Dockerfile详解

Dockerfile详解
[TOC]
📋 Dockerfile 核心指令速查表
| 执行顺序 | 指令 | 重要程度 | 简要介绍 |
|---|---|---|---|
| 1 | FROM | ⭐⭐⭐⭐⭐ | ==指定基础镜像==,必须是第一句(ARG除外) |
| 2 | ARG | ⭐⭐⭐ | ==定义构建时的变量==,在Dockerfile文件中可用 容器运行后, env命令查找不到该变量 在==构建镜像==时, 可被替换 --build-arg key=value |
| 3 | LABEL | ⭐⭐⭐⭐⭐ | ==标记== 给镜像加元数据,比如作者、版本描述,可以添加任意键值对元数据 |
| 4 | WORKDIR | ⭐⭐⭐⭐⭐ | ==设置工作目录==,后续的 RUN、CMD 等指令都在这个目录下执行 |
| 5 | ENV | ⭐⭐⭐⭐⭐ | ==设置环境变量==,比如 ENV MYPATH=/app,容器运行时也能读到 -> $MYPATH <- Dockerfile也可引用在==运行容器==时, 可以被替换 -e key=value |
| 6 | RUN | ⭐⭐⭐⭐⭐ | ==执行命令==,每执行一次会生成一个新的镜像层 |
| 7 | COPY | ⭐⭐⭐⭐⭐ | ==复制== 最推荐把本地文件复制到镜像里 |
| 8 | ADD | ⭐⭐⭐⭐⭐ | 类似 COPY,但能自动解压 tar 包或下载远程 URL 文件 |
| 9 | EXPOSE | ⭐⭐⭐⭐⭐ | ==声明端口== 告诉 Docker 容器在运行时会监听哪个端口,仅作声明,不自动映射 多个端口空格隔开 |
| 10 | VOLUME | ⭐⭐⭐⭐⭐ | ==数据卷== 创建一个挂载点,用于数据持久化,防止容器删除后数据丢失 |
| 11 | USER | ⭐⭐ | ==指定容器中的运行用户==或 UID 容器中必须有对应用户或UID |
| 12 | HEALTHCHECK | ⭐⭐⭐ | ==健康检查== 定义命令来检测容器服务是否存活 |
| 13 | ENTRYPOINT | ⭐⭐⭐⭐⭐ | 配置容器==启动后执行的命令==,且该命令不会被覆盖,适合做固定入口 后面的CMD或者 docker run命令作为==参数==传递给它 |
| 14 | CMD | ⭐⭐⭐⭐⭐ | ==默认命令== 用于容器启动时的默认参数或命令,容易被 docker run 后的参数覆盖 |
- 后面扩展指令还有三个 ---> ==共17个==
常用指令
WORKDIR指定工作目录
1)-w 手动指定工作目录✅️ 在运行容器的同时指定工作目录[root@Docker ~]# docker run -it --name v1 -w /usr/local/bin alpine:3.20.2/usr/local/bin # pwd"/usr/local/bin"
2)Dockerfile构建中的WORKDIR[root@Docker ~]# vim dockerfile-01FROM alpine:3.20.2WORKDIR /usr/local/bin[root@Docker ~]# docker build -f ./dockerfile-01 -t test:1.0 .[root@Docker ~]# docker run -it --name t01 test:1.0/usr/local/bin # pwd"/usr/local/bin"EXPOSE暴露端口
- 多个端口空格隔开
[root@Docker ~]# vim default.confserver { listen 80 default_server; server_name _; root /code;
location / { index index.html; }}[root@Docker ~]# vim dockerfile-02# 这里没有指定暴漏的端口80FROM alpine:3.20.2LABEL maintainer="Jiuzhao"WORKDIR /usr/RUN apk update && apk add nginxRUN mkdir /codeCOPY ./default.conf /etc/nginx/http.d/RUN echo "This is my page" > /code/index.htmlCMD ["nginx","-g","daemon off;"][root@Docker ~]# docker build -f dockerfile-02 -t test:2.0 .[root@Docker ~]# docker run -d -P --name t01 test:2.0'随机暴露容器端口' -P(大写)[root@Docker ~]# docker psIMAGE COMMAND PORTS NAMEStest:2.0 "nginx -g 'daemon of…" t01'✅️ 在构建镜像的时候,没有EXPOSE暴漏出来端口✅️ 这里也不会随机映射出来端口来[root@Docker ~]# docker run -d -p 90:80 --name t02 test:2.0[root@Docker ~]# docker ps6d4aed83832f test:2.0 "nginx -g 'daemon of…" 2 seconds ago Up 1 second 0.0.0.0:90->80/tcp, [::]:90->80/tcp'✅️ 虽然不可以通过-P(大写)随机映射,但我们可以-p(小写)指定端口映射✅️ 毕竟容器里面,运行着Nginx服务[root@Docker ~]# curl localhost:90This is my page
2)暴漏端口"多个端口空格隔开"[root@Docker ~]# vim dockerfile-02# 在Dockerfile中加入下面这句话EXPOSE 80[root@Docker ~]# docker build -f dockerfile-02 -t test:3.0 .[root@Docker ~]# docker run -d --name t03 test:3.0'先不加-P'[root@Docker ~]# docker psIMAGE COMMAND PORTS NAMEStest:3.0 "nginx -g 'daemon of…" 80/tcp t03''没有-P,只是提示你,有个端口可以映射' --> 仅做声明[root@Docker ~]# docker run -d -P --name t04 test:3.0✅️ -P能够随机映射到宿主机端口,前提是你得暴漏端口[root@Docker ~]# docker ps3ede1c2b420b test:3.0 "nginx -g 'daemon of…" 2 seconds ago Up 1 second 0.0.0.0:32768->80/tcp, [::]:32768->80/tcp t04'# 容器中的80端口映射到宿主机中的32768端口[root@Docker ~]# curl localhost:32768This is my pageADD拷贝
COPY 与 ADD 的选择
- 最佳实践是优先使用
COPY - 只有当你需要自动解压本地压缩包(如
.tar.gz)时,才使用ADD - 💚相同点:
- 都可以拷贝文件 —> 从宿主机到容器
- ❤️不同点:
- COPY 可以用于多阶段构建 <— 后面笔记📚有
COPY指令支持--from=<stage>这个参数来实现跨阶段复制- ADD无法实现 ❌️
- ADD 指令支持解压tar包
- 仅支持tar包, zip包不能解压
- COPY 可以用于多阶段构建 <— 后面笔记📚有
🔍 分层存储机制理解
使用COPY:分为两个层级(COPY tar包 → RUN解压)
-
为了让镜像更加的轻量, 我们在 RUN 解压tar包 && 删除tar包
- 放在一条命令中
-
==实际上==:虽然最终联合视图中用户看不到tar包,但镜像大小仍包含这些内容
-
本质:分层存储中,删除操作只是在当前层(RUN层)标记删除
- 实际数据仍存在于镜像中(==COPY层中==)
🎯 Dockerfile构建优化
- build构建镜像的时候, 是有缓存的
- 存在于宿主机中
缓存机制:
- 从上到下逐层构建
- 如果某一层的指令与之前构建的镜像层相同,则直接使用缓存
- 一旦某一层没有命中缓存,其后的所有层都将重新构建
如何充分利用缓存呢❓️
- 指令顺序优化
- 将基本不变、不常改动的指令放在Dockerfile前面
- 这些层最容易命中缓存
- 后续变化频繁的层更新时,不会影响前面的缓存
🚀 ==镜像瘦身技巧==
- 在同一
RUN指令中完成安装和清理:- 基于 alpine 镜像
- 合并在一个层,确保中间产物不会增加最终镜像大小
- ⬇️ 缓存被真正清除
RUN apk update && \ apk add nginx && \ rm -rf /var/cache- ❌️==错误示范==
RUN apk update && apk add nginx...其他指令...RUN rm -rf /var/cache-
类比👆的(COPY tar包 → RUN 解压&&删除 tar包)
- 最终并没有彻底删除掉
-
即使后续指令删除了缓存文件,之前的层中仍然保留着这些文件
FROM alpine:3.20.2LABEL maintainer="Jiuzhao"WORKDIR /usr/RUN apk update && \ apk add nginx && \ rm -rf /var/cacheRUN mkdir /codeCOPY ./default.conf /etc/nginx/http.d/RUN echo "This is my page" > /code/index.htmlCMD ["nginx","-g","daemon off;"]"清理环境"[root@Docker ~]# docker rm -f $(docker ps -aq)[root@Docker ~]# docker rmi test:1.0 test:2.0 test:3.0 server:1.0# 把没有用的镜像也删除了
[root@Docker ~]# vim dockerfile-01[root@Docker ~]# lsdefault.conf dockerfile-01[root@Docker ~]# docker build -f dockerfile-01 -t web:1.0 .[root@Docker ~]# docker imagesweb:1.0 6894a72f7fc8 13.9MBENTRYPOINT启动命令
RUN 与 CMD/ENTRYPOINT 的区别
-
RUN:是在构建镜像时执行的
- 比如安装软件,构建完就结束
-
CMD/ENTRYPOINT:是在容器启动时执行的
- 比如启动 Nginx 服务,是镜像运行后的==主进程==
- 多个 CMD 指令,只有最后一个会被执行
==面试题==: ENTRYPOINT和CMD的区别
-
相同点: 当用户没有指定容器的启动命令时,都可以作为容器的默认启动命令
-
不同点:
-
CMD指定的启动命令
-
容易被
docker run后的参数覆盖 -
ENTRYPOINT
-
该命令不会被覆盖,适合做固定入口
-
与后面的CMD或者
docker run命令==同时使用==时- 将作为==参数==传递给它
-
1)CMD指令演示[root@Docker ~]# docker run -d --name v1 web:1.0'正常启动'# 默认CMD,启动Nginx服务[root@Docker ~]# docker run -d --name v2 web:1.0 tail -f /etc/hosts'给了运行命令' --> ⚠️覆盖原来的 CMD nginx -g daemon off;[root@Docker ~]# docker psIMAGE COMMAND NAMESweb:1.0 "tail -f /etc/hosts" v2web:1.0 "nginx -g 'daemon of…" v1'[root@Docker ~]# docker exec v1 ps -efPID USER TIME COMMAND1 root 0:00 nginx: master process nginx -g daemon off;7 nginx 0:00 nginx: worker process8 nginx 0:00 nginx: worker process9 root 0:00 ps -ef[root@Docker ~]# docker exec v2 ps -efPID USER TIME COMMAND1 root 0:00 tail -f /etc/hosts7 root 0:00 ps -ef✅️ v2的Nginx并没有启动✅️ `CMD指令` 容易被 `docker run` 后的参数覆盖❤️ 我们可以把 run 后面的参数也理解为一个 CMD指令 "多个CMD指令,只会执行最后一个,被覆盖了"
2)ENTRYPOINT指令演示[root@Docker ~]# sed -i 's#CMD#ENTRYPOINT#' dockerfile-01[root@Docker ~]# tail -1 dockerfile-01ENTRYPOINT ["nginx","-g","daemon off;"][root@Docker ~]# docker build -f dockerfile-01 -t web:2.0 .[root@Docker ~]# docker run -d --name c1 web:2.0# 依旧是默认启动[root@Docker ~]# docker run -d --name c2 web:2.0 tail -f /etc/hosts# 继续以 tail -f /etc/hosts 这个命令启动[root@Docker ~]# docker ps -aIMAGE COMMAND STATUS NAMESweb:2.0 "nginx -g 'daemon of…" Exited (1) c2'web:2.0 "nginx -g 'daemon of…" Up 2 minutes c1'⚠️ c2退出了,而且是🔴异常退出的# 从这个角度看 COMMAND命令,似乎一样![root@Docker ~]# docker ps -a --no-trunc# 把它展开"nginx -g 'daemon off;' tail -f /etc/hosts" Exited (1) c2"nginx -g 'daemon off;'" Up 6 minutes c1✅️ 后面紧跟着 tail -f /etc/hosts 参数✅️ 后面的命令(CMD指令)将作为"参数"传给启动命令✅️ ENTRYPOINT没法被覆盖,就算用户run时指定了命令,也只能作为参数传递# 因为没有"nginx -g 'daemon off;' tail -f /etc/hosts"这条命令# 容器异常退出 Exited (1)
3)错误Dockerfile# 验证传参[root@Docker ~]# vim dockerfile-01[root@Docker ~]# tail -2 dockerfile-01ENTRYPOINT ["nginx","-g","daemon off;"]CMD ["tail","-f","/etc/hosts"][root@Docker ~]# docker build -f dockerfile-01 -t web:3.0 .[root@Docker ~]# docker run -d --name c3 web:3.0[root@Docker ~]# docker ps -a --no-trunc | grep c3web:3.0 "nginx -g 'daemon off;' tail -f /etc/hosts" 46 seconds ago Exited (1) 45 seconds ago c3# 运行后,直接退出了,Exited (1)# 看命令,CMD被作为参数传递给ENTRYPOINT
4)✅️正确的使用[root@Docker ~]# vim dockerfile-01[root@Docker ~]# tail -2 dockerfile-01ENTRYPOINT ["tail","-f"]CMD ["/etc/hosts"][root@Docker ~]# docker build -f dockerfile-01 -t web:4.0 .[root@Docker ~]# docker run -d --name c4 web:4.0[root@Docker ~]# docker ps | grep c4web:4.0 "tail -f /etc/hosts" Up 12 seconds c4❤️ 看启动命令 ❤️ ✅️ "当CMD和ENTRYPOINT一起使用时" ✅️ CMD将作为参数传递给ENTRYPOINT[root@Docker ~]# docker run -d --name c5 web:4.0 /etc/hostname[root@Docker ~]# docker ps | grep c5web:4.0 "tail -f /etc/hostname" Up 7 seconds c5✅️ /etc/hostname覆盖掉了CMD ["/etc/hosts"],现在是查看/etc/hostname✅️ ENTRYPOINT ["tail","-f"]依旧没有改变,适合作为固定入口❤️ 把docker run后面的命令,看做CMD指令,覆盖掉前面的CMD指令VOLUME数据卷
==数据卷== 创建一个挂载点,用于数据持久化,防止容器删除后数据丢失
在Dockerfile中使用VOLUME指令时
- 它只能指定容器内的路径 ✅️
- ==匿名挂载==
- 而不能指定宿主机上的具体路径 ❌️
'环境清理'[root@Docker ~]# docker rm -f $(docker ps -aq)[root@Docker ~]# docker rmi web:1.0 web:2.0 web:3.0 web:4.0[root@Docker ~]# docker volume prune -fa# -a选项,即使是手动创建的,也可以删除掉[root@Docker ~]# docker volume lsDRIVER VOLUME NAME# 没有任何数据卷
1)修改dockerfile文件[root@Docker ~]# vim dockerfile-01[root@Docker ~]# tail -2 dockerfile-01VOLUME /code/CMD ["nginx","-g","daemon off;"]✅️ 这里只需要指定容器里面的路径即可[root@Docker ~]# docker build -f dockerfile-01 -t web:1.0 .[root@Docker ~]# docker volume lsDRIVER VOLUME NAME# 此时依旧没有数据卷被创建[root@Docker ~]# docker run -d --name v1 web:1.0'这里并没有指定数据卷!'[root@Docker ~]# docker volume lsDRIVER VOLUME NAMElocal 1b6146e932...xxx'有对应的匿名数据卷创建出来了!'[root@Docker ~]# docker inspect v1 | grep -A 5 Mounts "Mounts": [ { "Type": "volume", "Name": "1b6146e932e5d73992bedc544aa13298917b16677c28567b93b46149977e1d90", "Source": "/var/lib/docker/volumes/1b6146e932e5d73992bedc544aa13298917b16677c28567b93b46149977e1d90/_data", "Destination": "/code",[root@Docker ~]# cd /var/lib/docker/volumes/1b6146e932e5d73992bedc544aa13298917b16677c28567b93b46149977e1d90/_data[root@Docker _data]# lsindex.html[root@Docker _data]# cat index.htmlThis is my page[root@Docker _data]# echo Hello > index.html'更改容器里面的默认主页'[root@Docker _data]# docker exec v1 tail -1 /etc/hosts172.17.0.2 a1030f07fc29[root@Docker _data]# curl 172.17.0.2HelloENV环境变量
-
使用键值对的形式
-
ENV
= -
比如
ENV MYPATH=/app -
容器运行时也能读到 -> $MYPATH <- Dockerfile也可引用
-
-
等价于
docker run -e 传递环境变量
[root@Docker ~]# vim dockerfile-01[root@Docker ~]# tail -4 dockerfile-01ENV school=oldboyedu \ name=JiuzhaoRUN echo "$name Welcome To You"CMD ["nginx","-g","daemon off;"][root@Docker ~]# docker build -f dockerfile-01 -t web:2.0 .=> [7/7] RUN echo "Jiuzhao Welcome To You"✅️ 在Dockerfile中,ENV变量也可以被解析和使用[root@Docker ~]# docker run -d --name v2 web:2.0[root@Docker ~]# docker exec v2 envPATH=...xxx/usr/sbin:/usr/bin:/sbin:/binHOSTNAME=9b982519ccd6school=oldboyeduname=Jiuzhao# 这些环境变量都可以读取到HEALTHCHECK健康检查
- 定义命令来检测容器服务是否存活
- 用
curl多一点
- 用
💡 depends_on(docker compose) 默认只等容器启动,不等服务就绪,加 healthcheck 可以解决启动顺序问题
HEALTHCHECK [OPTIONS] CMD command'当然你也可以使用别的command命令进行健康监测💚'
HEALTHCHECK --interval=10s --timeout=10s --retries=3 \ CMD curl -f http://localhost/ || exit 1--interval # 执行健康检查的间隔时间(默认: 30s)--timeout # 单次健康检查的超时时间(默认: 30s) 🌰 一个检查指令过去,30s都没有回应,则认为此次检查失败 ✅️ "一般和上面间隔时间保持一致"--retries # 连续失败多少次后将容器标记为 unhealthy 🌰 间隔 n 秒进行检查================================="start_period" 参数不是 Docker 健康检查的必需参数📌 "预热期" --> 给它"预热时间"start_period: 1.定义容器启动后的初始等待时间 2.在这段时间内,健康检查不会被视为失败 ✅️ 3.默认值为 0 秒以下情况建议保留:🌰 你的 MySQL 容器需要较长的启动时间(比如需要加载大量数据)🌰 你想避免在 MySQL 初始化期间出现健康检查失败的误报
curl -f# 如果访问成功(返回 200-399 状态码) # curl 退出码为 0,健康检查通过# 如果访问失败(返回 400-599 状态码) # curl 退出码为非零值,触发 || exit 1,健康检查失败
"这里访问的是容器本地,所以容器得启动Nginx"# 如果Nginx没有起来,curl失败,标记为不健康[root@Docker ~]# vim dockerfile-01[root@Docker ~]# tail -3 dockerfile-01HEALTHCHECK --interval=10s --timeout=10s --retries=3 \ CMD curl -f http://localhost/ || exit 1CMD ["nginx","-g","daemon off;"][root@Docker ~]# docker build -f dockerfile-01 -t web:3.0 .[root@Docker ~]# docker ps | grep v3"nginx -g 'daemon of…" Up(health: starting) v3'# 现在的状态是启动中ing🦄等了半分钟后...."nginx -g 'daemon of…" Up(unhealthy) v3'❌️ 变为了不健康了! ❌️[root@Docker ~]# docker exec v3 ps -efPID USER TIME COMMAND1 root 0:00 nginx: master process nginx -g daemon off;7 nginx 0:00 nginx: worker process8 nginx 0:00 nginx: worker process102 root 0:00 ps -ef'可是明明启动着 --> Nginx'[root@Docker ~]# docker exec v3 tail -1 /etc/hosts172.17.0.4 9484756b05e1[root@Docker ~]# curl 172.17.0.4This is my page# 也可拉取页面呀!===============================所以问题到底出现在哪里呢❓️⚠️一定要"在容器中"手动执行命令试试,观察👀问题到底出现在哪![root@Docker ~]# docker exec v3 curl http://localhost"curl": executable file not found in $PATH# 用curl确实是没有拉取到页面!'❌️容器里面没有curl命令!'[root@Docker ~]# docker exec v3 apk add curlOK: 15 MiB in 26 packages# 下载完成之后,再次查看状态[root@Docker ~]# docker ps"nginx -g 'daemon of…" Up(healthy) v3'✅️ 现在这个状态就对了===============================Dockerfile文件优化[root@Docker ~]# vim dockerfile-01[root@Docker ~]# tail -4 dockerfile-01RUN apk add curlHEALTHCHECK --interval=10s --timeout=10s --retries=3 \ CMD curl -f http://localhost/ || exit 1CMD ["nginx","-g","daemon off;"][root@Docker ~]# docker build -f dockerfile-01 -t web:4.0 .[root@Docker ~]# docker run -d --name v4 web:4.0[root@Docker ~]# docker ps | grep v4"nginx -g 'daemon of…" Up(health: starting) v4'🦄 最后也是变为健康❤️了"nginx -g 'daemon of…" Up(healthy) v4'服务案例
"环境清理"[root@Docker ~]# docker rm -f $(docker ps -aq)[root@Docker ~]# docker rmi web:1.0 web:2.0 web:3.0 web:4.0
🦄查看最新创建容器的IP地址"{{.NetworkSettings.Networks.bridge.IPAddress}}"[root@Docker ~]# docker run -d -it --name v1 alpine:3.20.2[root@Docker ~]# docker inspect -f {{.NetworkSettings.Networks.bridge.IPAddress}} v1172.17.0.2# 但是这里是指定容器 v1'把v1换成最新创建的容器即可'[root@Docker ~]# docker ps -l# 展示最新latest创建的容器alpine:3.20.2 "/bin/sh" Up 5 minutes v1[root@Docker ~]# docker ps -lq-q # 只显示容器id0884c7b3fb9d[root@Docker ~]# docker ps -n=1 -q0884c7b3fb9d-n=1 # 也是展示最新创建的容器[root@Docker ~]# docker inspect -f {{.NetworkSettings.Networks.bridge.IPAddress}} $(docker ps -n=1 -q)172.17.0.2'综合一下 --> 可以写到Xshell的快捷键中'ssh自定义密码
"基础镜像Alpine"制作镜像,要求支持ssh服务,具体要求如下:✅️ 支持启动容器时修改密码,若不指定则默认密码为:"123456"
1)安装&&启动ssh服务[root@Docker ~]# docker run -d -it --name t1 alpine:3.20.2[root@Docker ~]# docker exec -it t1 sh/ # sshdsh: sshd: not found'这里是ssh让别人可以登录,所以需要下载server服务端'/ # apk add openssh-serverOK: 9 MiB in 17 packages/ # sshdsshd re-exec requires execution with an absolute path# 需要绝对路径/ # which sshd/usr/sbin/sshd/ # /usr/sbin/sshdsshd: no hostkeys available -- exiting.'缺少有效的主机密钥对'✅️ 需要的是主机密钥,而不是用户密钥对❌️ 用户密钥对: ssh-keygen -t ed25519 -f ~/.ssh/my_key -N ''✅️ 主机秘钥对: ssh-keygen -A/ # /usr/sbin/sshd'以绝对路径启动'/ # ps -efPID USER COMMAND23 root sshd: /usr/sbin/sshd [listener] 0 of 10-100 startups# 出现这个进程就是sshd运行成功了
2)远程测试[root@Docker ~]# docker inspect -f {{.NetworkSettings.Networks.bridge.IPAddress}} $(docker ps -n=1 -q)172.17.0.3# 查看它的IP地址[root@Docker ~]# ssh root@172.17.0.3The host '172.17.0.3 (172.17.0.3)' can't be established.'Are you sure you want to continue connecting (yes/no/[fingerprint])? yesroot@172.17.0.3's password: 'Permission denied, please try again.⚠️ 这里忘记给root秘密了# 进入到容器中/ # passwd rootChanging password for rootNew password: "1"Bad password: too shortRetype password: "1"passwd: password for root changed by root
3)再次测试[root@Docker ~]# ssh root@172.17.0.3root@172.17.0.3's password: 'Permission denied, please try again."明明我输入的就是1,但是还是拒绝连接了!"❓️✅️ 默认是拒绝root远程连接的/ # grep PermitRootLogin /etc/ssh/sshd_config#PermitRootLogin prohibit-password"把#去掉,并yes允许root远程登录"#后向引用/ # sed -ri 's@#(PermitRootLogin).*@\1 yes@' /etc/ssh/sshd_config-r # 支持扩展正则s@@@@g # 因为这里有#\1 # 代表的就是括起来的(),把想要的取出来即可/ # grep PermitRootLogin /etc/ssh/sshd_configPermitRootLogin yes
4)再次测试[root@Docker ~]# ssh root@172.17.0.3root@172.17.0.3's password: 'Permission denied, please try again.# 好像还是不行啊"重启sshd服务试试"/ # killall sshd/ # /usr/sbin/sshd'杀死后,在启动'[root@Docker ~]# ssh root@172.17.0.3root@172.17.0.3's password: 'Welcome to Alpine!b63803a3e7ed:~# tail -1 /etc/hosts172.17.0.3 b63803a3e7ed✅️ 成功登录上去了1)创建对应的目录[root@Docker ~]# mkdir /lb-web-ssh[root@Docker ~]# cd /lb-web-ssh/[root@Docker lb-web-ssh]#[root@Docker lb-web-ssh]# mkdir ./source# 存放配置文件[root@Docker lb-web-ssh]# mkdir ./dockerfile# Dockerfile文件[root@Docker lb-web-ssh]# tree /lb-web-ssh//lb-web-ssh/├── dockerfile└── source
2)v0.1版root@Docker lb-web-ssh]# vim ./source/default.confserver { listen 80 default_server; server_name _; root /code;
location / { index index.html; }}[root@Docker lb-web-ssh]# vim ./dockerfile/v0.1'同时安装curl为后面健康检查❤️做准备'# 同时下载sshd --> 允许别人连接FROM alpine:3.20.2LABEL maintainer="Jiuzhao"EXPOSE 80 22RUN apk update && \ apk add nginx curl openssh-server && \ rm -rf /var/cacheRUN mkdir /code && \ ssh-keygen -A && \ sed -ri 's@#(PermitRootLogin).*@\1 yes@' /etc/ssh/sshd_configCOPY ./default.conf /etc/nginx/http.d/RUN echo "This is my page" > /code/index.htmlCMD ["nginx","-g","daemon off;"]'默认启动Nginx'[root@Docker lb-web-ssh]# docker build -f ./dockerfile/v0.1 -t web:0.1 ./source/[+] Building 43.9s (10/10) FINISHED[root@Docker lb-web-ssh]# docker images web:0.1web:0.1 97993117c2a2 24.3MB[root@Docker lb-web-ssh]# docker rm -f $(docker ps -aq)[root@Docker lb-web-ssh]# docker run -d --name v1 web:0.1[root@Docker lb-web-ssh]# docker exec -it v1 sh/ # passwd root'先去给root密码'Changing password for rootNew password:Bad password: too shortRetype password:passwd: password for root changed by root/ # ps -efPID USER TIME COMMAND 1 root 0:00 nginx: master process nginx -g daemon off; 7 nginx 0:00 nginx: worker process 8 nginx 0:00 nginx: worker process 9 root 0:00 sh 16 root 0:00 ps -ef/ # /usr/sbin/sshd'启动sshd服务'/ # ps -efPID USER TIME COMMAND 1 root 0:00 nginx: master process nginx -g daemon off; 7 nginx 0:00 nginx: worker process 8 nginx 0:00 nginx: worker process 9 root 0:00 sh19 root 0:00 sshd: /usr/sbin/sshd [listener] 0 of 10-100 startups20 root 0:00 ps -ef/ # exit[root@Docker lb-web-ssh]# docker inspect -f {{.NetworkSettings.Networks.bridge.IPAddress}} $(docker ps -n=1 -q)172.17.0.2[root@Docker lb-web-ssh]# ssh root@172.17.0.2root@172.17.0.2's password: 'Welcome to Alpine!4bb94c86838a:~# tail -1 /etc/hosts172.17.0.2 4bb94c86838a==================================缺点: 1.需要手动启动sshd服务 2.需要手动给root密码v1.0优化
1)多服务启动# 写一个脚本来把它们同时启动起来'Alpine镜像中没有bash只有sh'/ # /bin/bash❌️ sh: /bin/bash: not found/ # /bin/sh/ # ✅️ exit/ #[root@Docker lb-web-ssh]# vim ./source/start.sh'后面需要把这个脚本也拷贝过去'#!/bin/sh
# start sshd/usr/sbin/sshd
# start nginxnginx -g "daemon off;"==================================COPY ./start.sh /code/CMD ["/bin/sh","/code/start.sh"]
2)非交互式地设置密码/ # chpasswd --helpBusyBox v1.36.1 (2024-06-10 07:11:47 UTC)'Alpine默认是有的,所以就不用下载了'/ # echo "root:123456" | chpasswdchpasswd: password for 'root' changed[root@Docker ~]# ssh root@172.17.0.2root@172.17.0.2's password:' --> 123456Welcome to Alpine!4bb94c86838a:~# 密码成功被修改✅️==================================# 设置一个变量,并修改root密码ENV ROOT_PASSWD=123456RUN echo "root:${ROOT_PASSWD}" | chpasswd
3)Dockerfile编写✅️ 加上健康检查[root@Docker lb-web-ssh]# vim ./dockerfile/v1.0'重新整合'FROM alpine:3.20.2LABEL maintainer="Jiuzhao"ENV ROOT_WD=123456EXPOSE 80 22RUN apk update && \ apk add nginx curl openssh-server && \ rm -rf /var/cacheRUN mkdir /code && \ ssh-keygen -A && \ echo "root:${ROOT_WD}" | chpasswd && \ echo "This is my page" > /code/index.html && \ sed -ri 's@#(PermitRootLogin).*@\1 yes@' /etc/ssh/sshd_configCOPY ./default.conf /etc/nginx/http.d/COPY ./start.sh /code/HEALTHCHECK --interval=10s --timeout=10s --retries=3 \ CMD curl -f http://localhost/ || exit 1CMD ["/bin/sh","/code/start.sh"]ENV 中的key值不要太敏感 —> PASSWD
- 不然后面build构建镜像时,会有警告⚠️
- Do not use ARG or ENV instructions(介绍) for sensitive(敏感的) data
- ENV “ROOT_PASSWD”
- 后面改为
ROOT_WD就不警告了✅️
COPY 指令本身永远无法执行多个命令,因为:
COPY是 Docker 的配置指令,不是 shell 命令- ==&& \==❌️ 不可行
- 用两个COPY指令实现
[root@Docker lb-web-ssh]# tree /lb-web-ssh//lb-web-ssh/├── dockerfile│ ├── v0.1│ └── v1.0└── source ├── default.conf └── start.sh[root@Docker lb-web-ssh]# docker build -f ./dockerfile/v1.0 -t web:1.0 ./source/[root@Docker lb-web-ssh]# docker images web:1.0web:1.0 df6624c37956 24.3MB[root@Docker lb-web-ssh]# docker run -d --name v2 web:1.0[root@Docker lb-web-ssh]# docker ps"/bin/sh /code/start…" Up(health: starting) 22/tcp,80/tcp v2# 再次查看 health: starting --> healthy[root@Docker lb-web-ssh]# docker inspect -f {{.NetworkSettings.Networks.bridge.IPAddress}} $(docker ps -n=1 -q)172.17.0.3[root@Docker lb-web-ssh]# curl 172.17.0.3This is my page[root@Docker lb-web-ssh]# ssh root@172.17.0.3@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ WARNING: REMOTE HOST IDENTIFICATION HAS CHANGED! @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@'这个登录不上去是因为,同一个IP,我们以前有ssh登录过,记录的有对应的指纹'# 但是这个IP的指纹和之前登陆过的指纹不一致,所以不让你登录✅️ 防止中间人攻击[root@Docker lb-web-ssh]# > /root/.ssh/known_hosts# 把里面的指纹清理一下[root@Docker lb-web-ssh]# ssh root@172.17.0.3root@172.17.0.3's password': --> 123456Welcome to Alpine!7550665f3771:~# tail -1 /etc/hosts172.17.0.3 7550665f3771✅️ 这次使用的是默认密码 123456'我们也可以 docker run -e ROOT_WD=指定密码'[root@Docker lb-web-ssh]# docker run -d -e ROOT_WD=passwd --name test web:1.0[root@Docker lb-web-ssh]# docker inspect -f {{.NetworkSettings.Networks.bridge.IPAddress}} $(docker ps -n=1 -q)172.17.0.4[root@Docker lb-web-ssh]# ssh root@172.17.0.4root@172.17.0.4's password': --> passwdPermission denied, please try again.root@172.17.0.4's password': --> passwdPermission denied, please try again.root@172.17.0.4's password': --> 123456Welcome to Alpine!9367d6e642d1:~# exitConnection to 172.17.0.4 closed.'密码没有改过来!'
为什么呢❓️# 在构建镜像的时候RUN echo "root:${ROOT_WD}" | chpasswd# 已经把它写死了'这个命令应该放在启动脚本中start.sh'# 👆 容器的启动命令# 当有新的变量出现时,重新给root赋值[root@Docker lb-web-ssh]# echo 'echo "root:${ROOT_WD}" | chpasswd' >> ./source/start.sh'追加到脚本末尾'[root@Docker lb-web-ssh]# tail -1 ./source/start.shecho "root:${ROOT_WD}" | chpasswd[root@Docker lb-web-ssh]# sed -i '/root:${ROOT_WD}/d' ./dockerfile/v1.0✅️ 并在Dockerfile中删除它并加入到启动脚本[root@Docker lb-web-ssh]# docker build -f ./dockerfile/v1.0 -t web:1.1 ./source/# 这次是1.1版本[root@Docker lb-web-ssh]# docker run -d --name hh -e ROOT_WD=oldboy web:1.1[root@Docker lb-web-ssh]# docker inspect -f {{.NetworkSettings.Networks.bridge.IPAddress}} $(docker ps -n=1 -q)172.17.0.5[root@Docker lb-web-ssh]# ssh root@172.17.0.5root@172.17.0.5's password': --> oldboyPermission denied, please try again.root@172.17.0.5's password': --> 123456Permission denied, please try again.❌️ 两个密码都不对!!![root@Docker lb-web-ssh]# docker inspect hh# 看看环境变量传过去了没有❓️"Env": [ "ROOT_WD=oldboy",# 变量也对呀!✅️ 实际上 echo "root:${ROOT_WD}" | chpasswd 压根没有执行[root@Docker lb-web-ssh]# tail -3 ./source/start.sh# start nginxnginx -g "daemon off;"echo "root:${ROOT_WD}" | chpasswd✅️ nginx放在前台运行了,会被阻塞住# --> 后面的命令没有办法执行'调整一下位置就可以!'[root@Docker lb-web-ssh]# tail -3 ./source/start.shecho "root:${ROOT_WD}" | chpasswd# start nginxnginx -g "daemon off;"[root@Docker lb-web-ssh]# docker build -f ./dockerfile/v1.0 -t web:1.2 ./source/⚠️ 这次是1.2版本[root@Docker lb-web-ssh]# docker run -d -e ROOT_WD=oldboy --name xx web:1.2[root@Docker lb-web-ssh]# docker inspect -f {{.NetworkSettings.Networks.bridge.IPAddress}} $(docker ps -n=1 -q)172.17.0.6[root@Docker lb-web-ssh]# ssh root@172.17.0.6root@172.17.0.6's password: 'Welcome to Alpine!63dd4cdc2145:~# tail -1 /etc/hosts172.17.0.6 63dd4cdc2145✅️ '终于是可以了'[root@Docker lb-web-ssh]# docker run -d --name fs web:1.2# 不传参,默认就是123456[root@Docker lb-web-ssh]# docker inspect -f {{.NetworkSettings.Networks.bridge.IPAddress}} $(docker ps -n=1 -q)172.17.0.7[root@Docker lb-web-ssh]# ssh root@172.17.0.7'有点问题! --> 我们是在Dockerfile中写的 ssh-keygen -A '# 主机秘钥被写死了!# 后面基于它的启动的容器,主机指纹是一致的ED25519 key fingerprint is SHA256:BKCNN5J6r8iW2iu0gAs5eQzdW6wGBM7xtOcxQtNJves.This host key is known by the following other names/addresses: ~/.ssh/known_hosts:5: 172.17.0.5 🌰 ~/.ssh/known_hosts:8: 172.17.0.6 🌰root@172.17.0.7's password: 'Welcome to Alpine!ca0b892545c9:~# exit✅️ 删除Dockerfile中的命令,把它放在启动脚本中# 这样每次启动的容器,主机指纹也是不一样的![root@Docker lb-web-ssh]# sed -i '/ssh-keygen/d' ./dockerfile/v1.0[root@Docker lb-web-ssh]# vim ./source/start.sh'生成主机密钥的指令需要在启动sshd服务之前才行!!'❌️ 否则sshd没有办法启动起来#!/bin/bash# start sshdssh-keygen -A/usr/sbin/sshdecho "root:${ROOT_WD}" | chpasswd# start nginxnginx -g "daemon off;"[root@Docker lb-web-ssh]# docker build -f ./dockerfile/v1.0 -t web:1.3 ./source/⚠️ 这次是1.3版本[root@Docker lb-web-ssh]# docker run -d --name c1 web:1.3[root@Docker lb-web-ssh]# docker run -d --name c2 -e ROOT_WD=123 web:1.3[root@Docker lb-web-ssh]# docker run -d --name c3 -e ROOT_WD=passwd web:1.3'IP地址分别是 172.17.0.8 172.17.0.9 172.17.0.10 '[root@Docker lb-web-ssh]# ssh root@172.17.0.8ED25519 key fingerprint is SHA256:I+hlnxgcM2siRncwqna996MSvsKQZbH4DUMlIJXLlH0.root@172.17.0.8's password: 'Welcome to Alpine![root@Docker lb-web-ssh]# ssh root@172.17.0.9ED25519 key fingerprint is SHA256:AGRRebloBJFZ5/RSZS8HP/lEG1tSua5jS5MUXCDnld0.root@172.17.0.9's password: 'Welcome to Alpine![root@Docker lb-web-ssh]# ssh root@172.17.0.10ED25519 key fingerprint is SHA256:X7w8YbxJrQ7u5jEPy/FobQyzg9u35ZnYZqlt3G7yaoA.root@172.17.0.10's password: 'Welcome to Alpine!✅️ 现在每次的主机密钥都不相同了 ✅️[root@Docker lb-web-ssh]# docker run -d --name cc web:1.2❌️ 这次是1.2版本 ❌️[root@Docker lb-web-ssh]# ssh root@172.17.0.11ED25519 key fingerprint is SHA256:BKCNN5J6r8iW2iu0gAs5eQzdW6wGBM7xtOcxQtNJves.This host key is known by the following other names/addresses: ~/.ssh/known_hosts:5: 172.17.0.5 ~/.ssh/known_hosts:8: 172.17.0.6 ~/.ssh/known_hosts:9: 172.17.0.7'他们的主机秘钥都是一样的!'v2.0优化
server { listen 80 default_server; server_name _; root /code;
location / { index index.html; }}- vim ./source/default.conf
#!/bin/bash
# start sshdssh-keygen -A/usr/sbin/sshdecho "root:${ROOT_WD}" | chpasswd# start nginxnginx -g "daemon off;"- vim ./source/start.sh
FROM alpine:3.20.2LABEL maintainer="Jiuzhao"ENV ROOT_WD=123456EXPOSE 80 22RUN apk update && \ apk add nginx curl openssh-server && \ rm -rf /var/cacheRUN mkdir /code && \ echo "This is my page" > /code/index.html && \ sed -ri 's@#(PermitRootLogin).*@\1 yes@' /etc/ssh/sshd_configCOPY ./default.conf /etc/nginx/http.d/COPY ./start.sh /code/HEALTHCHECK --interval=10s --timeout=10s --retries=3 \ CMD curl -f http://localhost/ || exit 1CMD ["/bin/sh","/code/start.sh"]- vim ./dockerfile/v1.0
"环境清理"[root@Docker lb-web-ssh]# docker rm -f $(docker ps -aq)[root@Docker lb-web-ssh]# docker rmi web:0.1 web:1.0 web:1.1 web:1.2 web:1.3[root@Docker lb-web-ssh]# rm -rf ./dockerfile/v0.1[root@Docker lb-web-ssh]# tree /lb-web-ssh//lb-web-ssh/├── dockerfile│ └── v1.0└── source ├── default.conf └── start.sh
1)构建镜像[root@Docker lb-web-ssh]# docker build -f ./dockerfile/v1.0 -t web:1.0 ./source/[root@Docker lb-web-ssh]# docker imagesweb:1.0 7eb7a083ccc2 24.3MB- Dockerfile中, 最后指令是CMD, 容易被docker run后的命令覆盖
[root@Docker lb-web-ssh]# docker run -d --name v1 web:1.0 passwd"后面传了一个参数 passwd "[root@Docker lb-web-ssh]# docker ps -aweb:1.0 "passwd" Exited (1) v1❌️ 直接异常退出了# 覆盖了CMD指令=================================如何把后面的参数作为密码传给环境变量呢❓️1.在Dockerfile中把CMD更换为ENTRYPOINT# CMD会被覆盖,ENTRYPOINT可以传参# 因为我们运行的是shell脚本,后面是可以跟参数的2.在shell脚本中,使用if-else判断if [ -n "$1" ];then # 如果传参数了($1非空),则直接用这个参数作为root密码 # docker run xxx 参数(密码) echo "root:${1}" | chpasswdelse [ -n "$ROOT_WD" ] # 没有参数($1为空)但ROOT_WD变量存在且非空 # 则用ROOT_WD变量作为root密码 # Dockerfile中 ROOT_WD=123456 (默认值) # docker run -e ROOT_WD=新密码 xxx echo "root:${ROOT_WD}" | chpasswdfi-n ✅️ 用于测试字符串长度是否大于0(非空) ✅️ 后面的变量用⚠️双引号⚠️包围 # 规避空格或其他特殊字符 # 不加引号 --> if判断有问题❌️=================================[root@Docker lb-web-ssh]# vim ./source/start.sh#!/bin/bash# start sshdssh-keygen -A/usr/sbin/sshdif [ -n "$1" ];then echo "root:${1}" | chpasswdelse [ -n "$ROOT_WD" ] echo "root:${ROOT_WD}" | chpasswdfi# start nginxnginx -g "daemon off;"
[root@Docker lb-web-ssh]# vim ./dockerfile/v2.0[root@Docker lb-web-ssh]# tail ./dockerfile/v2.0# 只是更改了一下,最后默认的启动方式 CMD --> ENTRYPOINTENTRYPOINT ["/bin/sh","/code/start.sh"][root@Docker lb-web-ssh]# docker build -f ./dockerfile/v2.0 -t web:2.0 ./source/[root@Docker lb-web-ssh]# docker imagesweb:2.0 aa06dc8c5f1a 24.3MB[root@Docker lb-web-ssh]# docker rm -f $(docker ps -aq)[root@Docker lb-web-ssh]# docker run -d --name v1 web:2.0[root@Docker lb-web-ssh]# docker psweb:2.0 "/bin/sh /code/start…" Up(healthy) 22/tcp,80/tcp v1[root@Docker lb-web-ssh]# docker exec v1 ps -efPID USER TIME COMMAND 1 root 0:00 /bin/sh /code/start.sh 9 root 0:00 sshd: /usr/sbin/sshd [listener]...xx12 root 0:00 nginx: master process nginx -g daemon off;13 nginx 0:00 nginx: worker process14 nginx 0:00 nginx: worker process'进程也都起来了!'[root@Docker lb-web-ssh]# docker inspect -f {{.NetworkSettings.Networks.bridge.IPAddress}} $(docker ps -n=1 -q)172.17.0.2
1)默认密码: 123456"docker run -d --name v1 web:2.0"[root@Docker ~]# ssh root@172.17.0.2root@172.17.0.2's password': --> 123456Welcome to Alpine!959400652de9:~# exit
2)手动 -e 指定密码[root@Docker ~]# docker run -d -e ROOT_WD=passwd --name v2 web:2.0[root@Docker ~]# ssh root@172.17.0.3root@172.17.0.3's password': --> passwdWelcome to Alpine!7a27760b2666:~# exit
3)run运行后面传参[root@Docker ~]# docker run -d --name v3 web:2.0 oldboyedu[root@Docker ~]# ssh root@172.17.0.4root@172.17.0.4's password': --> oldboyeduWelcome to Alpine!580cae44499f:~# exit❤️排错流程
[root@Docker lb-web-ssh]# docker ps -aweb:2.0 "/bin/sh /code/start…" Exited (2) v1❌️ 异常退出![root@Docker lb-web-ssh]# tail -2 ./dockerfile/v2.0# ENTRYPOINT ["/bin/sh","/code/start.sh"]ENTRYPOINT ["tail","-f","/etc/hosts"]✅️ 把上面的先注释掉,先保证容器运行起来
1)重新构建镜像[root@Docker lb-web-ssh]# docker build -f ./dockerfile/v2.0 -t web:2.0 ./source/
2)正常启动起来[root@Docker lb-web-ssh]# docker run -d --name v2 web:2.0[root@Docker lb-web-ssh]# docker psweb:2.0 "tail -f /etc/hosts" Up 1 second v1
3)进入到容器中测试[root@Docker lb-web-ssh]# docker exec -it v2 sh/ # ls /code/index.html start.sh/ # /bin/sh /code/start.sh/code/start.sh: line 8: syntax error:✅️ 这里面会显示详细的错误位置📍'在容器中调试脚本 ---> 更容易排错'LB负载调度
- 👆ssh服务完结 —> ==完善LB负载==
- 自由调度比例 —> shell脚本取参数
- sed awk 替换
- 自由调度比例 —> shell脚本取参数
具体要求如下:✅️ 支持nginx调度比例变量(传的参数不同,比例不同)# 默认3:1
[root@Docker ~]# docker run -d --name v4 -e weight=5:3 web:2.0[root@Docker ~]# docker exec v4 envPATH=/usr/sbin:/usr/bin:/sbin:/bin...weight=5:3ROOT_WD=123456HOME=/root# 这里面有我们传递的参数
1)如何取值 ✅️ cut && awk[root@Docker ~]# docker exec -it v4 sh/ # echo ${weight}5:3 # echo $weight | cut -d ":" -f 15/ # echo $weight | cut -d ":" -f 23'上面是cut命令,当然类似的功能awk也是可以实现的'/ # echo $weight | awk -F ":" '{print $1}'5/ # echo $weight | awk -F ":" '{print $2}'3
2)准备配置文件/ # vi lb.conf upstream webs { server web01 weight=3; server web02 weight=1;}server { listen 80; server_name diy.kpyun.com;
location / { proxy_pass http://webs; proxy_set_header Host $http_host; proxy_http_version 1.1; }}/ # Left=$(echo $weight | cut -d ":" -f 1)/ # Right=$(echo $weight | awk -F ":" '{print $2}')'先分别赋值给不同的变量'/ # echo $Left5/ # echo $Right3'再sed替换'/ # sed -ri "s#(weight=)3#\1${Left}#g" lb.conf/ # sed -ri "s#(weight=)1#\1${Right}#g" lb.conf/ # head -3 lb.conf upstream webs { server web01 weight=5; server web02 weight=3;
3)Dockerfile编写[root@Docker ~]# cd /lb-web-ssh/[root@Docker lb-web-ssh]# vim ./source/lb.conf'把上面的配置文件拷贝过去!'⚠️ 配置文件中默认权重为3:1 --> 已经写到配置文件中去了[root@Docker lb-web-ssh]# vim ./dockerfile/v3.0✅️ 这次拷贝lb.conf✅️ ENV声明多个变量 "空格隔开" --> weight=3:1 ENV key1=value1 key2=value2 key3=value3FROM alpine:3.20.2LABEL maintainer="Jiuzhao"ENV ROOT_WD=123456 weight=3:1EXPOSE 80 22RUN apk update && \ apk add nginx curl openssh-server && \ rm -rf /var/cacheRUN mkdir /code && \ echo "This is my page" > /code/index.html && \ sed -ri 's@#(PermitRootLogin).*@\1 yes@' /etc/ssh/sshd_configCOPY ./lb.conf /etc/nginx/http.d/COPY ./start.sh /code/HEALTHCHECK --interval=10s --timeout=10s --retries=3 \ CMD curl -f http://localhost/ || exit 1ENTRYPOINT ["/bin/sh","/code/start.sh"]4)脚本整合"docker run -e weight=5:3 xxx"# 启动容器的同时📌传递环境变量,可以替换配置文件中的权重[root@Docker lb-web-ssh]# vim ./source/start.sh#!/bin/bash# start sshdssh-keygen -A/usr/sbin/sshdif [ -n "$1" ];then echo "root:${1}" | chpasswdelse [ -n "$ROOT_WD" ] echo "root:${ROOT_WD}" | chpasswdfi# start nginxLeft=$(echo $weight | cut -d ":" -f 1)Right=$(echo $weight | awk -F ":" '{print $2}')sed -ri "s#(weight=)3#\1${Left}#g" /etc/nginx/http.d/lb.confsed -ri "s#(weight=)1#\1${Right}#g" /etc/nginx/http.d/lb.confnginx -g "daemon off;"
5)web服务器准备&&自定义页面[root@Docker lb-web-ssh]# docker rm -f $(docker ps -aq)'先把web01 和web02 创建过出来'👆用的v2.0版本的镜像'因为配置文件中 server web01; '✅️ 得用自定义网络[root@Docker lb-web-ssh]# docker network ls5297e0639824 mynet bridge local# 如果没有自行创建网络[root@Docker lb-web-ssh]# docker run -d --name web01 --net mynet web:2.0[root@Docker lb-web-ssh]# docker run -d --name web02 --net mynet web:2.0[root@Docker lb-web-ssh]# docker psweb:2.0 "/bin/sh /code/start…" Up 22/tcp, 80/tcp web02web:2.0 "/bin/sh /code/start…" Up 22/tcp, 80/tcp web01[root@Docker lb-web-ssh]# docker exec -it web01 sh/ # echo "web01..." > /code/index.html/ # curl localhostweb01.../ # exit[root@Docker lb-web-ssh]# docker exec -it web02 sh/ # echo "web02..." > /code/index.html/ # curl localhostweb02.../ # exit
6)生成镜像"这次是v3.0"[root@Docker lb-web-ssh]# docker build -f ./dockerfile/v3.0 -t web:3.0 ./source/[root@Docker lb-web-ssh]# docker imagesweb:3.0 ecf6b5894872 24.3MB[root@Docker lb-web-ssh]# docker run -d --name lb --net mynet web:3.0[root@Docker lb-web-ssh]# docker ps -n=1web:3.0 "/bin/sh /code/start…" Up 22/tcp, 80/tcp lb[root@Docker lb-web-ssh]# docker exec lb ps -efPID USER TIME COMMAND 1 root 0:00 /bin/sh /code/start.sh10 root 0:00 sshd: /usr/sbin/sshdxxx....21 root 0:00 nginx: master process nginx -g daemon off;22 nginx 0:00 nginx: worker process23 nginx 0:00 nginx: worker process'进程也都起来了'[root@Docker lb-web-ssh]# docker inspect -f {{.NetworkSettings.Networks.mynet.IPAddress}} $(docker ps -n=1 -q)'中间这个换为mynet而不是bridge'172.20.0.4[root@Docker lb-web-ssh]# tail -1 /etc/hosts172.20.0.4 diy.kpyun.com[root@Docker lb-web-ssh]# for i in `seq 10`;do curl diy.kpyun.com;doneweb01... ❤️web01... ❤️web01... ❤️web02... 💚web01... ❤️web01... ❤️web01... ❤️web02... 💚web01... ❤️web01... ❤️'也就是3:1的样子'
7) -e 指定环境变量[root@Docker lb-web-ssh]# docker run -d --name lb-test -e weight=5:1 --net mynet web:3.0⚠️ -e weight=5:1 ⚠️[root@Docker lb-web-ssh]# docker inspect -f {{.NetworkSettings.Networks.mynet.IPAddress}} $(docker ps -n=1 -q)172.20.0.5[root@Docker lb-web-ssh]# sed -i 's#172.20.0.4#172.20.0.5#g' /etc/hosts[root@Docker lb-web-ssh]# tail -1 /etc/hosts172.20.0.5 diy.kpyun.com[root@Docker lb-web-ssh]# for i in `seq 10`;do curl diy.kpyun.com;doneweb01... ❤️web01... ❤️web01... ❤️web01... ❤️web02... 💚web01... ❤️web01... ❤️web01... ❤️web01... ❤️web01... ❤️[root@Docker lb-web-ssh]# curl diy.kpyun.comweb02... 💚'每5次web01 --> 就该web02了'httpd服务编写
- Apache 服务
- 也是==web服务==的一种
- 没有Nginx处理==高并发==的能力
"清理环境"[root@Docker ~]# docker rm -f $(docker ps -aq)[root@Docker ~]# vim /tmp/httpd.imgFROM alpine:3.20.2LABEL maintainer="Shihao"EXPOSE 80WORKDIR /root/RUN apk update && \ apk add apache2 curl && \ rm -rf /var/cache && \ echo 'It works! Apache httpd' > /var/www/localhost/htdocs/index.htmlVOLUME /var/www/HEALTHCHECK --interval=10s --timeout=10s --retries=3 \ CMD curl -f http://localhost/ || exit 1ENTRYPOINT ["httpd","-D","FOREGROUND","-e","info"][root@Docker ~]# docker build -f /tmp/httpd.img -t myhttp:1.0 $(pwd)[+] Building 0.3s (7/7) FINISHED...........=> => unpacking to docker.io/library/myhttp:1.0[root@Docker ~]# docker image ls myhttpmyhttp:1.0 4424dd2544b9 25.7MB[root@Docker ~]# docker run -d --name v1 -P myhttp:1.0[root@Docker ~]# docker inspect -f {{.NetworkSettings.Networks.bridge.IPAddress}} $(docker ps -n=1 -q)172.17.0.2[root@Docker ~]# docker inspect v1 | grep -i -A 2 'Hostip' | head -2 "HostIp": "0.0.0.0", "HostPort": "32768"[root@Docker ~]# docker container ls -lfb20cb437808 myhttp:1.0 "httpd -D FOREGROUND…" 4 minutes ago Up 4 minutes (healthy) 0.0.0.0:32768->80/tcp, [::]:32768->80/tcp v1[root@Docker ~]# curl 172.17.0.2It works! Apache httpd[root@Docker ~]# curl localhost:32768It works! Apache httpd扩展指令
ARG变量
面试题: ARG和ENV的区别❓️💚相同点: 都可以向容器传递环境变量❤️不同点:1.ARG传递环境变量的生命周期⚠️"仅在构建镜像阶段有效" ✅️ 仅在Dockerfile中生效 ✅️ 在构建镜像时, 可被替换 `--build-arg key=value`2.ENV传递环境变量的生命周期在⚠️"镜像构建阶段和容器运行阶段均有效" ✅️ 容器运行后,也生效 --> env命令可以查看到 ✅️ 在运行容器时, 可以被替换 `-e key=value`
1)环境清理[root@Docker lb-web-ssh]# cd[root@Docker ~]# rm -rf /lb-web-ssh/[root@Docker ~]# docker rm -f $(docker ps -aq)[root@Docker ~]# docker volume prune -fa[root@Docker ~]# docker rmi web:1.0 web:2.0 web:3.0
2)Dockerfile文件编写# 这次不玩花活🎆了# 文件全部在/root目录下创建了[root@Docker ~]# pwd/root[root@Docker ~]# rm -rf ./*[root@Docker ~]# vim DockerfileFROM alpine:3.20.2LABEL maintainer="Jiuzhao"ARG school=kpyunENV user=ShiRUN mkdir /redhat && \ touch /redhat/$school && \ touch /redhat/$userCMD ["/bin/sh"]3)构建镜像[root@Docker ~]# docker build -f Dockerfile -t demo:1.0 .[root@Docker ~]# docker build -f Dockerfile -t demo:2.0 --build-arg school=heima .--build-arg ✅️ 替换Dockerfile中的ARG变量"kpyun" ---> "heima"[root@Docker ~]# docker images | grep demodemo:1.0 d614a574b7d1 11.7MB
1)保留ARG变量[root@Docker ~]# docker run -d -it --name v1 demo:1.0[root@Docker ~]# docker exec v1 envPATH=/usr/sbin:/usr/bin:/sbin:/binHOSTNAME=d32238bbaa48user=ShiHOME=/root'只有user=Shi,并没有ARG中的school=kpyun'[root@Docker ~]# docker exec v1 ls /redhatShikpyun --> 仅在构建镜像时 $school生效
2)替换ARG变量[root@Docker ~]# docker run -d -it --name v2 demo:2.0[root@Docker ~]# docker exec v2 envPATH=/usr/sbin:/usr/bin:/sbin:/bin...xxxuser=Shi'自然是没有ARG中的变量' --> 只在Dockerfile中生效[root@Docker ~]# docker exec v2 ls /redhatShiheima --> 把原有的 kpyun 替换掉了
3)替换ENV变量[root@Docker ~]# docker run -d -it --name v3 -e user=Jiu demo:2.0⚠️ -e user=Jiu[root@Docker ~]# docker exec v3 envPATH=/usr/sbin:/usr/bin:/sbin:/bin...xxxuser=Jiu[root@Docker ~]# docker exec v3 ls /redhatShi <--- 为什么这里没有被替换❓️heima✅️ 这两个文件是在构建镜像的时候就被创建好了的USER运行用户
==指定容器运行用户==或 UID
- 容器中必须有对应用户或UID
- 有的服务不能使用root启动
"清理环境"[root@Docker ~]# docker rm -f $(docker ps -aq)
1)正常启动✅️ 默认root用户运行[root@Docker ~]# docker run -d -it --name v1 alpine:3.20.2[root@Docker ~]# docker exec v1 whoamiroot[root@Docker ~]# docker exec v1 cat /etc/passwdroot❌0:0:root:/root:/bin/sh....xxxsync❌5:0🔄/sbin:/bin/syncnobody❌65534:65534:nobody:/:/sbin/nologin'要指定的用户必须在/etc/passwd中存在'
2)Dockerfile编写[root@Docker ~]# > Dockerfile[root@Docker ~]# vim DockerfileFROM alpine:3.20.2USER oldboyCMD ["/bin/sh"][root@Docker ~]# docker build -f Dockerfile -t demo:3.0 .'成功构建'# 运行试试[root@Docker ~]# docker run -d -it --name v2 demo:3.0docker: Error response xxx unable to find user oldboy:"不能找到oldboy用户,在passwd文件中"no matching entries in passwd file
3)已存在的用户[root@Docker ~]# sed -i 's#oldboy#nobody#g' Dockerfile'更换为nobody'[root@Docker ~]# docker build -f Dockerfile -t demo:3.0 .'覆盖构建'[root@Docker ~]# docker run -d -it --name v3 demo:3.0[root@Docker ~]# docker exec v3 whoami⚠️nobody⚠️ --> 而不是root[root@Docker ~]# docker exec v3 ps -efPID USER TIME COMMAND 1 nobody 0:00 /bin/sh14 nobody 0:00 ps -ef
4)创建&&并指定用户[root@Docker ~]# vim DockerfileFROM alpine:3.20.2RUN adduser -D oldboy && \ echo "oldboy:passwd" | chpasswdUSER oldboyCMD ["/bin/sh"]=====================================-D # 免交互 --> 无密码账户 --> 否则会卡住✅️ 从root切换至oldboy用户📌 echo xxx | chpasswd "可省略"
5)再次构建&&测试[root@Docker ~]# docker build -f Dockerfile -t demo:4.0 .[root@Docker ~]# docker run -d -it --name v4 demo:4.0[root@Docker ~]# docker exec v4 whoamioldboy[root@Docker ~]# docker exec v4 ps -ef | awk '{print $2}'USERoldboyoldboyONBUILD基础镜像触发器
🏗️ 什么是 ONBUILD❓️
-
ONBUILD是 Docker 提供的一种**“延时执行”**机制- 🎯 定义时不执行,只有被继承时才执行
-
你可以把它想象成一个”当别人继承我时,就做这些事”的指令
1)清理环境[root@Docker ~]# rm -rf ./*[root@Docker ~]# docker rm -f $(docker ps -aq)[root@Docker ~]# docker images[root@Docker ~]# docker rmi demo:1.0 demo:2.0 demo:3.0 demo:4.0
2)创建爷爷👴镜像[root@Docker ~]# vim yeyeFROM alpine:3.20.2LABEL maintainer=JiuONBUILD ENV school=kpyunONBUILD RUN touch /test.md && \ echo This From yeye >> /test.mdCMD ["/bin/sh"][root@Docker ~]# docker run -d -it --name v1 demo:1.0[root@Docker ~]# docker exec -it v1 sh/ # envPATH=/usr/sbin:/usr/bin:/sbin:/binHOSTNAME=b8d62ec454b6HOME=/root'没有school变量 && 也没有test.md文件'✅️ "定义时不执行,只有被继承时才执行"/ # ls / | grep test.md/ # exit[root@Docker ~]# docker image inspect demo:1.0 -f {{.Config.Labels}}map[maintainer:Jiu]✅️ 只有它的标签🏷️ && OnBuild字段[root@Docker ~]# docker image inspect demo:1.0 -f {{.Config.OnBuild}}[ENV school=kpyunRUN touch /test.md && echo This From yeye >> /test.md]
3)创建父镜像[root@Docker ~]# vim fuFROM demo:1.0RUN touch /fu.mdONBUILD ENV my_user=heimaCMD ["/bin/sh"][root@Docker ~]# docker build -f fu -t demo:2.0 .[root@Docker ~]# docker run -d -it --name v2 demo:2.0[root@Docker ~]# docker exec -it v2 sh/ # envPATH=/usr/sbin:/usr/bin:/sbin:/binHOSTNAME=909df5bfc1ebschool=kpyun'这些信息继承于yeye👴'/ # ls / | grep test.mdtest.md/ # cat /test.mdThis From yeye --> yeye👴/ # ls /fu.md/fu.md <-- 本身创建的
4)创建孙子镜像[root@Docker ~]# vim sunziFROM demo:2.0CMD ["/bin/sh"][root@Docker ~]# docker build -f sunzi -t demo:3.0 .[root@Docker ~]# docker run -d -it --name v3 demo:3.0[root@Docker ~]# docker exec -it v3 sh/ # envPATH=/usr/sbin:/usr/bin:/sbin:/binHOSTNAME=22e7483f41beschool=kpyun <-- ❗️ 继承于父镜像my_user=heima --> ✅️ ONBUILD 在发力=============================================[root@Docker ~]# docker image inspect demo:2.0 -f {{.Config.OnBuild}}[ENV my_user=heima]=============================================/ # ls / | grep test.mdtest.md <-- ❗️ 继承于父镜像/ # cat test.mdThis From yeye <-- ❗️ 继承于父镜像❗️"因为父镜像本身有这些东西,所以被继承过来了"👇 和它来源一样!/ # ls /fu.md/fu.md📌自定义基础镜像
scratch 是 Docker 官方提供的一个特殊镜像
-
它是一个==空的镜像==,什么都没有,也被称为”基础镜像”
-
当你写 FROM scratch 时,这意味着你从零开始构建镜像
- 👆==链接==

- 找根文件系统 —> 下载

1)上传并解压[root@Docker ~]# rm -rf ./*[root@Docker ~]# rz -Erz waiting to receive.[root@Docker ~]# lsrootfs.tar.xz[root@Docker ~]# mkdir test[root@Docker ~]# lsrootfs.tar.xz test[root@Docker ~]# tar xf rootfs.tar.xz -C ./test/[root@Docker ~]# ls ./test/bin dev etc home lib media mnt opt proc root run sbin srv sys tmp usr var
2)更改根目录信息[root@Docker ~]# pwd/root[root@Docker ~]# lsrootfs.tar.xz test[root@Docker ~]# cd ./test/[root@Docker test]# cat ./etc/os-release✅️ 我们可以在里面进行随意的更改!!NAME="Alpine Linux"ID=alpineVERSION_ID=3.23.4PRETTY_NAME="Jiuzhao996"HOME_URL="https://kpyun.fun/"[root@Docker test]# tar zcf Jiuzhao-rootfs.tar.xz *'将当前的所有文件都打包!'[root@Docker test]# ls Jiuzhao-rootfs.tar.xzJiuzhao-rootfs.tar.xz✅️ 这个就成我们自己的根文件系统了
3)返回上一级目录[root@Docker test]# cp Jiuzhao-rootfs.tar.xz ../[root@Docker test]# cd ../[root@Docker ~]# lsJiuzhao-rootfs.tar.xz rootfs.tar.xz test[root@Docker ~]# rm -rf rootfs.tar.xz test/[root@Docker ~]# lsJiuzhao-rootfs.tar.xz✅️ 只留下这个压缩包即可
4)编写自己的基础镜像[root@Docker ~]# vim scratch.dockerfileFROM scratchLABEL maintainer=JiuzhaoADD Jiuzhao-rootfs.tar.xz /CMD ["/bin/sh"][root@Docker ~]# docker build -f scratch.dockerfile -t mylinux:1.0 $(pwd)[root@Docker ~]# docker image ls mylinuxmylinux:1.0 7657409cad41 15.5MB
5)测试与验证[root@Docker ~]# docker history mylinux:1.0CREATED CREATED BY49 seconds ago CMD ["/bin/sh"]49 seconds ago ADD Jiuzhao-rootfs.tar.xz /49 seconds ago LABEL maintainer=Jiuzhao[root@Docker ~]# docker run -d -it --name m1 mylinux:1.0root@Docker ~]# docker psmylinux:1.0 "/bin/sh" Up 1 second m1[root@Docker ~]# docker exec m1 cat /etc/os-releaseNAME="Alpine Linux"ID=alpineVERSION_ID=3.23.4PRETTY_NAME="Jiuzhao996"HOME_URL="https://kpyun.fun/"[root@Docker ~]# docker exec m1 ps -efPID USER TIME COMMAND 1 root 0:00 /bin/sh14 root 0:00 ps -efDockerfile优化
.dockerignore文件
.dockerignore 文件用于指定在构建 Docker 镜像时需要忽略的文件和目录
- 它告诉 Docker 哪些文件不需要发送到==服务端(Docker daemon)==
- Docker 也是经典的 ==C/S架构==
✅️ 主要优势:提升构建速度 —> 减少需要传输的文件数量
多阶段构建
-
在一个
Dockerfile中定义多个FROM指令 -
每个
FROM开启一个新的构建阶段 -
分离构建环境和运行环境,只保留必要文件
-
COPY指令支持--from=<stage>这个参数来实现跨阶段复制- ADD无法实现 ❌️
- 👆去下载稳定版的源码包
1)准备nginx的源码包[root@Docker ~]# rm -rf ./*[root@Docker ~]# wget https://nginx.org/download/nginx-1.30.0.tar.gz'下载nginx源码包'# 注意版本号
2)编写Dockerfile文件[root@Docker ~]# vim multiple-step.dockerfile# --- 第一阶段:构建环境 ---FROM ubuntu:22.04 AS base# AS base 起的别名# 后面 --from=base 引用它
# 更新包列表并安装编译 Nginx 所需的依赖RUN apt-get update && apt-get install -y \ gcc \ make \ libpcre3-dev \ zlib1g-dev \ openssl \ libssl-dev \ wget && \ rm -rf /var/lib/apt/lists/* && \ rm -rf /var/cache
# 拷贝 Nginx 源码包ADD nginx-1.30.0.tar.gz /tmp/nginx-build
# 进入源码目录,配置、编译、安装WORKDIR /tmp/nginx-build/nginx-1.30.0RUN ./configure \ --prefix=/soft \ --sbin-path=/soft/sbin/nginx \ --conf-path=/soft/conf/nginx.conf \ --error-log-path=/soft/logs/error.log \ --http-log-path=/soft/logs/access.log \ --pid-path=/soft/run/nginx.pid \ --lock-path=/soft/run/nginx.lock \ --with-http_ssl_module \ --with-http_v2_module \ --with-http_realip_module \ --with-http_stub_status_module \ --with-http_gzip_static_module \ --with-pcre \ && make && make install
# --- 第二阶段:运行环境 ---FROM ubuntu:22.04
# 只安装 Nginx 运行时所需的依赖和库文件RUN apt-get update && apt-get install -y \ libpcre3 \ libssl3 && \ rm -rf /var/lib/apt/lists/* && \ rm -rf /var/cache
# 把第一阶段编译安装好的 Nginx 目录复制过来COPY --from=base /soft /soft# 两者目录结构必须保持一致,相同的位置# ADD,不能 --from 用于多阶段构建
# 定义启动命令CMD ["/bin/bash"]FROM ubuntu:22.04 AS base✅️ 第一阶段基础镜像起的别名👆
COPY --from=base /soft /soft--form # 引用第一阶段的镜像 别名# 两者目录结构必须保持一致,相同的位置# 不然后面会报错的,一直找不到文件
'刚开始我是 /code --> /usr/local/nginx/'# 编译的是/code,现在把它移到/usr/local/nginx/❌️ 尝试启动的时候,依旧会去编译时的目录去找文件root@04356adb932f:/# ls /usr/local/nginx/conf html logs run sbinroot@04356adb932f:/# /usr/local/nginx/sbin/nginxnginx: [alert] could not open error log file: open() "/code/logs/error.log" failed (2: No such file or directory)2026/04/30 01:37:20 [emerg] 18#0: open() "/code/conf/nginx.conf" failed (2: No such file or directory)✅️ 当前容器压根就没有这个目录 "/code"3)构建&&测试验证[root@Docker ~]# docker build -t demo:1.0 -f multiple-step.dockerfile .[root@Docker ~]# docker run -d -it --name myweb demo:1.0[root@Docker ~]# docker exec -it myweb /bin/bashroot@9dad99e49cbe:/# ls /soft/conf html logs run sbinroot@9dad99e49cbe:/# /soft/sbin/nginx'成功启动'root@9dad99e49cbe:/# cd /soft/root@9dad99e49cbe:/# echo Hello World > ./html/index.html'修改默认主页'[root@Docker ~]# curl 172.17.0.2Hello World# 在宿主机中成功拉去页面第一阶段 (AS base):
👆创建==构建环境==
- 以
ubuntu:22.04为基础镜像启动构建 - 安装了大量编译工具(如
gcc,make)和开发库(如libpcre3-dev),- 这些在最终运行时是不需要的
- 下载 Nginx 源码,进行配置、编译 (
make) 并安装 (make install)- 到
/soft目录
- 到
第二阶段:
👆创建==运行环境==
- 再次以
ubuntu:22.04为基础镜像启动新的构建 - 只安装 Nginx 运行时真正需要的库和依赖
- 关键一步:
COPY --from=base /soft /soft - 将第一阶段的
/soft目录- 包含了编译好的 Nginx 二进制文件和相关文件
- 完整地复制到了当前运行时镜像的相同路径下
- 清理不必要的构建依赖和缓存
最终生成的镜像只包含 ==运行Nginx== 所需的最小文件集合
不包含任何编译工具、源码或临时文件,大大减小了镜像体积
分离构建环境和运行环境,只保留必要文件
优化总结
1️⃣ ==构建速度优化==
- 将不常变化的指令放在 Dockerfile 上方,提高缓存命中率
- 使用国内或私有软件源,提升软件包下载速度
- 将大型软件包==提前下载==到本地,通过 COPY/ADD 直接复制
- 使用
.dockerignore排除不必要的文件,减少传输到 Docker daemon 的数据量
2️⃣ ==镜像体积优化==
- 清理缓存和垃圾:及时删除构建过程中的临时文件和无用软件包
- 选择轻量基础镜像:使用 Alpine 或其他精简版基础镜像
- 合并多条Dockefile指令:减少镜像层数,优化存储效率
- 采用多阶段构建:分离构建环境和运行环境,只保留必要文件
构建过程中避免使用==交互命令==,否则会导致构建意外终止
文章分享
如果这篇文章对你有帮助,欢迎分享给更多人!



