docker入门教程四:Dockerfile制作镜像与指令详解

构建镜像docker bulid

简介

镜像构建的完整命令为docker build [OPTIONS] PATH | URL | -。在这条语句中,并没有指定Dockerfile路径,这是因为大家都习惯使用默认的文件名,实际上 Dockerfile 的文件名并不要求必须为 Dockerfile,而且并不要求必须位于上下文目录中,比如可以用 -f ../Dockerfile.php 参数指定某个文件作为 Dockerfile

PATH指的是上下文目录,那么什么是上下文呢?首先我们要理解 docker build 的工作原理。Docker 在运行时分为 Docker daemon(也就是服务端守护进程)和docker client客户端工具。Docker 的引擎提供了一组 REST API,被称为 Docker Remote API,而如 docker 命令这样的客户端工具,则是通过这组 API 与 Docker 引擎交互,从而完成各种功能。因此,虽然表面上我们好像是在本机执行各种 docker 功能,但实际上,一切都是使用的远程调用形式在服务端(Docker 引擎)完成。也因为这种 C/S 设计,让我们操作远程服务器的 Docker 引擎变得轻而易举。

docker build 命令构建镜像,其实并非在本地构建,而是在服务端,也就是 Docker 引擎中构建的。所以才引入了上下文的概念。当构建的时候,用户会指定构建镜像上下文的路径,docker build 命令得知这个路径后,会将路径下的所有内容打包,然后上传给 Docker 引擎。这样 Docker 引擎收到这个上下文包后,展开就会获得构建镜像所需的一切文件。

所以在一般情况下,我们会在构建镜像的时候,会先创建一个空目录,然后把所需要的文件都放置在这个目录下,包括Dockerfile文件,需要构建时,使用docker bulid -t name:verison .来运行。而 . 表示PATH,即上下文,docker client会把这个目录里面的文件都打包,传给docker daemon。正是由于这个特性,所有Dockerfile的指令中,COPY、ADD这两个都只能使用相对绝对,即COPY ./a.txt /test/

实例

使用Dockerfile来构建一个tank大战的image。先下载源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
[root@master ~]# git clone https://codehub.devcloud.huaweicloud.com/Demo04260/tank.git
Cloning into 'tank'...
remote: Enumerating objects: 90, done.
remote: Counting objects: 100% (90/90), done.
remote: Compressing objects: 100% (86/86), done.
remote: Total 90 (delta 9), reused 0 (delta 0)
Unpacking objects: 100% (90/90), done.
[root@master ~]# cd tank/
[root@master tank]# cat Dockerfile
FROM nginx
LABEL maintainer "fangdm<8@8994.cn>"

EXPOSE 80
COPY . /usr/share/nginx/html

开始创建,运行命令之后,从第一个输出内容可以看到,docker有包内容全部打包给了Docker daemon。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
[root@master tank]# docker build -t tank .
Sending build context to Docker daemon 2.315MB
Step 1/4 : FROM nginx
---> 62c261073ecf
Step 2/4 : LABEL maintainer "fangdm<8@8994.cn>"
---> Running in 33da0adbde73
Removing intermediate container 33da0adbde73
---> ca6c7c4571b7
Step 3/4 : EXPOSE 80
---> Running in 74879a5d980b
Removing intermediate container 74879a5d980b
---> d0649d7b613d
Step 4/4 : COPY . /usr/share/nginx/html
---> 46a940d07f6d
Successfully built 46a940d07f6d
Successfully tagged tank:latest
[root@master tank]# docker images tank
REPOSITORY TAG IMAGE ID CREATED SIZE
tank latest 46a940d07f6d 3 minutes ago 111MB

这样就创建成功了。运行:

1
2
3
4
[root@master tank]# docker run --name tank -d -P tank
11c8314f6f5db62fe0783b52c6fccb45b36ed0539c192076be3f56bf7605fd0b
[root@master tank]# docker port tank
80/tcp -> 0.0.0.0:32769

访问docker服务器的32769端口即可开始坦克大战了。

由于在构建的过程中docker会采用缓存的机制,如果需要重新构建,不想使用cache需要添加—no-cache。

Dockerfile指令

Dockerfile是一个包含用户能够构建镜像的所有命令的文本文档,它有自己的语法以及命令,docker能够从dockerfile中读取指令自动的构建镜像。

FROM

FROM 就是指定 基础镜像,因此一个 DockerfileFROM 是必备的指令,并且必须是第一条指令。有以下格式:

1
2
3
4
FROM scratch
FROM <image> [AS <name>]
FROM <image>[:<tag>] [AS <name>]
FROM <image>[@<digest>] [AS <name>]

scratch 这个镜像是虚拟的概念,并不实际存在,它表示一个空白的镜像。以 scratch 为基础镜像的话,意味着你不以任何镜像为基础,接下来所写的指令将作为镜像第一层开始存在。需要自己来一层一层地创建。直接 FROM scratch 会让镜像体积更加小巧。使用 Go 语言 开发的应用很多会使用这种方式来制作镜像,这也是为什么有人认为 Go 是特别适合容器微服务架构的语言的原因之一。

如果没有指定 tag ,latest 将会被指定为要使用的基础镜像版本。

允许多个from,即就是多阶段构建。后续介绍

MAINTAINER

格式为MAINTAINER user_name user_email,指定维护者信息。可以使用LABEL代替。

LABEL

给镜像添加元数据,可以用LABEL命令替换MAINTAINER命令。指定一些作者、邮箱等信息。设置之后,使用docker inspect来查看。

具体格式:LABEL <key>=<value> <key>=<value> <key>=<value> ...

1
2
[root@master tank]# docker inspect -f '{{.Config.Labels}}' tank
map[maintainer:fangdm<8@8994.cn>]

RUN

格式为:RUN ["executable", "param1", "param2"] 或者 RUN <command>

RUN会在当前镜像的最上面创建一个新层,并且能执行任何的命令,然后对执行的结果进行提交。提交后的结果镜像在dockerfile的后续步骤中可以使用。

