在这里我们来整理一下docker容器、dockerfile、docker镜像的关系:
dockerfile是面向开发的,发布项目做镜像的时候就要编写dockerfile文件。
dockerfile:构建文件,定义了一切的步骤,源代码。
dockerImanges:通过dockerfile构建生成的镜像,最终发布和运行的产品。
docker容器:容器就是镜像运行起来提供服务的
一、Dockerfile 指令选项。
Dockerfile 指令选项:
FROM #基础镜像 。 (centos)
MAINTAINER #镜像的作者和邮箱。(已被弃用,结尾介绍代替词)
RUN #镜像构建的时候需要执行的命令。
CMD #类似于 RUN 指令,用于运行程序(只有最后一个会生效,可被替代)
EXPOSE #对外开放的端口。
ENV #设置环境变量,定义了环境变量,那么在后续的指令中,就可以使用这个环境变量。
ADD # 步骤:tomcat镜像,这个tomcat压缩包。添加内容。
COPY #复制指令,将文件拷贝到镜像中。
VOLUME #设置卷,挂载的主机目录。
USER #用于指定执行后续命令的用户和用户组,
这边只是切换后续命令执行的用户(用户和用户组必须提前已经存在)。
WORKDIR #工作目录(类似CD命令)。
ENTRYPOINT #类似于 CMD 指令,但其不会被 docker run
的命令行参数指定的指令所覆盖,会追加命令。
ONBUILD #当构建一个被继承Dokcerfile,就会运行ONBUILD的指令。出发执行。
注意:CMD类似于 RUN 指令,用于运行程序,但二者运行的时间点不同:
CMD 在docker run 时运行。
RUN 是在 docker build。
作用:为启动的容器指定默认要运行的程序,程序运行结束,容器也就结束。
CMD 指令指定的程序可被 docker run 命令行参数中指定要运行的程序所覆盖。
如果 Dockerfile 中如果存在多个 CMD 指令,仅最后一个生效。
LABEL(MAINTALNER已经被弃用了,目前是使用LABEL代替)
LABEL 指令用来给镜像添加一些元数据(metadata),以键值对的形式,语法格式如下:
LABEL <key>=<value> <key>=<value> <key>=<value> ...
比如我们可以添加镜像的作者:
LABEL org.opencontainers.image.authors="runoob"
ENV 设置环境变量:
格式有两种:
ENV <key> <value>
ENV <key1>=<value1> <key2>=<value2>...
这个指令很简单,就是设置环境变量而已,无论是后面的其它指令,如 RUN,还是运行时的应用,都可以直接使用这里定义的环境变量。
ARG 构建参数:
格式:ARG <参数名>[=<默认值>]
构建参数和 ENV 的效果一样,都是设置环境变量。所不同的是,ARG 所设置的构建环境的环境变量,在将来容器运行时是不会存在这些环境变量的。但是不要因此就使用 ARG 保存密码之类的信息,因为 docker history 还是可以看到所有值的。
EXPOSE 声明端口:
格式为 EXPOSE <端口1> [<端口2>...]。
EXPOSE 指令是声明容器运行时提供服务的端口,这只是一个声明,在容器运行时并不会因为这个声明应用就会开启这个端口的服务。在 Dockerfile 中写入这样的声明有两个好处,一个是帮助镜像使用者理解这个镜像服务的守护端口,以方便配置映射;另一个用处则是在运行时使用随机端口映射时,也就是 docker run -P 时,会自动随机映射 EXPOSE 的端口。
要将 EXPOSE 和在运行时使用 -p <宿主端口>:<容器端口> 区分开来。-p,是映射宿主端口和容器端口,换句话说,就是将容器的对应端口服务公开给外界访问,而 EXPOSE 仅仅是声明容器打算使用什么端口而已,并不会自动在宿主进行端口映射。
WORKDIR 指定工作目录:
格式为 WORKDIR <工作目录路径>。
使用 WORKDIR 指令可以来指定工作目录(或者称为当前目录),以后各层的当前目录就被改为指定的目录,如该目录不存在,WORKDIR 会帮你建立目录
[root@docker1 ~]# mkdir /test-multi
[root@docker1 ~]# cd /test-multi
[root@docker1 test-multi]# vim Dockerfile
[root@docker1 test-multi]# cat Dockerfile
FROM centos
ENV aa=1 bb=2 cc=3 envdir=/a/b/c
ARG dd=4 ee=5 ff=6 argdir=/a/b/c
WORKDIR $argdir
COPY test.sh $envdir
RUN echo $aa $bb $cc $dd $dd $ff > /var.txt
CMD ["./test.sh"]
[root@docker1 test-multi]#
[root@docker1 test-multi]# vim test.sh
[root@docker1 test-multi]# cat test.sh
#! /bin/bash
echo output env vars:
echo $aa $bb $cc
echo output arg vars:
echo $dd $ee $ff
echo current work directory:
pwd
[root@docker1 test-multi]#
[root@docker1 test-multi]# chmod +x test.sh
[root@docker1 test-multi]# docker build -t mymulti:v1 .
……
=> [2/4] WORKDIR /a/b/c 0.1s
=> [3/4] COPY test.sh /a/b/c 0.0s
=> [4/4] RUN echo 1 2 3 4 4 6 > /var.txt
……
验证:
[root@docker1 test-multi]# docker run --rm mymulti:v1
output env vars:
1 2 3
output arg vars:
current work directory:
/a/b/c
[root@docker1 test-multi]#
VOLUME 定义匿名卷:
格式为:
VOLUME ["<路径1>", "<路径2>"...]
VOLUME <路径>
之前我们说过,容器运行时应该尽量保持容器存储层不发生写操作,对于数据库类需要保存动态数据的应用,其数据库文件应该保存于卷(volume)中,后面的章节我们会进一步介绍 Docker 卷的概念。为了防止运行时用户忘记将动态文件所保存目录挂载为卷,在 Dockerfile 中,我们可以事先指定某些目录挂载为匿名卷,这样在运行时如果用户不指定挂载,其应用也可以正常运行,不会向容器存储层写入大量数据。
[root@docker1 ~]# mkdir /test_volume
[root@docker1 ~]# cd /test_volume
[root@docker1 test_volume]#
[root@docker1 test_volume]# cat Dockerfile
from alpine
RUN mkdir /test && touch /test/test.txt
[root@docker1 test_volume]# docker build -t myvolume:v1 .
[root@docker1 test_volume]# docker run -it myvolume:v1 sh
/ # cat /test/test.txt
/ # echo 123 > /test/test.txt
/ # cat /test/test.txt
123
/ # exit
[root@docker1 test_volume]# docker ps -a
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
c2de419ef2d3 myvolume:v1 "sh" 36 seconds ago Exited (0) 3 seconds ago zealous_lumiere
[root@docker1 test_volume]#
[root@docker1 test_volume]# docker start zealous_lumiere
zealous_lumiere
[root@docker1 test_volume]# docker ps -a
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
c2de419ef2d3 myvolume:v1 "sh" About a minute ago Up 7 seconds zealous_lumiere
[root@docker1 test_volume]# docker exec -it zealous_lumiere sh
/ # cat /test/test.txt
123
/ # exit
[root@docker1 test_volume]# docker stop zealous_lumiere
zealous_lumiere
[root@docker1 test_volume]# docker rm zealous_lumiere
zealous_lumiere
[root@docker1 test_volume]#注:容器没删除,则数据存在
重新创建的容器没有之前的数据,数据是非持久化的:
[root@docker1 test_volume]# docker run -it --name mv1 myvolume:v1 sh
/ # cat /test/test.txt
/ # exit
[root@docker1 test_volume]# docker rm mv1
mv1
[root@docker1 test_volume]#
匿名卷:
[root@docker1 test_volume]# cat Dockerfile
from alpine
RUN mkdir /test && touch /test/test.txt
VOLUME /test
[root@docker1 test_volume]#
[root@docker1 test_volume]# docker build -t myvolume:v2 .
[root@docker1 test_volume]# docker run --rm -d myvolume:v2 sleep 100
2b2922070b503298b4195c7fa7007a6b0f3f51528f035098beb4d32801288c11
[root@docker1 test_volume]#
[root@docker1 test_volume]# docker run --rm -d myvolume:v2 sleep 100
2b2922070b503298b4195c7fa7007a6b0f3f51528f035098beb4d32801288c11
[root@docker1 test_volume]# docker ps -a
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
2b2922070b50 myvolume:v2 "sleep 100" 13 seconds ago Up 13 seconds eloquent_agnesi
[root@docker1 test_volume]# docker inspect eloquent_agnesi | grep -i -A 5 mounts
"Mounts": [
{
"Type": "volume",
"Name": "581af512e8399049c748fb8f2918df6041435aba355b83618537accbada60f72",
"Source": "/var/lib/docker/volumes/581af512e8399049c748fb8f2918df6041435aba355b83618537accbada60f72/_data",
"Destination": "/test",
[root@docker1 test_volume]# docker volume ls
DRIVER VOLUME NAME
local 581af512e8399049c748fb8f2918df6041435aba355b83618537accbada60f72
[root@docker1 test_volume]#
[root@docker1 test_volume]# docker ps -a(容器停止后自动删除了)
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
[root@docker1 test_volume]# docker volume ls 匿名卷生命周期跟随容器
DRIVER VOLUME NAME
[root@docker1 test_volume]#
不在dockerfile定义volume也可以启用匿名卷:
[root@docker1 test_volume]# docker run -d -v /opt alpine sleep 200
37d9216c975dac39d75845bfa14db0b983d8eb710d1072ce75d16b5d609d54db
[root@docker1 test_volume]# docker volume ls
DRIVER VOLUME NAME
local 847f7e6cc35beca7f50a9a6b41c28a84b7d60abfabd3e7677636f84f5d8694a4
[root@docker1 test_volume]#
[root@docker1 test_volume]# docker rmi myvolume:v2
Untagged: myvolume:v2
Deleted: sha256:6ccc8e360eed876906d7bf5fdd8109a995e7afe11b8595f00ec594db81760664
[root@docker1 test_volume]#
持久存储:推荐使用命名卷或者宿主机目录(或共享的目录)
数据卷管理:数据持久化
这一章介绍如何在 Docker 内部以及容器之间管理数据,在容器中管理数据主要有两种方式:
数据卷(Volumes)
挂载主机目录 (Bind mounts)
[root@docker1 test_volume]# docker volume ls
DRIVER VOLUME NAME
[root@docker1 test_volume]#
[root@docker1 test_volume]# docker volume create my-vol1
my-vol1
[root@docker1 test_volume]# docker volume ls
DRIVER VOLUME NAME
local my-vol1
[root@docker1 test_volume]#
[root@docker1 test_volume]# docker inspect my-vol1
[
{
"CreatedAt": "2024-05-21T09:32:49+08:00",
"Driver": "local",
"Labels": null,
"Mountpoint": "/var/lib/docker/volumes/my-vol1/_data",
"Name": "my-vol1",
"Options": null,
"Scope": "local"
}
]
[root@docker1 test_volume]#
[root@docker1 test_volume]# cat Dockerfile
from alpine
RUN mkdir /test && touch /test/test.txt
VOLUME /test
[root@docker1 test_volume]#
[root@docker1 test_volume]# docker build -t myvolume:v2 .
[root@docker1 test_volume]# docker inspect myvolume:v2| grep -i -A 2 volumes
"Volumes": {
"/test": {}
},
[root@docker1 test_volume]#
[root@docker1 test_volume]# docker run -it --rm -v my-vol1:/test myvolume:v2 sh
/ # cat /test/test.txt
/ # echo 123 > /test/test.txt
/ # echo 456 > /test/test2.txt
/ # exit
[root@docker1 test_volume]# docker ps -a
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
[root@docker1 test_volume]# docker run -it --rm -v my-vol1:/test myvolume:v2 sh
/ # cat /test/test.txt
123
/ # cat /test/test2.txt
456
/ # exit
[root@docker1 test_volume]#
[root@docker1 ~]# ls /var/lib/docker/volumes/my-vol1/_data
test2.txt test.txt
[root@docker1 ~]#
[root@docker1 test_volume]# docker run -it --rm --mount source=my-vol1,target=/test myvolume:v2 sh
/ # cat /test/test2.txt
456
/ # exit
[root@docker1 test_volume]#
Linux目录挂载:加bind选项
[root@docker1 ~]# mkdir /a /b
[root@docker1 ~]# touch /b/b.txt
[root@docker1 ~]# mount /a /b
mount: /b: /a is not a block device.
[root@docker1 ~]# mount -o bind /a /b
[root@docker1 ~]# ls /b
[root@docker1 ~]# umount /b
[root@docker1 ~]# ls /b
b.txt
[root@docker1 ~]#
[root@docker1 test_volume]# mkdir /testmount
[root@docker1 test_volume]# echo 3 > /testmount/3.txt
[root@docker1 test_volume]# docker run -it --rm --mount type=bind,source=/testmount,target=/test myvolume:v2 sh
/ # ls /test
3.txt
/ # echo 33 > /test/3.txt
/ # exit
[root@docker1 test_volume]# docker run -it --rm --mount type=bind,source=/testmount,target=/test,readonly myvolume:v2 sh
/ # cat /test/3.txt
33
/ # echo 333 > /test/3.txt
sh: can't create /test/3.txt: Read-only file system
/ # exit
[root@docker1 test_volume]#
容器共享存储:
[root@docker1 ~]# ls /webdir/
Dockerfile page1.html page2.html
[root@docker1 ~]# cat /webdir/page1.html
page1
[root@docker1 ~]# cat /webdir/page2.html
page2
[root@docker1 ~]#
[root@docker1 test_volume]# docker run -d --rm --name nginx1 -v /webdir:/usr/share/nginx/html -p 81:80 nginx:latest
6b9920b4b7d3f666c486a1d372b45fa01b63cd64252fbdc629053aff0b953ce8
[root@docker1 test_volume]# docker run -d --rm --name nginx2 -v /webdir:/usr/share/nginx/html -p 82:80 nginx:latest
9666d000921ee7d78edaf8fc0f91ab4127b68ca9195c70601fa3eaf628c79616
[root@docker1 test_volume]# curl 127.0.0.1:81/page1.html
page1
[root@docker1 test_volume]# curl 127.0.0.1:82/page1.html
page1
[root@docker1 test_volume]# docker stop nginx1
nginx1
[root@docker1 test_volume]# docker stop nginx2
nginx2
[root@docker1 test_volume]#
使用local以外的volume driver:
使用vieux/sshfs:
安装插件:
[root@docker1 test_volume]# docker plugin install --grant-all-permissions vieux/sshfs
latest: Pulling from vieux/sshfs
Digest: sha256:1d3c3e42c12138da5ef7873b97f7f32cf99fb6edde75fa4f0bcf9ed277855811
52d435ada6a4: Complete
Installed plugin vieux/sshfs
[root@docker1 test_volume]#
[root@docker2 ~]# mkdir /sharedir
[root@docker2 ~]# touch /sharedir/share.txt
创建卷:
[root@docker1 test_volume]# docker volume create --driver vieux/sshfs -o sshcmd=root@192.168.99.129:/sharedir -o password=1 sshvolume
sshvolume
[root@docker1 test_volume]#
[root@docker1 test_volume]# docker volume ls
DRIVER VOLUME NAME
local my-vol1
vieux/sshfs:latest sshvolume
[root@docker1 test_volume]#
[root@docker1 test_volume]# docker run -it --rm -v sshvolume:/opt alpine sh
/ # ls /opt/
share.txt
/ # exit
[root@docker1 test_volume]#
(
官方参考如下:
docker run -d \
--name sshfs-container \
--volume-driver vieux/sshfs \
--mount src=sshvolume,target=/app,volume-opt=sshcmd=test@node2:/home/test,volume-opt=password=testpassword \
nginx:latest
)
使用基于nfs的卷:
(
官方参考如下:
Nfsv3:
docker service create -d \
--name nfs-service \
--mount 'type=volume,source=nfsvolume,target=/app,volume-driver=local,volume-opt=type=nfs,volume-opt=device=:/var/docker-nfs,volume-opt=o=addr=10.0.0.10' \
nginx:latest
nfsv4:
docker service create -d \
--name nfs-service \
--mount 'type=volume,source=nfsvolume,target=/app,volume-driver=local,volume-opt=type=nfs,volume-opt=device=:/var/docker-nfs,"volume-opt=o=addr=10.0.0.10,rw,nfsvers=4,async"' \
nginx:latest
)
配置nfs服务,自行解决防火墙和selinux:
[root@docker2 ~]# mkdir /nfs1 /nfs2
[root@docker2 ~]# chmod 777 /nfs1 /nfs2
[root@docker2 ~]# vim /etc/exports
[root@docker2 ~]# cat /etc/exports
/nfs1 *(rw,no_root_squash)
/nfs2 *(rw,no_root_squash)
[root@docker2 ~]#
[root@docker2 ~]# systemctl start nfs-server.service
[root@docker1 test_volume]# yum -y install nfs-utils
[root@docker1 test_volume]# showmount -e 192.168.99.129
Export list for 192.168.99.129:
/nfs2 *
/nfs1 *
[root@docker1 test_volume]#
手动创建卷:
[root@docker1 test_volume]# docker volume create --driver local -o type=nfs -o device=:/nfs1 -o o=addr=192.168.99.129,rw,nfsvers=4 nfs1volume
nfs1volume
[root@docker1 test_volume]#
[root@docker1 test_volume]# docker volume ls
DRIVER VOLUME NAME
local my-vol1
local nfs1volume
vieux/sshfs:latest sshvolume
[root@docker1 test_volume]#
验证:
[root@docker1 test_volume]# docker run -it --rm --mount 'type=volume,source=nfs1volume,target=/opt,volume-driver=local,volume-opt=type=nfs,volume-opt=device=:/nfs1,"volume-opt=o=addr=192.168.99.129,rw,nfsvers=4"' --privileged alpine sh
/ # touch /opt/1.txt
/ # exit
[root@docker1 test_volume]#
[root@docker2 ~]# ls /nfs1
1.txt
[root@docker2 ~]#
[root@docker1 test_volume]# docker run -it --rm --mount 'type=volume,source=nfs1volume,target=/opt' --privileged alpine sh
/ # ls /opt/
1.txt
/ # exit
[root@docker1 test_volume]#
创建容器时自动创建nfs卷:
[root@docker1 ~]# docker volume ls
DRIVER VOLUME NAME
local my-vol1
local nfs1volume
vieux/sshfs:latest sshvolume
[root@docker1 ~]#
[root@docker1 test_volume]# docker run -it --rm --mount 'type=volume,source=nfs2volume,target=/opt,volume-driver=local,volume-opt=type=nfs,volume-opt=device=:/nfs2,"volume-opt=o=addr=192.168.99.129,rw,nfsvers=4"' --privileged alpine sh
/ # touch /opt/2.txt
/ # exit
[root@docker1 test_volume]#
[root@docker1 test_volume]# docker volume ls
DRIVER VOLUME NAME
local my-vol1
local nfs1volume
local nfs2volume
vieux/sshfs:latest sshvolume
[root@docker1 test_volume]#
[root@docker2 ~]# ls /nfs2
2.txt
[root@docker2 ~]#
练习:使用samba共享提供持久存储
官方参考:
docker volume create \
--driver local \
--opt type=cifs \
--opt device=//uxxxxx.your-server.de/backup \
--opt o=addr=uxxxxx.your-server.de,username=uxxxxxxx,password=*****,file_mode=0777,dir_mode=0777 \
--name cif-volume
其他数据卷内容参考:Volumes | Docker DocsLearn how to create, manage, and use volumes instead of bind mounts for persisting data generated and used by Docker.https://docs.docker.com/storage/volumes/
USER 指定当前用户:
格式:USER <用户名>[:<用户组>]
USER 指令和 WORKDIR 相似,都是改变环境状态并影响以后的层。WORKDIR 是改变工作目录,USER 则是改变之后层的执行 RUN, CMD 以及 ENTRYPOINT 这类命令的身份。
USER 只是帮助你切换到指定用户而已,这个用户必须是事先建立好的,否则无法切换。
[root@docker1 test_volume]# vim Dockerfile
[root@docker1 test_volume]# cat Dockerfile
from centos
RUN id > /user.txt && chmod 777 /user.txt
RUN useradd test
USER test
RUN id >> /user.txt
CMD cat /user.txt
[root@docker1 test_volume]#
[root@docker1 test_volume]# docker build -t test-user:v1 .
[root@docker1 test_volume]# docker run -it --rm test-user:v1
uid=0(root) gid=0(root) groups=0(root)
uid=1000(test) gid=1000(test) groups=1000(test)
[root@docker1 test_volume]#
HEALTHCHECK 健康检查:
格式:
HEALTHCHECK [选项] CMD <命令>:设置检查容器健康状况的命令
HEALTHCHECK NONE:如果基础镜像有健康检查指令,使用这行可以屏蔽掉其健康检查指令
HEALTHCHECK 指令是告诉 Docker 应该如何进行判断容器的状态是否正常,这是 Docker 1.12 引入的新指令。
[root@docker1 ~]# mkdir /test-health
[root@docker1 ~]# cd /test-health
[root@docker1 test-health]# vim Dockerfile
[root@docker1 test-health]# cat Dockerfile
FROM nginx
RUN apt-get update && apt-get install -y curl && rm -rf /var/lib/apt/lists/*
HEALTHCHECK --interval=5s --timeout=3s CMD curl -fs http://localhost/ || exit 1
[root@docker1 test-health]# docker build -t myhealth:v1 .
另起一个终端,每秒刷新一次docker ps -a的输出:
[root@docker1 ~]# watch -n 1 docker ps -a
[root@docker1 test-health]# docker run -d --rm --name myh myhealth:v1
a1fffb3d0193408d81a1db9484576a3e2df214b12d9fa19ae78da6ff875d7704
[root@docker1 test-health]#
[root@docker1 ~]# docker ps -a
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
a1fffb3d0193 myhealth:v1 "/docker-entrypoint.…" 28 seconds ago Up 27 seconds (healthy) 80/tcp myh
[root@docker1 ~]# 速度快,可以看到从starting到healthy的过程
为了帮助排障,健康检查命令的输出(包括 stdout 以及 stderr)都会被存储于健康状态里,可以用 docker inspect 来查看。
[root@docker1 test-health]# docker inspect --format '{{json .State.Health}}' myh | python3 -m json.tool
模拟故障:
再容器中删除index.html,因为健康检查指令curl -fs http://localhost/ || exit 1查看的时首页,此时删除首页则访问报错,但是不能使用nginx -s stop停服务,因为nginx进程是容器的主体进程,停服务则容器退出:
[root@docker1 test-health]# docker exec -it myh sh
# ls /usr/share/nginx/html/index.html
/usr/share/nginx/html/index.html
# rm -rf /usr/share/nginx/html/index.html
# exit
[root@docker1 test-health]#
等待一段时间可以看到unhealthy:
[root@docker1 ~]# docker ps -a
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
cf26f8e926d2 myhealth:v1 "/docker-entrypoint.…" 2 minutes ago Up 2 minutes (unhealthy) 80/tcp myh
[root@docker1 ~]#
自行创建index.html,测试恢复为healthy的过程
[root@docker1 test-health]# vim Dockerfile
[root@docker1 test-health]# cat Dockerfile
FROM alpine
LABEL custom alpine
[root@docker1 test-health]# docker build -t mylabel:v1 .
[root@docker1 test-health]# docker image ls -f LABEL=custom
REPOSITORY TAG IMAGE ID CREATED SIZE
mylabel v1 bcefe279deed 3 months ago 7.38MB
[root@docker1 test-health]#