在这里我们来整理一下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]#
练习
从centos基础镜像创建一个容器,安装nginx软件包,配置健康检查,如果/usr/share/nginx/html/test.html存在,则检查正常,不存在则unhealthy,验证效果
[root@docker1 ~]# mkdir /test-h2
[root@docker1 ~]# cd /test-h2
[root@docker1 test-h2]# vim Dockerfile
[root@docker1 test-h2]# docker build -t myh2 .
[root@docker1 test-h2]# cat Dockerfile
FROM centos
RUN rm -rf /etc/yum.repos.d/* \
&& curl -o /etc/yum.repos.d/CentOS-Base.repo https://mirrors.aliyun.com/repo/Centos-vault-8.5.2111.repo \
&& yum -y install nginx \
&& yum clean all \
&& echo test > /usr/share/nginx/html/test.html
EXPOSE 80
HEALTHCHECK --interval=5s --timeout=3s \
CMD [ -f "/usr/share/nginx/html/test.html" ] || exit 1
[root@docker1 test-h2]#
注: 此时运行容器,没有启动nginx服务,无法使用curl判定,所以使用[ -f “xxxx” ]
练习2
使用samba共享提供卷
[root@docker2 ~]# mkdir /sambashare
[root@docker2 ~]# yum -y install samba samba-client
[root@docker2 ~]# vim /etc/samba/smb.conf
[root@docker2 ~]# tail -5 /etc/samba/smb.conf
[smbshare]
path = /sambashare/
read only = No
hosts allow = 192.168.99.
valid users = testuser
[root@docker2 ~]#
[root@docker2 ~]# chmod 777 /sambashare/
[root@docker2 ~]# useradd -s /sbin/nologin testuser
[root@docker2 ~]# pdbedit -a testuser
new password: (redhat)
retype new password:(redhat)
……
[root@docker2 ~]# pdbedit -L
testuser:1001:
[root@docker2 ~]#
[root@docker2 ~]# systemctl restart smb.service nmb.service
[root@docker1 test-h2]# yum -y install samba-client
使用testuser用户和redhat密码查看192.168.99.129服务器的共享:
[root@docker1 ~]# smbclient -L 192.168.99.129 -U testuser%redhat
Sharename Type Comment
--------- ---- -------
print$ Disk Printer Drivers
smbshare Disk
IPC$ IPC IPC Service (Samba 4.19.4)
testuser Disk Home Directories
SMB1 disabled -- no workgroup available
[root@docker1 ~]#
[root@docker1 ~]# smbclient -L 192.168.99.129 (直接回车,使用匿名用户查看)
Password for [SAMBA\root]:
Anonymous login successful
Sharename Type Comment
--------- ---- -------
print$ Disk Printer Drivers
smbshare Disk
IPC$ IPC IPC Service (Samba 4.19.4)
SMB1 disabled -- no workgroup available
[root@docker1 ~]#
[root@docker1 ~]# yum -y install cifs-utils
[root@docker1 ~]# mount -o username=testuser,password=redhat //192.168.99.129/smbshare /opt
[root@docker1 ~]# df -h /opt
Filesystem Size Used Avail Use% Mounted on
//192.168.99.129/smbshare 70G 13G 58G 19% /opt
[root@docker1 ~]#
[root@docker1 ~]# umount /opt
创建samba共享的卷:
[root@docker1 ~]# docker volume create \
> --driver local \
> --opt type=cifs \
> --opt device=//192.168.99.129/smbshare \
> --opt o=addr=192.168.99.129,username=testuser,password=redhat,file_mode=0777,dir_mode=0777 \
> --name cifs-volume
cifs-volume
[root@docker1 ~]#
[root@docker1 ~]# docker run -it --rm -v cifs-volume:/opt alpine sh
/ # touch /opt/cifs.txt
/ # exit
[root@docker1 ~]#
[root@docker1 ~]# docker volume ls
DRIVER VOLUME NAME
local cifs-volume
local my-vol1
[root@docker1 ~]#
[root@docker2 ~]# ls /sambashare/
cifs.txt
[root@docker2 ~]#
[root@docker1 ~]# docker volume rm cifs-volume
cifs-volume
[root@docker1 ~]#
[root@docker2 ~]# systemctl stop smb nmb
SHELL 指令:
[root@docker1 test-shell]# cat Dockerfile
from centos
RUN rm -rf /etc/yum.repos.d/* \
&& curl -o /etc/yum.repos.d/CentOS-Base.repo https://mirrors.aliyun.com/repo/Centos-vault-8.5.2111.repo \
&& yum -y install python3 \
&& yum clean all \
&& touch /result.txt
COPY * /
SHELL ["/usr/bin/python3"]
RUN /python.py
SHELL ["/bin/bash", "-c"]
RUN /shell.sh
CMD cat /result.txt
[root@docker1 test-shell]#
[root@docker1 test-shell]# cat shell.sh
echo hello shell >> /result.txt
[root@docker1 test-shell]# cat python.py
import os
with open('/result.txt', 'w') as f:
f.write("hello python\n")
[root@docker1 test-shell]#
[root@docker1 test-shell]# docker build -t myshell:v1 .
[root@docker1 test-shell]# docker run --rm myshell:v1
hello python
hello shell
[root@docker1 test-shell]#
ONBUILD 为他人做嫁衣裳:
ONBUILD 是一个特殊的指令,它后面跟的是其它指令,比如 RUN, COPY 等,而这些指令,在当前镜像构建时并不会被执行。只有当以当前镜像为基础镜像,去构建下一级镜像的时候才会被执行。
Dockerfile 中的其它指令都是为了定制当前镜像而准备的,唯有 ONBUILD 是为了帮助别人定制自己而准备的。
[root@docker1 ~]# mkdir /test-onbuild
[root@docker1 ~]# cd /test-onbuild
[root@docker1 test-onbuild]# cat start.sh
#! /bin/bash
shell_var=$(ls /*.sh)
$shell_var
[root@docker1 test-onbuild]# chmod +x start.sh
[root@docker1 test-onbuild]# cat Dockerfile
from centos
RUN mkdir /app
WORKDIR /app
ONBUILD COPY *.txt /
ONBUILD COPY *.sh /
COPY start.sh /app
CMD ["./start.sh"]
[root@docker1 test-onbuild]#
[root@docker1 test-onbuild]# docker build -t onbuild_base .
注:此时ONBUILD指令没有执行
[root@docker1 ob1]# pwd
/ob1
[root@docker1 ob1]#
[root@docker1 ob1]# cat shell.sh
#! /bin/bash
cat /1.txt > /12.txt
cat /2.txt >> /12.txt
cat /12.txt
[root@docker1 ob1]#
[root@docker1 ob1]# chmod +x shell.sh
[root@docker1 ob1]# cat 1.txt
1
[root@docker1 ob1]# cat 2.txt
2
[root@docker1 ob1]# cat Dockerfile
FROM onbuild_base:latest
[root@docker1 ob1]#
[root@docker1 ob1]# docker build -t ob_sub:v1 .
[root@docker1 ob1]# docker run --rm ob_sub:v1
1
2
[root@docker1 ob1]#
[root@docker1 ob1]# mkdir /ob2
[root@docker1 ob1]# cd /ob2
[root@docker1 ob2]# vim jiaoben.sh
[root@docker1 ob2]# cat jiaoben.sh
#! /bin/bash
cat /3.txt > /34.txt
cat /4.txt >> /34.txt
cat /34.txt
[root@docker1 ob2]#
[root@docker1 ob2]# chmod +x jiaoben.sh
[root@docker1 ob2]# echo 3 > 3.txt
[root@docker1 ob2]# echo 4 > 4.txt
[root@docker1 ob2]#
[root@docker1 ob2]# vim Dockerfile
[root@docker1 ob2]# cat Dockerfile
from onbuild_base:latest
[root@docker1 ob2]#
[root@docker1 ob2]# docker build -t ob_sub:v2 .
[root@docker1 ob2]# docker run --rm ob_sub:v2
3
4
[root@docker1 ob2]#
参考文档:
-
- Dockerfie 官方文档:https://docs.docker.com/engine/reference/builder/
- Dockerfile 最佳实践文档:
https://docs.docker.com/develop/develop-images/dockerfile_best-practices/
-
- Docker 官方镜像 Dockerfile:https://github.com/docker-library/docshttps://github.com/docker-library/docs
练习:
- 在docker1安装httpd服务,共享页面index.html(内容为common-index)
- 制作基础镜像osbase:
以centos为基础,配置yum源,安装wget,拷贝脚本web.sh至/app
脚本内容:
判断安装的web服务是什么:
如果是httpd,则执行httpd -D FOREGROUND
如果是nginx,则执行nginx -g “daemon off;”
在dockerfile中使用onbuild关键字,将docker1共享的index.html下载到/目录
- 在osbase的基础上构建镜像,安装httpd或nginx,将/index.html拷贝至网站根目录
镜像分别为os_httpd和os_nginx
在每个镜像中,有对应的ONBUILD指令,将页面文件拷贝至对应服务的网站根目录中
- 在os_httpd和os_nginx的基础上,构建镜像,拷贝页面至指定目录,测试访问
多阶段构建:
使用单个dockerfile可能遇到的问题:
将所有的构建过程编包含在一个 Dockerfile 中,包括项目及其依赖库的编译、测试、打包等流程,这里可能会带来的一些问题:
镜像层次多,镜像体积较大,部署时间变长
源代码存在泄露的风险
使用一个dockerfile源码编译出hello.c的二进制文件并运行:
[root@docker1 ~]# mkdir /multi-hello
[root@docker1 ~]# cd /multi-hello
[root@docker1 multi-hello]#
[root@docker1 multi-hello]# vim hello.c
[root@docker1 multi-hello]# cat hello.c
#include<stdio.h>
int main(void)
{
printf ("Hello world!\n");
return 0;
}
[root@docker1 multi-hello]#
[root@docker1 multi-hello]# cat Dockerfile
FROM centos
RUN rm -rf /etc/yum.repos.d/* \
&& curl -o /etc/yum.repos.d/CentOS-Base.repo https://mirrors.aliyun.com/repo/Centos-vault-8.5.2111.repo \
&& yum -y install gcc gcc-c++ make \
&& yum clean all \
&& mkdir /app
COPY hello.c /
RUN gcc -o /hello /hello.c && mv /hello /app
CMD ["/app/hello"]
[root@docker1 multi-hello]#
[root@docker1 multi-hello]# docker build -t multi-hello:v1 .
[root@docker1 multi-hello]# docker run --rm multi-hello:v1
Hello world!
[root@docker1 multi-hello]#
此时镜像多了200M左右:
[root@docker1 multi-hello]# docker image ls centos
REPOSITORY TAG IMAGE ID CREATED SIZE
centos latest 5d0da3dc9764 2 years ago 231MB
[root@docker1 multi-hello]# docker image ls multi-hello:v1
REPOSITORY TAG IMAGE ID CREATED SIZE
multi-hello v1 42afade795ac 51 seconds ago 424MB
[root@docker1 multi-hello]#
如果想要所需的效果,及运行二进制文件,编译环境不是必须的,所有可以分两次构建:
[root@docker1 multi-hello]# ls
Dockerfile hello.c
[root@docker1 multi-hello]#
从容器复制文件到宿主机,只需容器存在即可,所有此处用了docker create
(使用docker run也可以)
[root@docker1 multi-hello]# docker create --name m1 multi-hello:v1
14285a08b091a714ae9182a61ab90d75df7437987cd2a702ce84d7cd315487da
[root@docker1 multi-hello]# docker ps -a
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
14285a08b091 multi-hello:v1 "/app/hello" 4 seconds ago Created m1
[root@docker1 multi-hello]#
[root@docker1 multi-hello]# docker cp m1:/app/hello .
Successfully copied 19.5kB to /multi-hello/.
[root@docker1 multi-hello]# ls hello
hello
[root@docker1 multi-hello]#
[root@docker1 multi-hello]# vim dockerfile2
[root@docker1 multi-hello]# cat dockerfile2
FROM centos
RUN mkdir /app
COPY hello /app
CMD ["/app/hello"]
[root@docker1 multi-hello]#
[root@docker1 multi-hello]# docker build -f dockerfile2 -t multi-hello:v2 .
[root@docker1 multi-hello]# docker run --rm multi-hello:v2
Hello world!
[root@docker1 multi-hello]#
[root@docker1 multi-hello]# docker image ls multi-hello
REPOSITORY TAG IMAGE ID CREATED SIZE
multi-hello v2 99036d01a922 About a minute ago 231MB
multi-hello v1 42afade795ac 10 minutes ago 424MB
[root@docker1 multi-hello]# docker image ls centos
REPOSITORY TAG IMAGE ID CREATED SIZE
centos latest 5d0da3dc9764 2 years ago 231MB
[root@docker1 multi-hello]#
此时multi-hello:v2实现了同样的效果,但是体积小了很多
多阶段构建即使用1个dockerfile实现上面的多次构建过程:
[root@docker1 multi-hello]# rm -rf hello
[root@docker1 multi-hello]# vim dockerfile3
[root@docker1 multi-hello]# vim dockerfile3
[root@docker1 multi-hello]# cat dockerfile3
FROM centos as build1
RUN rm -rf /etc/yum.repos.d/* \
&& curl -o /etc/yum.repos.d/CentOS-Base.repo https://mirrors.aliyun.com/repo/Centos-vault-8.5.2111.repo \
&& yum -y install gcc gcc-c++ make \
&& yum clean all \
COPY hello.c /
RUN gcc -o /hello /hello.c
FROM centos as build2
WORKDIR /app
COPY --from=build1 /hello .
CMD ["/app/hello"]
[root@docker1 multi-hello]#
[root@docker1 multi-hello]# docker build -f dockerfile3 -t multi-hello:v3 .
[root@docker1 multi-hello]# docker run --rm multi-hello:v3
Hello world!
[root@docker1 multi-hello]# docker image ls multi-hello
REPOSITORY TAG IMAGE ID CREATED SIZE
multi-hello v3 0163f8be5f45 18 seconds ago 231MB
multi-hello v2 99036d01a922 17 minutes ago 231MB
multi-hello v1 42afade795ac 26 minutes ago 424MB
[root@docker1 multi-hello]#
练习:三阶段的构建
第一阶段构:
下载两个包glibc-static-2.28-251.el8.x86_64.rpm和libxcrypt-static-4.1.1-6.el8.x86_64.rpm
第二阶段:
从第一阶段拷贝两个包到根目录并安装,拷贝hello.c的源码到根目录
使用静态编译,如:gcc -o hello -static hello.c
第三阶段:基于空白镜像
获取第二阶段的hello二进制文件,复制到/app目录,并从cmd运行
[root@docker1 ~]# docker image ls centos_stream8
REPOSITORY TAG IMAGE ID CREATED SIZE
centos_stream8 latest e502e2d948d7 18 hours ago 335MB
[root@docker1 ~]#
[root@docker1 ~]# mkdir /mbuild
[root@docker1 ~]# cd /mbuild
[root@docker1 mbuild]# vim Dockerfile
[root@docker1 mbuild]# cat Dockerfile
from centos_stream8 as build1
RUN yum -y install wget \
&& wget http://rpmfind.net/linux/centos/8-stream/PowerTools/x86_64/os/Packages/glibc-static-2.28-251.el8.x86_64.rpm \
&& wget http://rpmfind.net/linux/centos/8-stream/PowerTools/x86_64/os/Packages/libxcrypt-static-4.1.1-6.el8.x86_64.rpm
from centos_stream8 as build2
COPY --from=build1 /*.rpm .
COPY hello.c /
RUN yum -y install gcc /glibc-static-2.28-251.el8.x86_64.rpm /libxcrypt-static-4.1.1-6.el8.x86_64.rpm \
&& gcc -o /hello -static /hello.c
from scratch as build3
WORKDIR /app
COPY --from=build2 /hello /app
CMD ["./hello"]
[root@docker1 mbuild]#
[root@docker1 mbuild]# vim hello.c
[root@docker1 mbuild]# cat hello.c
#include<stdio.h>
int main(void)
{
printf ("Hello world!\n");
return 0;
}
[root@docker1 mbuild]#
[root@docker1 mbuild]# docker build -t mhello .
[root@docker1 mbuild]# docker image ls mhello
REPOSITORY TAG IMAGE ID CREATED SIZE
mhello latest 17b68d3ec2d5 16 seconds ago 1.75MB
[root@docker1 mbuild]# docker run --rm mhello
Hello world!
[root@docker1 mbuild]#
注:可以使用--target,构建其中某一个阶段的镜像
如:docker build --target build1 -t mhello-prepare .
练习:
在docker2部署私有仓库,并配置https
在docker1能够直接将镜像推送到docker2的仓库中即可
[root@docker2 ~]# cat /etc/docker/daemon.json
cat: /etc/docker/daemon.json: No such file or directory
[root@docker2 ~]#
[root@docker2 ~]# docker pull registry
[root@docker2 ~]# docker image ls registry
REPOSITORY TAG IMAGE ID CREATED SIZE
registry latest d6b2c32a0f14 7 months ago 25.4MB
[root@docker2 ~]#
这里假设我们将要搭建的私有仓库地址为 docker.domain.com,下面我们介绍使用 openssl 自行签发 docker.domain.com 的站点 SSL 证书。
第一步创建 CA 私钥:
[root@docker2 certs]# openssl genrsa -out "root-ca.key" 4096
[root@docker2 certs]# ls root-ca.key
root-ca.key
[root@docker2 certs]#
第二步利用私钥创建 CA 根证书请求文件:
[root@docker2 certs]# openssl req \
> -new -key "root-ca.key" \
> -out "root-ca.csr" -sha256 \
> -subj '/C=CN/ST=tj/L=tj/O=tj/CN=tj Docker Registry CA'
[root@docker2 certs]# ls
root-ca.csr root-ca.key
[root@docker2 certs]#
第三步配置 CA 根证书,新建 root-ca.cnf:
[root@docker2 certs]# vim root-ca.cnf
[root@docker2 certs]# cat root-ca.cnf
[root_ca]
basicConstraints = critical,CA:TRUE,pathlen:1
keyUsage = critical, nonRepudiation, cRLSign, keyCertSign
subjectKeyIdentifier=hash
[root@docker2 certs]#
第四步签发根证书:
[root@docker2 certs]# openssl x509 -req -days 3650 -in "root-ca.csr" \
> -signkey "root-ca.key" -sha256 -out "root-ca.crt" \
> -extfile "root-ca.cnf" -extensions \
> root_ca
Signature ok
subject=C = CN, ST = tj, L = tj, O = tj, CN = tj Docker Registry CA
Getting Private key
[root@docker2 certs]# ls
root-ca.cnf root-ca.crt(根证书) root-ca.csr(签名请求) root-ca.key(私钥)
[root@docker2 certs]#
第五步生成站点 SSL 私钥:
[root@docker2 certs]# openssl genrsa -out "docker.domain.com.key" 4096
Generating RSA private key, 4096 bit long modulus (2 primes)
...............................................++++
..................................................................++++
e is 65537 (0x010001)
[root@docker2 certs]#
[root@docker2 certs]# ls
docker.domain.com.key root-ca.cnf root-ca.crt root-ca.csr root-ca.key
[root@docker2 certs]#
第六步使用私钥生成证书请求文件:
[root@docker2 certs]# openssl req -new -key "docker.domain.com.key" -out "site.csr" -sha256 \
> -subj '/C=CN/ST=tj/L=tj/O=tj/CN=docker.domain.com'
[root@docker2 certs]# ls
docker.domain.com.key root-ca.crt root-ca.key
root-ca.cnf root-ca.csr site.csr
[root@docker2 certs]#
第七步配置证书,新建 site.cnf 文件:
[root@docker2 certs]# vim site.cnf
[root@docker2 certs]# cat site.cnf
[server]
authorityKeyIdentifier=keyid,issuer
basicConstraints = critical,CA:FALSE
extendedKeyUsage=serverAuth
keyUsage = critical, digitalSignature, keyEncipherment
subjectAltName = DNS:docker.domain.com, IP:127.0.0.1
subjectKeyIdentifier=hash
[root@docker2 certs]#
第八步签署站点 SSL 证书:
[root@docker2 certs]# openssl x509 -req -days 750 -in "site.csr" -sha256 \
> -CA "root-ca.crt" -CAkey "root-ca.key" -CAcreateserial \
> -out "docker.domain.com.crt" -extfile "site.cnf" -extensions server
Signature ok
subject=C = CN, ST = tj, L = tj, O = tj, CN = docker.domain.com
Getting CA Private Key
[root@docker2 certs]#
[root@docker2 certs]# mkdir /ssl
[root@docker2 certs]# cp docker.domain.com.key docker.domain.com.crt root-ca.crt /ssl
[root@docker2 certs]# ls /ssl/
docker.domain.com.crt docker.domain.com.key root-ca.crt
[root@docker2 certs]#
[root@docker2 ~]# mkdir /registry
[root@docker2 ~]# mv /ssl/ /registry/
[root@docker2 ~]# cd /registry/
[root@docker2 registry]# vim config.yml
[root@docker2 registry]# cat config.yml
version: 0.1
log:
accesslog:
disabled: true
level: debug
formatter: text
fields:
service: registry
environment: staging
storage:
delete:
enabled: true
cache:
blobdescriptor: inmemory
filesystem:
rootdirectory: /var/lib/registry
auth:
htpasswd:
realm: basic-realm
path: /etc/docker/registry/auth/nginx.htpasswd
http:
addr: :443
host: https://docker.domain.com
headers:
X-Content-Type-Options: [nosniff]
http2:
disabled: false
tls:
certificate: /etc/docker/registry/ssl/docker.domain.com.crt
key: /etc/docker/registry/ssl/docker.domain.com.key
health:
storagedriver:
enabled: true
interval: 10s
threshold: 3
[root@docker2 registry]#
[root@docker2 registry]# mkdir auth
[root@docker2 registry]# yum -y install httpd-tools
[root@docker2 registry]# htpasswd -Bbn testuser redhat > auth/nginx.htpasswd
[root@docker2 registry]#
[root@docker2 registry]# tree /registry/
/registry/
├── auth
│ └── nginx.htpasswd
├── config.yml
└── ssl
├── docker.domain.com.crt
├── docker.domain.com.key
└── root-ca.crt
2 directories, 5 files
[root@docker2 registry]#
安装docker-compose编排工具:
[root@docker2 ~]# DOCKER_CONFIG=/usr/local/lib/docker/cli-plugins
[root@docker2 ~]# mkdir -p $DOCKER_CONFIG/cli-plugins
[root@docker2 ~]# curl -SL https://github.com/docker/compose/releases/download/v2.6.1/docker-compose-linux-x86_64 -o $DOCKER_CONFIG/cli-plugins/docker-compose
[root@docker2 ~]# chmod +x $DOCKER_CONFIG/cli-plugins
[root@docker2 ~]# docker compose version
Docker Compose version v2.27.0
[root@docker2 ~]#
[root@docker2 ~]# mkdir /dcompose
[root@docker2 ~]# cd /dcompose
[root@docker2 dcompose]# vim docker-compose.yml
[root@docker2 dcompose]# cat docker-compose.yml
version: '3'
services:
registry:
image: registry
ports:
- "443:443"
volumes:
- /registry:/etc/docker/registry
- registry-data:/var/lib/registry
volumes:
registry-data:
[root@docker2 dcompose]#
[root@docker2 dcompose]# docker volume ls
DRIVER VOLUME NAME
[root@docker2 dcompose]#
[root@docker2 dcompose]# cat /etc/hosts
127.0.0.1 localhost localhost.localdomain localhost4 localhost4.localdomain4 docker.domain.com
::1 localhost localhost.localdomain localhost6 localhost6.localdomain6
[root@docker2 dcompose]#
[root@docker2 dcompose]# pwd
/dcompose
[root@docker2 dcompose]# ls
docker-compose.yml
[root@docker2 dcompose]# docker compose up -d
WARN[0000] /dcompose/docker-compose.yml: `version` is obsolete
[+] Running 3/3
✔ Network dcompose_default Cr... 0.3s
✔ Volume "dcompose_registry-data" Created 0.0s
✔ Container dcompose-registry-1 Started 0.7s
[root@docker2 dcompose]#
[root@docker2 dcompose]# docker volume ls
DRIVER VOLUME NAME
local dcompose_registry-data
[root@docker2 dcompose]#
[root@docker2 dcompose]# docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
976fbe0456cc registry "/entrypoint.sh /etc…" 31 seconds ago Up 29 seconds 0.0.0.0:443->443/tcp, :::443->443/tcp, 5000/tcp dcompose-registry-1
[root@docker2 dcompose]#
测试:
由于自行签发的 CA 根证书不被系统信任,所以我们需要将 CA 根证书 ssl/root-ca.crt 移入 /etc/docker/certs.d/docker.domain.com 文件夹中。
[root@docker2 ~]# mkdir -p /etc/docker/certs.d/docker.domain.com
[root@docker2 ~]# cp /registry/ssl/root-ca.crt /etc/docker/certs.d/docker.domain.com/ca.crt
[root@docker2 ~]# ls /etc/docker/certs.d/docker.domain.com/ca.crt
/etc/docker/certs.d/docker.domain.com/ca.crt
[root@docker2 ~]#
登录:
[root@docker2 ~]# docker login docker.domain.com
Username: testuser
Password:
WARNING! Your password will be stored unencrypted in /root/.docker/config.json.
Configure a credential helper to remove this warning. See
https://docs.docker.com/engine/reference/commandline/login/#credentials-store
Login Succeeded
[root@docker2 ~]#
[root@docker2 ~]# docker tag hello-world:latest docker.domain.com/testuser/hello:v1
[root@docker2 ~]# docker push docker.domain.com/testuser/hello:v1
The push refers to repository [docker.domain.com/testuser/hello]
ac28800ec8bb: Pushed
v1: digest: sha256:d37ada95d47ad12224c205a938129df7a3e52345828b4fa27b03a98825d1e2e7 size: 524
[root@docker2 ~]#