如果有多个 RUN 指令,最佳建议是使用 && 合并为一个,并且格式要统一。

1
2
3
4
5
6
FROM php:7.1-fpm

RUN apt-get update && apt-get install -y libfreetype6-dev libjpeg62-turbo-dev libmcrypt-dev libpng-dev \
&& docker-php-ext-install -j$(nproc) iconv mcrypt mysqli pdo_mysql \
&& docker-php-ext-configure gd --with-freetype-dir=/usr/include/ --with-jpeg-dir=/usr/include/ \
&& docker-php-ext-install -j$(nproc) gd

WORKDIR

格式为: WORKDIR /path/to/workdir

WORKDIR 用来指定工作目录的。Docker 默认的工作目录是/,只有 RUN 能执行 cd 命令切换目录,而且还只作用在当下下的 RUN,也就是说每一个 RUN 都是独立进行的。如果想让其他指令在指定的目录下执行,就得靠 WORKDIR。WORKDIR 动作的目录改变是持久的,不用每个指令前都使用一次 WORKDIR。如果该目录不存在,则会创建。

ENV

设置环境变量,设置的变量可供后面指令使用。可以使用docker inspect查的。使用docker exec进入容器之后,使用env命令也是可以输出的。

设置的方法是: ENV <key1>=<value1> <key2>=<value2>... 或者 ENV <key> <value>,建议还加上等号,比较直观。

如果在容器运行时想修改环境变量的值 ,可以使用docker run --env <key>=<value>来修改环境变量。

1
2
3
4
5
6
7
[root@master tank]# docker exec tank env
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
HOSTNAME=11c8314f6f5d
NGINX_VERSION=1.17.0
NJS_VERSION=0.3.2
PKG_RELEASE=1~stretch
HOME=/root

ARG

格式:ARG <参数名>[=<默认值>]

构建参数和 ENV 的效果一样,都是设置环境变量。所不同的是,ARG 所设置的构建环境的环境变量,在将来容器运行时是不会存在这些环境变量的。即ARG 定义的变量只在建立 image 时有效,建立完成后变量就失效消失

Dockerfile 中的 ARG 指令是定义参数名称,以及定义其默认值。该默认值可以在构建命令 docker build 中用 --build-arg <参数名>=<值> 来覆盖。

使用时,类似shell的变量一样,前面加$即可。

COPY

格式:COPY [--chown=<user>:<group>] <源路径>... <目标路径> 或者 COPY [--chown=<user>:<group>] ["<源路径1>",... "<目标路径>"]

COPY 将文件从路径 复制添加到容器内部路径 必须是想对于源文件夹的一个文件或目录,也可以是一个远程的url, 是目标容器中的绝对路径。所有的新文件和文件夹都会创建UID 和 GID 。事实上如果 是一个远程文件URL,那么目标文件的权限将会是600。用法是:COPY [--chown=<user>:<group>] <源路径>... <目标路径>

标路径不需要事先创建,如果目录不存在会在复制文件前先行创建缺失目录。用 COPY 指令,源文件的各种元数据都会保留。比如读、写、执行权限、文件变更时间等。在使用该指令的时候还可以加上 --chown=<user>:<group> 选项来改变文件的所属用户及所属组。

ADD

跟COPY的作用一样,都是复制文件到容器。但比COPY更高级,会自动下载或者自动解压,如果 <源路径> 为一个 tar 压缩文件的话,压缩格式为 gzip, bzip2 以及 xz 的情况下,ADD 指令将会自动解压缩这个压缩文件到 <目标路径> 去。

VOLUME

定义数据卷,格式为:VOLUME ["<路径1>", "<路径2>"...] 或者 VOLUME <路径>

定义完成之后,docker在创建容器时,会自动创建volume数据卷,这时即使容器删除了,其数据也不是丢失。

EXPOSE

指定端口。格式为 EXPOSE <端口1> [<端口2>...]

指定容器运行时对外暴露的端口,但是该指定实际上不会发布该端口,它的功能是镜像构建者和容器运行者之间的记录文件。run命令有-p和-P两个参数,如果是-P(大写)就是会随机端口映射,容器内会随机映射到EXPOSE指定的端口。

CMD

格式:

  • shell 格式:CMD <命令>
  • exec 格式:CMD [“可执行文件”, “参数1”, “参数2”…]

在指令格式上,一般推荐使用 exec 格式,这类格式在解析时会被解析为 JSON 数组,因此一定要使用双引号 ",而不要使用单引号。注意,如果使用 shell 格式的话,实际的命令会被包装为 sh -c 的参数的形式进行执行。即如果CMD echo $HOME,在实际执行中,会将其变更为:CMD [ "sh", "-c", "echo $HOME" ]

指定容器启动时默认运行的命令,在一个Dockerfile文件中,如果有多个CMD命令,只有一个最后一个会生效。RUN指令是在构建镜像时候执行的,而CMD指令是在每次容器运行的时候执行的。而如果docker run命令接了command,会覆盖CMD的命令。

对于容器而言,其启动程序就是容器应用进程,容器就是为了主进程而存在的,主进程退出,容器就失去了存在的意义,从而退出,其它辅助进程不是它需要关心的东西。如果写为 CMD service nginx start,会解析为 CMD [ "sh", "-c", "service nginx start"],这样主进程是sh了。那么当 service nginx start 命令结束后,sh 也就结束了,sh 作为主进程退出了,自然就会令容器退出。正确的做法是直接执行 nginx 可执行文件,并且要求以前台形式运行。比如:CMD ["nginx", "-g", "daemon off;"]

ENTRYPOINT

ENTRYPOINT 的格式和 RUN 指令格式一样,分为 exec 格式和 shell 格式。

ENTRYPOINT 的目的和 CMD 一样,都是在指定容器启动程序及参数。ENTRYPOINT 在运行时也可以替代,不过比 CMD 要略显繁琐,需要通过 docker run 的参数 --entrypoint 来指定。

ENTRYPOINT和CMD同时存在时, docker把CMD的命令拼接到ENTRYPOINT命令之后,即<ENTRYPOINT> "<CMD>", 拼接后的命令才是最终执行的命令。

在一个Dockerfile文件中,如果有多个ENTRYPOINT命令,也只有一个最后一个会生效。而不同点是通过docker run command命令会覆盖CMD的命令,但如果有ENTRYPOINT,则执行的命令不会覆盖ENTRYPOINT,docker run命令中指定的任何参数都会被当做参数传递给ENTRYPOINT。

例如,如果Dockerfile只有 CMD [ "curl", "-s", "https://ip.cn" ],那么运行 docker run myip -i 会出错,如果是 ENTRYPOINT [ "curl", "-s", "https://ip.cn" ],运行 docker run myip -i 就会输出Http头部信息,这是因为当存在 ENTRYPOINT 后,CMD 的内容将会作为参数传给 ENTRYPOINT,而这里 -i 就是新的 CMD,因此会作为参数传给 curl,从而达到了我们预期的效果。

有关这两者的区别,可以参考:https://yeasy.gitbooks.io/docker_practice/content/image/dockerfile/entrypoint.html 以及 Dockerfile: ENTRYPOINT和CMD的区别

HEALTHCHECK

格式:

  • HEALTHCHECK [选项] CMD <命令>:设置检查容器健康状况的命令
  • HEALTHCHECK NONE:如果基础镜像有健康检查指令,使用这行可以屏蔽掉其健康检查指令

HEALTHCHECK 指令是告诉 Docker 应该如何进行判断容器的状态是否正常,这是 Docker 1.12 引入的新指令。支持下列选项:

  • --interval=<间隔>:两次健康检查的间隔,默认为 30 秒;
  • --timeout=<时长>:健康检查命令运行超时时间,如果超过这个时间,本次健康检查就被视为失败,默认 30 秒;
  • --retries=<次数>:当连续失败指定次数后,则将容器状态视为 unhealthy,默认 3 次。

如下的Dockerfile

1
2
3
4
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

运行之后就会检测其健康,如下是正常的,healthy:

1
2
3
root@fdm:~/test# docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
6cd513fdb549 myweb:v1 "nginx -g 'daemon ..." 19 minutes ago Up 19 minutes (healthy) 80/tcp sleepy_booth

.dockerignore

这个不是指令,只是一个文件,用来忽略上下文目录中包含的一些image用不到的文件,它们不会传送到docker daemon。

其他的一些指令,可以参考:

https://docs.docker.com/v17.09/engine/reference/builder/

Dockerfile多阶段构建

Dockerfile最佳实践

实例

ubuntu ssh

官方镜像都没有ssh的功能,给他加上吧。先创建一个空的目录,然后创建以下文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
root@fdm:~/sshd_ubuntu# cat Dockerfile
FROM ubuntu:18.04
MAINTAINER fangdm <5608381@qq.com>
ADD run.sh /
RUN apt-get update && apt-get install -y openssh-server && \
mkdir -p /var/run/sshd && \
echo 'root:tjvIa1fp' | chpasswd && \
echo "PermitRootLogin yes" >>/etc/ssh/sshd_config && \
sed -i 's/#PasswordAuthentication/PasswordAuthentication/' /etc/ssh/sshd_config && \
echo 'tjvIa1fp' >/root/passwd.txt && \
chmod 755 /run.sh && \
apt-get clean && \
rm -rf /var/lib/apt/lists/*
EXPOSE 22
CMD ["/run.sh"]

root@fdm:~/sshd_ubuntu# cat run.sh
#!/bin/bash
echo "root password: tjvIa1fp"
/usr/sbin/sshd -D

然后直接docker build -t ubuntu-sshd:v1 .即可。完成之后,可以看到,其大小比原来的ubuntu大了三分之二。仅仅只是安装了openssh-server。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
[root@master ubuntu_ssh]# docker images |grep ubuntu
ubuntu-sshd v1 d7c322e5bcf5 10 minutes ago 179MB
ubuntu 18.04 2ca708c1c9cc 3 days ago 64.2MB
ubuntu latest a2a15febcdf3 5 weeks ago 64.2MB
[root@master ubuntu_ssh]# docker run -itd -P --name ssh ubuntu-sshd:v1
fba8152f891cad00ef2e5aeb4cdeede5d17e59b0182e5ea049bb2fae6f8f2732
[root@master ubuntu_ssh]# docker ps -l
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
fba8152f891c ubuntu-sshd:v1 "/run.sh" 10 seconds ago Up 6 seconds 0.0.0.0:32771->22/tcp ssh
#查密码
[root@master ubuntu_ssh]# docker logs ssh
root password: tjvIa1fp
#查IP
[root@master ubuntu_ssh]# docker inspect -f '{{ .NetworkSettings.IPAddress }}' ssh
172.17.0.4
#登陆
[root@master ubuntu_ssh]# ssh 172.17.0.4
The authenticity of host '172.17.0.4 (172.17.0.4)' can't be established.
ECDSA key fingerprint is SHA256:bS6WcqPKrrbhgAJ9LgRdabc5kKbtRMH1Hgykz71tatY.
ECDSA key fingerprint is MD5:a5:3d:88:2f:2b:c7:a0:78:0e:c1:95:51:1d:72:4d:2b.
Are you sure you want to continue connecting (yes/no)? yes
Warning: Permanently added '172.17.0.4' (ECDSA) to the list of known hosts.
root@172.17.0.4's password:
root@fba8152f891c:~# cat /etc/issue
Ubuntu 18.04.3 LTS \n \l

root@fba8152f891c:~#

在此基本上,可以再次制作nginx,不过是简单版:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
root@fdm:~# cat nginx/Dockerfile
FROM ubuntu-sshd:v1
LABEL maintainer="fangdm <8@8994.cn>"

ADD run.sh /run.sh
WORKDIR /etc/nginx

RUN apt-get install -y nginx && \
rm -rf /var/lib/apt/lists/* && \
echo "daemon off;" >>/etc/nginx/nginx.conf && \
chown -R www-data:www-data /var/lib/nginx && \
rm -f /usr/share/nginx/html/index.html && \
echo "Asia/Shanghai" > /etc/timezone && \
#dpkg-reconfigure -f noninteractive tzdata && \
chmod 755 /run.sh

COPY index.html /var/www/html
VOLUME ["/etc/nginx/sites-enabled","/etc/nginx/certs","/etc/nginx/conf.d","/var/log/nginx"]

CMD ["/run.sh"]
EXPOSE 80
EXPOSE 443

root@fdm:~# cat nginx/run.sh
#!/bin/bash

/usr/sbin/sshd &
/usr/sbin/nginx

可以使用supervisord来控制服务,https://my.oschina.net/renguijiayi/blog/365087

https://blog.csdn.net/Ricky110/article/details/78360229

nginx-alpine版

alpine介绍

首先先介绍下busybox以及alpine。

两者的大小为:

1
2
3
[root@master ~]# docker images |egrep 'busybox|alpine'
busybox latest 19485c79a9bb 2 weeks ago 1.22MB
alpine latest 961769676411 4 weeks ago 5.58MB

由于alpine小巧、安全、简单以及功能完备的特点,被广泛应用于众多Docker容器中。不同于centos使用yum、ubuntu使用apt-get去安装软件,alpine使用了apk这个指令去安装相应的指令。以下是修改apk使用阿里的源。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
# alpine 只有/bin/sh,没有/bin/bash
[root@master ~]# docker exec -it alpine /bin/bash
OCI runtime exec failed: exec failed: container_linux.go:345: starting container process caused "exec: \"/bin/bash\": stat /bin/bash: no such file or directory": unknown
# 修改源
[root@master ~]# docker exec -it alpine /bin/sh
/ # cat /etc/apk/repositories
http://dl-cdn.alpinelinux.org/alpine/v3.10/main
http://dl-cdn.alpinelinux.org/alpine/v3.10/community
/ # echo -e "http://mirrors.aliyun.com/alpine/latest-stable/main\nhttp://mirrors.aliyun.com/alpine/latest-stable/community" >/etc/apk/repositories
/ # cat /etc/apk/repositories
http://mirrors.aliyun.com/alpine/latest-stable/main
http://mirrors.aliyun.com/alpine/latest-stable/community
/ # vim /etc/apk/repositories
/bin/sh: vim: not found

# 安装 vim 命令
/ # apk add vim
fetch http://mirrors.aliyun.com/alpine/latest-stable/main/x86_64/APKINDEX.tar.gz
fetch http://mirrors.aliyun.com/alpine/latest-stable/community/x86_64/APKINDEX.tar.gz
(1/5) Installing lua5.3-libs (5.3.5-r2)
(2/5) Installing ncurses-terminfo-base (6.1_p20190518-r0)
(3/5) Installing ncurses-terminfo (6.1_p20190518-r0)
(4/5) Installing ncurses-libs (6.1_p20190518-r0)
(5/5) Installing vim (8.1.1365-r0)
Executing busybox-1.30.1-r2.trigger
OK: 40 MiB in 19 packages
/ # exit

使用apk --update add --no-cache <package>来安装命令。安装完成vim,就增加了40MB了。

根据官方的alpine Dockerfile来看,其实非常简单:

1
2
3
FROM scratch
ADD alpine-minirootfs-3.10.2-x86_64.tar.gz /
CMD ["/bin/sh"]

构建nginx

首先先看一下官方的写法 docker-nginx Dockerfile ,可以看出,其思路是直接nginx提供的源,然后直接使用apk去安装。这是比较方便,但是无法自定义参数,所以还是需要使用编译大法。

先下载必要的软件包:

1
2
3
git clone https://github.com/alibaba/nginx-http-concat.git
git clone https://github.com/FRiCKLE/ngx_cache_purge.git
wget https://nginx.org/download/nginx-1.16.1.tar.gz

编写Dockerfile,nginx默认安装在/usr/local/nginx目录下,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
[root@master nginx]# cat Dockerfile
FROM alpine:3.10 as builder

LABEL maintainer="fangdm<8@8994.cn>"

ENV NGINX_VERSION 1.16.1
ENV CONFIG "--prefix=/usr/local/nginx --user=www --group=www --with-http_auth_request_module --with-http_realip_module --with-http_v2_module --with-debug --with-http_random_index_module --with-http_sub_module --with-http_addition_module --with-http_secure_link_module --with-http_ssl_module --with-stream_ssl_module --with-stream_realip_module --with-stream_ssl_preread_module --with-stream --add-module=../ngx_cache_purge --add-module=../nginx-http-concat --with-http_slice_module --with-threads --with-http_gzip_static_module --with-http_gunzip_module --with-http_stub_status_module"

ADD . /nginx/

RUN set -x \
&& addgroup -g 101 -S www \
&& adduser -S -D -H -u 101 -h /var/cache/www -s /sbin/nologin -G www -g www www \
&& echo -e "http://mirrors.aliyun.com/alpine/latest-stable/main\nhttp://mirrors.aliyun.com/alpine/latest-stable/community" >/etc/apk/repositories \
&& apk add --no-cache --virtual .bulidDeps gcc g++ make libxslt-dev libxml2-dev gd-dev wget linux-headers pcre pcre-dev zlib zlib-dev openssl openssl-dev geoip geoip-dev \
&& cd /nginx \
&& tar zxf nginx-1.16.1.tar.gz \
&& cd nginx-1.16.1 \
&& ./configure $CONFIG \
&& make \
&& make install \
&& cd / \
&& tar zcvf nginx.tar.gz /usr/local/nginx


FROM alpine:3.10

ENV NGINX_VERSION="1.16.1" LANG="en_US.UTF-8" GLIBC_PKG_VERSION="2.30-r0"
COPY --from=builder /nginx.tar.gz /

RUN set -x \
&& cd / \
&& tar zxvf /nginx.tar.gz \
&& addgroup -g 101 -S www \
&& adduser -S -D -H -u 101 -h /var/cache/www -s /sbin/nologin -G www -g www www \
&& echo -e "http://mirrors.aliyun.com/alpine/latest-stable/main\nhttp://mirrors.aliyun.com/alpine/latest-stable/community" >/etc/apk/repositories \
&& apk add --no-cache --virtual .nginx-rundeps gettext gd libxslt geoip pcre\
&& mv /usr/bin/envsubst /tmp/ \
&& runDeps="$( \
scanelf --needed --nobanner /tmp/envsubst \
| awk '{ gsub(/,/, "\nso:", $2); print "so:" $2 }' \
| sort -u \
| xargs -r apk info --installed \
| sort -u \
)" \
&& apk add --no-cache $runDeps \
&& mv /tmp/envsubst /usr/local/bin/ \
&& apk add --no-cache tzdata \
&& ln -sf /usr/share/zoneinfo/Asia/Shanghai /etc/localtime \
&& echo "Asia/Shanghai" > /etc/timezone \
&& ln -sf /dev/stdout /usr/local/nginx/logs/access.log \
&& ln -sf /dev/stderr /usr/local/nginx/logs/error.log \
\
#安装中文字体库
&& cd /usr/local/src \
&& curl -Lo /etc/apk/keys/sgerrand.rsa.pub "https://github.com/sgerrand/alpine-pkg-glibc/releases/download/${GLIBC_PKG_VERSION}/sgerrand.rsa.pub" \
&& curl -Lo glibc-${GLIBC_PKG_VERSION}.apk "https://github.com/sgerrand/alpine-pkg-glibc/releases/download/${GLIBC_PKG_VERSION}/glibc-${GLIBC_PKG_VERSION}.apk" \
&& curl -Lo glibc-bin-${GLIBC_PKG_VERSION}.apk "https://github.com/sgerrand/alpine-pkg-glibc/releases/download/${GLIBC_PKG_VERSION}/glibc-bin-${GLIBC_PKG_VERSION}.apk" \
&& curl -Lo glibc-i18n-${GLIBC_PKG_VERSION}.apk "https://github.com/sgerrand/alpine-pkg-glibc/releases/download/${GLIBC_PKG_VERSION}/glibc-i18n-${GLIBC_PKG_VERSION}.apk" \
&& apk add glibc-${GLIBC_PKG_VERSION}.apk glibc-bin-${GLIBC_PKG_VERSION}.apk glibc-i18n-${GLIBC_PKG_VERSION}.apk \
&& /usr/glibc-compat/bin/localedef --force --inputfile POSIX --charmap UTF-8 "$LANG" || true \
&& echo "export LANG=$LANG" > /etc/profile.d/locale.sh \
&& rm -fr /usr/local/src /var/cache/apk/*

EXPOSE 80

CMD ["/usr/local/nginx/sbin/nginx", "-g", "daemon off;"]

为了减少镜像的体积,使用了多阶段构建的方法。同时多阶段构建时,只能复制文件,不能复制目录,所以需要对/usr/local/nginx进行打包,使用``docker build -t nginx_alpine .构建完成之后,可以看到nginx_alpine大小为31.1M,而编译过程中,产生了284M。

1
2
3
4
[root@master nginx]# docker images
REPOSITORY TAG IMAGE ID CREATED SIZE
nginx_alpine latest 7c74ceae2589 24 minutes ago 31.1MB
<none> <none> bc55a29db5af 25 minutes ago 284MB

另外,alpine是不支持中文,要集成中文环境,可以参考:https://www.clxz.top/2019/05/09/160241/

创建centos镜像

tar打包构建

第一种方法,通过tar打包构建基础镜像:

1
2
3
yum clean all
rm -rf /var/cache/yum
tar --numeric-owner --exclude=/proc --exclude=/sys --exclude=/mnt --exclude=/var/cache --exclude=/usr/share/{foomatic,backgrounds,perl5,fonts,cups,qt4,groff,kde4,icons,pixmaps,emacs,gnome-background-properties,sounds,gnome,games,desktop-directories} --exclude=/var/log -zcvf /mnt/CentOS-7.6-BaseImage.tar.gz /

安装docker:

1
2
3
4
5
6
7
# 安装EPEL源和REMI源
rpm -Uvh https://dl.fedoraproject.org/pub/epel/epel-release-latest-7.noarch.rpm
rpm -Uvh https://rpms.remirepo.net/enterprise/remi-release-7.rpm
# 安装Docker软件包
yum install -y docker-io
# 启动Docker服务
systemctl start docker.service

将打包好的文件导入到docker中,然后验证:

1
2
3
4
5
6
7
8
9
[root@localhost ~]# cat /mnt/CentOS-7.-BaseImage.tar.gz |docker import - centos-tar:7.6
sha256:b629e1dd6b5365d96cc8aa0d4918215a1609fd2d8d41688a3507c78f428451d9
[root@localhost ~]# docker images
REPOSITORY TAG IMAGE ID CREATED SIZE
centos-tar 7.6 b629e1dd6b53 2 minutes ago 751 MB
[root@localhost ~]# docker run --rm -it centos-tar:7.6 bash
[root@949f13b77435 /]# w
09:11:52 up 1:12, 2 users, load average: 0.12, 0.62, 0.42
USER TTY FROM LOGIN@ IDLE JCPU PCPU WHAT

mkimage-yum.sh脚本构建

第二种方法,运行mkimage-yum.sh脚本,创建CentOS的基础镜像,首先下载
wget https://raw.githubusercontent.com/moby/moby/master/contrib/mkimage-yum.sh,然后运行脚本:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
[root@localhost ~]# sh mkimage-yum.sh centos-mkimage
+ mkdir -m 755 /tmp/mkimage-yum.sh.sB2M3f/dev
+ mknod -m 600 /tmp/mkimage-yum.sh.sB2M3f/dev/console c 5 1
+ mknod -m 600 /tmp/mkimage-yum.sh.sB2M3f/dev/initctl p
+ mknod -m 666 /tmp/mkimage-yum.sh.sB2M3f/dev/full c 1 7
...
+ version=
+ for file in '"$target"/etc/{redhat,system}-release'
+ '[' -r /tmp/mkimage-yum.sh.7yJbJ7/etc/redhat-release ']'
++ sed 's/^[^0-9\]*\([0-9.]\+\).*$/\1/' /tmp/mkimage-yum.sh.7yJbJ7/etc/redhat-release
+ version=7.6.1810
+ break
+ '[' -z 7.6.1810 ']'
+ tar --numeric-owner -c -C /tmp/mkimage-yum.sh.7yJbJ7 .
+ docker import - centos-mkimage:7.6.1810
sha256:e3156476a6818c2f12453da3b6f182a38cb70a055b75e153b9dcee36c1adbd68
+ docker run -i -t --rm centos-mkimage:7.6.1810 /bin/bash -c 'echo success'
success
+ rm -rf /tmp/mkimage-yum.sh.7yJbJ7

[root@localhost ~]# docker images
REPOSITORY TAG IMAGE ID CREATED SIZE
centos-mkimage 7.6.1810 e3156476a681 49 seconds ago 281 MB
centos-tar 7.6 b629e1dd6b53 13 minutes ago 751 MB

由上述两图可知,通过tar打包创建的基础镜像的体积大约是通过mkimage-yum.sh脚本创建的基础镜像的3倍左右。这是由于前者是基于最小化安装的CentOS系统而创建的,而后者是从CentOS官方源安装必要的软件包而创建的,前者比后者多安装了很多软件包,包括邮件工具、设备驱动程序,等等

http://ghoulich.xninja.org/2017/10/13/how-to-create-docker-base-image-of-centos/

mysql 5.7

基础知识

想对mysql进行镜像化,必须掌握mysql的一些操作命令。

my_print_defaults为查询my.cnf配置文件的命令,第一个参数为section

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
[root@ee1995b6bc44 /]# my_print_defaults mysqld 
--port=3306
--datadir=/var/lib/mysql
--max_connections=200
--character-set-server=utf8
--bind-address=0.0.0.0
--socket=/var/sock/mysqld/mysqld.sock
--pid-file=/var/run/mysqld/mysqld.pid
--log-error=/var/log/mysql/error.log
--slow_query_log_file=/var/log/mysql/slow.log
--general_log_file=/var/log/mysql/query.log
[root@ee1995b6bc44 /]#
[root@ee1995b6bc44 /]#
[root@ee1995b6bc44 /]#
[root@ee1995b6bc44 /]# my_print_defaults mysqld |grep "^--datadir=" | cut -d= -f2- | tail -n 1
/var/lib/mysql

另外,也可以使用mysqld —verbose —help方法来获取

1
2
3
4
[root@xmxyk bin]#./mysqld --verbose --help  2>/dev/null | awk -v key="datadir" '$1 == key { print $2; exit }'
/usr/local/mysql/var/
[root@xmxyk bin]#./mysqld --verbose --help 2>/dev/null | awk -v key="socket" '$1 == key { print $2; exit }'
/tmp/mysql.sock

以空密码的方式初始化数据库:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
[root@ee1995b6bc44 /]# mysqld --initialize-insecure --datadir="/var/lib/mysql/" --user=${MYSQL_USER}
[root@ee1995b6bc44 /]# ll /var/lib/mysql
total 110620
-rw-r----- 1 mysql mysql 56 Mar 16 12:35 auto.cnf
-rw-r----- 1 mysql mysql 419 Mar 16 12:35 ib_buffer_pool
-rw-r----- 1 mysql mysql 50331648 Mar 16 12:35 ib_logfile0
-rw-r----- 1 mysql mysql 50331648 Mar 16 12:35 ib_logfile1
-rw-r----- 1 mysql mysql 12582912 Mar 16 12:35 ibdata1
drwxr-x--- 2 mysql mysql 4096 Mar 16 12:35 mysql
drwxr-x--- 2 mysql mysql 4096 Mar 16 12:35 performance_schema
drwxr-x--- 2 mysql mysql 12288 Mar 16 12:35 sys
[root@ee1995b6bc44 /]# cat /var/log/mysql/error.log
2019-03-16T12:35:48.881747Z 0 [Warning] TIMESTAMP with implicit DEFAULT value is deprecated. Please use --explicit_defaults_for_timestamp server option (see documentation for more details).
2019-03-16T12:35:49.345885Z 0 [Warning] InnoDB: New log files created, LSN=45790
2019-03-16T12:35:49.439030Z 0 [Warning] InnoDB: Creating foreign key constraint system tables.
2019-03-16T12:35:49.521977Z 0 [Warning] No existing UUID has been found, so we assume that this is the first time that this server has been started. Generating a new UUID: 077e6497-47e8-11e9-9508-0242ac110002.
2019-03-16T12:35:49.524840Z 0 [Warning] Gtid table is not ready to be used. Table 'mysql.gtid_executed' cannot be opened.
2019-03-16T12:35:49.526731Z 1 [Warning] root@localhost is created with an empty password ! Please consider switching off the --initialize-insecure option.

Dockerfile

从Dockerfile文件可以看到是使用了yum进行安装的mysql 5.7,安装好了之后,生成了/etc/my.cnf文件,而启动mysql则是交给了docker-entrypoint.sh

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
[root@host mysql]# cat Dockerfile 
FROM centos:7
MAINTAINER fangdm <fang2000@vip.qq.com>

LABEL image="mysql-5.7" vendor=fangdm build-date="2019-03-15"
ENV REPO_URL="https://repo.mysql.com//mysql57-community-release-el7-11.noarch.rpm" \
MYSQL_ROOT_PASSWORD="**Random**" \
MYSQL_VERSION=5.7 \
MYSQL_USER=mysql \
MYSQL_GROUP=mysql \
MYSQL_UID=27 \
MYSQL_GID=27

COPY docker-entrypoint.sh /
RUN groupadd -g ${MYSQL_GID} -r ${MYSQL_GROUP} && chmod 755 /docker-entrypoint.sh && \
adduser ${MYSQL_USER} -u ${MYSQL_UID} -s /sbin/nologin -M -g ${MYSQL_GROUP}

RUN yum install -y epel-release && \
rpm -ivh ${REPO_URL} && \
yum-config-manager --disable mysql55-community && \
yum-config-manager --disable mysql56-community && \
yum-config-manager --disable mysql80-community && \
yum-config-manager --enable mysql57-community && \
yum install -y mysql-community-server && \
yum -y autoremove && \
yum clean all

ENV MYSQL_BASE_INCL="/etc/my.cnf.d" \
MYSQL_CUST_INCL1="/etc/mysql/conf.d" \
MYSQL_MY_CNF="/etc/my.cnf" \
MYSQL_CUST_INCL2="/etc/mysql/docker-default.d" \
MYSQL_DEF_DATA="/var/lib/mysql" \
MYSQL_DEF_PID="/var/run/mysqld" \
MYSQL_DEF_SOCK="/var/sock/mysqld" \
MYSQL_DEF_LOG="/var/log/mysql"

ENV MYSQL_LOG_SLOW="${MYSQL_DEF_LOG}/slow.log" \
MYSQL_LOG_ERROR="${MYSQL_DEF_LOG}/error.log" \
MYSQL_LOG_QUERY="${MYSQL_DEF_LOG}/query.log"

RUN rm -rf $MYSQL_BASE_INCL $MYSQL_CUST_INCL1 $MYSQL_CUST_INCL2 $MYSQL_DEF_DATA $MYSQL_DEF_SOCK $MYSQL_DEF_LOG && \
mkdir -p $MYSQL_BASE_INCL $MYSQL_CUST_INCL1 $MYSQL_CUST_INCL2 $MYSQL_DEF_DATA $MYSQL_DEF_SOCK $MYSQL_DEF_LOG && \
chmod 0755 $MYSQL_BASE_INCL $MYSQL_CUST_INCL1 $MYSQL_CUST_INCL2 $MYSQL_DEF_DATA $MYSQL_DEF_SOCK $MYSQL_DEF_LOG && \
chown ${MYSQL_USER}:${MYSQL_GROUP} $MYSQL_BASE_INCL $MYSQL_CUST_INCL1 $MYSQL_CUST_INCL2 $MYSQL_DEF_DATA $MYSQL_DEF_SOCK $MYSQL_DEF_LOG

RUN echo "[client]" >$MYSQL_MY_CNF && \
echo "socket = ${MYSQL_DEF_SOCK}/mysqld.sock" >>$MYSQL_MY_CNF && \
echo -e "default-character-set=utf8\n" >>$MYSQL_MY_CNF && \
echo "[mysql]" >>$MYSQL_MY_CNF && \
echo "socket = ${MYSQL_DEF_SOCK}/mysqld.sock" >>$MYSQL_MY_CNF && \
echo -e "default-character-set=utf8\n" >>$MYSQL_MY_CNF && \
echo "[mysqld]" >>$MYSQL_MY_CNF && \
echo "skip-name-resolve" >>$MYSQL_MY_CNF && \
echo "skip-host-cache" >>$MYSQL_MY_CNF && \
echo "port = 3306" >>$MYSQL_MY_CNF && \
echo "user = ${MYSQL_USER}" >>$MYSQL_MY_CNF && \
echo "datadir=$MYSQL_DEF_DATA" >>$MYSQL_MY_CNF && \
echo "max_connections = 200" >>$MYSQL_MY_CNF && \
echo "character-set-server = utf8" >>$MYSQL_MY_CNF && \
echo "bind-address = 0.0.0.0" >>$MYSQL_MY_CNF && \
echo "socket = ${MYSQL_DEF_SOCK}/mysqld.sock" >>$MYSQL_MY_CNF && \
echo "pid-file = ${MYSQL_DEF_PID}/mysqld.pid" >>$MYSQL_MY_CNF && \
echo "log-error = ${MYSQL_LOG_ERROR}" >>$MYSQL_MY_CNF && \
echo "general_log_file = ${MYSQL_LOG_QUERY}" >>$MYSQL_MY_CNF && \
echo "slow_query_log_file = ${MYSQL_LOG_SLOW}" >>$MYSQL_MY_CNF && \
echo "!includedir ${MYSQL_BASE_INCL}/" >>$MYSQL_MY_CNF && \
echo "!includedir ${MYSQL_CUST_INCL1}/" >>$MYSQL_MY_CNF && \
echo "!includedir ${MYSQL_CUST_INCL2}/" >>$MYSQL_MY_CNF

EXPOSE 3306
VOLUME ["$MYSQL_DEF_DATA","$MYSQL_DEF_LOG","$MYSQL_CUST_INCL1","$MYSQL_CUST_INCL2"]
ENTRYPOINT ["/docker-entrypoint.sh"]

docker-entrypoint.sh

docker-entrypoint.sh内容如下,主要作用是启动Mysql,且进行初始化。此脚本由国外大神cytopia制作,实现了shell的debug模式。值得认真学习。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
[root@host mysql]# cat docker-entrypoint.sh 
#!/bin/bash

run() {
_cmd="${1}"
_debug="0"

_red="\033[0;31m"
_green="\033[0;32m"
_reset="\033[0m"
_user="$(whoami)"

# If 2nd argument is set and enabled, allow debug command
if [ "${#}" = "2" ]; then
if [ "${2}" = "1" ]; then
_debug="1"
fi
fi

if [ "${DEBUG_COMMANDS}" = "1" ] || [ "${_debug}" = "1" ]; then
printf "${_red}%s \$ ${_green}${_cmd}${_reset}\n" "${_user}"
fi
sh -c "LANG=C LC_ALL=C ${_cmd}"
}

log() {
_lvl="${1}"
_msg="${2}"

_clr_ok="\033[0;32m" #绿
_clr_info="\033[0;34m" #蓝
_clr_warn="\033[0;33m" #黄
_clr_err="\033[0;31m" #红
_clr_rst="\033[0m" #结束标记

if [ "${_lvl}" = "ok" ]; then
printf "${_clr_ok}[OK] %s${_clr_rst}\n" "${_msg}"
elif [ "${_lvl}" = "info" ]; then
printf "${_clr_info}[INFO] %s${_clr_rst}\n" "${_msg}"
elif [ "${_lvl}" = "warn" ]; then
printf "${_clr_warn}[WARN] %s${_clr_rst}\n" "${_msg}" 1>&2 # stdout -> stderr
elif [ "${_lvl}" = "err" ]; then
printf "${_clr_err}[ERR] %s${_clr_rst}\n" "${_msg}" 1>&2 # stdout -> stderr
else
printf "${_clr_err}[???] %s${_clr_rst}\n" "${_msg}" 1>&2 # stdout -> stderr
fi
}

get_mysql_default_config() {
_key="${1}"
mysqld --verbose --help 2>/dev/null | awk -v key="${_key}" '$1 == key { print $2; exit }'
}

if [ "$MYSQL_ROOT_PASSWORD" = "**Random**" ]; then
export MYSQL_ROOT_PASSWORD=`cat /dev/urandom | tr -dc A-Z-a-z-0-9 | head -c${1:-16}`
fi

if [ "$MYSQL_SOCKET_DIR"X = ""X ]; then
export MYSQL_SOCKET_DIR=`get_mysql_default_config socket`
fi

if [ "$DB_DATA_DIR"X = ""X ]; then
export DB_DATA_DIR="$( get_mysql_default_config "datadir" )"
fi

if [ -d "${DB_DATA_DIR}/mysql" ] && [ "$( ls -A "${DB_DATA_DIR}/mysql" )" ]; then
log "info" "Found existing data directory. MySQL already setup."
else
log "info" "No existing MySQL data directory found. Setting up MySQL for the first time."
# Create datadir if not exist yet
if [ ! -d "${DB_DATA_DIR}" ]; then
log "info" "Creating empty data directory in: ${DB_DATA_DIR}."
run "mkdir -p ${DB_DATA_DIR}"
run "chown -R ${MY_USER}:${MY_GROUP} ${DB_DATA_DIR}"
run "chmod 0777 ${MY_USER}:${MY_GROUP} ${DB_DATA_DIR}"
fi
#initialize no password
run "mysqld --initialize-insecure --datadir=${DB_DATA_DIR} --user=${MYSQL_USER}"
run "mysqld --skip-networking &"
for i in `seq 1 60`;do
if echo 'SELECT 1' | mysql --protocol=socket -uroot > /dev/null 2>&1; then
break
fi
log "info" "Initializing ..."
sleep 1s
i=$(( i + 1 ))
done
pid="$(pgrep mysqld | head -1)"
if [ "${pid}" = "" ]; then
log "err" "Could not find running MySQL PID."
log "err" "MySQL init process failed."
exit 1
fi

# Bootstrap MySQL
log "info" "Setting up root user permissions."
echo "DELETE FROM mysql.user ;" | mysql --protocol=socket -uroot
echo "CREATE USER 'root'@'%' IDENTIFIED BY '${MYSQL_ROOT_PASSWORD}' ;" | mysql --protocol=socket -uroot
echo "GRANT ALL ON *.* TO 'root'@'%' WITH GRANT OPTION ;" | mysql --protocol=socket -uroot
echo "DROP DATABASE IF EXISTS test ;" | mysql --protocol=socket -uroot
echo "FLUSH PRIVILEGES ;" | mysql --protocol=socket -uroot

log "info" "Shutting down MySQL."
run "kill -s TERM ${pid}"
sleep 5
if pgrep mysqld >/dev/null 2>&1; then
log "err" "Unable to shutdown MySQL server."
log "err" "MySQL init process failed."
exit 1
fi
log "info" "MySQL successfully installed."
fi

log "info" "Starting $(mysqld --version)"
log "info" "MYSQL Config: /etc/my.cnf"
log "info" "MYSQL Password: $MYSQL_ROOT_PASSWORD"
log "info" "MYSQL Data_db_dir: $DB_DATA_DIR"
log "info" "Auther: https://github.com/fangdm/docker-mysql"
exec mysqld

运行之后,使用docker logs mysql就可以看出mysql输出的密码。如果想自定义密码,可以使用-e MYSQL_ROOT_PASSWORD=password来实现。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
[root@host mysql]# docker logs mysql 
[INFO] No existing MySQL data directory found. Setting up MySQL for the first time.
[INFO] Initializing ...
[INFO] Setting up root user permissions.
[INFO] Shutting down MySQL.
[INFO] MySQL successfully installed.
[INFO] Starting mysqld Ver 5.7.25 for Linux on x86_64 (MySQL Community Server (GPL))
[INFO] MYSQL Config: /etc/my.cnf
[INFO] MYSQL Password: fVKtzGsS06fQ3o9c
[INFO] MYSQL Data_db_dir: /var/lib/mysql/
[INFO] Auther: https://github.com/fangdm/docker-mysql
[root@host mysql]# docker exec -it mysql mysql -uroot -pfVKtzGsS06fQ3o9c -e "show databases"
mysql: [Warning] Using a password on the command line interface can be insecure.
+--------------------+
| Database |
+--------------------+
| information_schema |
| mysql |
| performance_schema |
| sys |
+--------------------+
  • 本文作者: wumingx
  • 本文链接: https://www.wumingx.com/k8s/docker-Dockerfile.html
  • 本文主题: docker入门教程四:Dockerfile制作镜像与指令详解
  • 版权声明: 本站所有文章除特别声明外,转载请注明出处!如有侵权,请联系我删除。
0%