Dockerfile详解

一、初识Dockerfile

1.1 什么是Dockerfile

"Dockerfile"是一个用来构建镜像的文本文件,其内包含了一条条的指令(Instruction),每一条指令构建一层,因此每一条指令的内容,就是描述该层应当如何构建

1.2 什么是定制镜像

镜像的定制实际上就是定制每一层所添加的配置、文件

# 如果我们可以把每一层修改、安装、构建、操作的命令都写入一个脚本,用这个脚本来构建、定制镜像,那么之前体积的无法重复的问题、镜像构建透明性的问题、体积的问题就都会解决,这个脚本就是 Dockerfil

二、Dockerfile指令详解

2.1 基本条件

# 1. 新建一个空白目录,建立一个文本文件,并命名为Dockerfile
[root@docker ~]# mkdir mynginx
[root@docker ~]# cd mynginx/
[root@docker mynginx]# touch Dockerfile

# 2. 更改Dockerfile内容
[root@docker mynginx]# cat Dockerfile 
FROM nginx
RUN echo "hello,docker!" > /usr/share/nginx/html/index.html


'这个Dockerfile一共只有两行,涉及到了两条指令, FROM和RUN'

2.2 FROM指定基础镜像

	所谓定制镜像,那一定是以一个镜像为基础,在其上进行定制。就像我们之前运行了一个nginx镜像的容器,再进行修改一样,基础镜像是必须指定的。而"FROM就是指定 基础镜像" ,因此一个Dockerfile中FROM是必备的指定,"并且必须是第一条指令".
	
我们除了选择现有镜像为基础镜像外,Docker还存在一个 特殊的镜像,名为"scratch"。这个镜像是虚拟概念,并不实际存在,他表示一个"空白的镜像"

# FROM指定基础镜像的格式如下
FROM <镜像名>			# 如 (nginx,centos:7......)

2.3 RUN执行命令

RUN指定是用来执行命令行命令的。由于命令行的强大能力,RUN指令在定制镜像时是最常用的指令之一。其格式有以下两种:

1) shell格式

# shell格式如下
RUN <命令>		# 就像直接在命令行中输入的命令一样。刚才写的Dockerfile中的RUN指令就是这种格式
RUN echo "hello,docker!" > /usr/share/nginx/html/index.html

2) exec格式

# exec格式如下
RUN ["可执行文件","参数1","参数2"]	# 这更像是函数调用中的格式

3) 正确使用RUN指令的方法

'由于Dockerfile中每一个指令都会建立一层,RUN也不例外,每一个RUN的行为就会新建立一层,在其上执行这些命令,执行结束后,commit这一层的修改,构成新的镜像'

Union FS 是有最大层数限制的,比如 AUFS,曾经是最大不得超过 42 层,现在是不得超过 127 层。

# 什么是Union FS??
	Union FS即联合文件系统,它可以把多个目录(分支)内容联合挂载到同一个目录下,而目录的物理位置是分开的
	
# 假如说我们要构建一个redis,我们正常思路该怎么写?(虽说我不会,但我会抄)
FROM debian:stretch
RUN apt-get update
RUN apt-get install -y gcc libc6-dev make wget
RUN wget -O redis.tar.gz "http://download.redis.io/releases/redis-5.0.3.tar.gz"
RUN mkdir -p /usr/src/redis
RUN tar -xzf redis.tar.gz -C /usr/src/redis --strip-components=1
RUN make -C /usr/src/redis
RUN make -C /usr/src/redis install


# 但是如果我们考虑到docker的用途就是轻量快捷,此时如果多加几层镜像就脱离了初衷,所以该用如下方法
FROM debian:stretch
RUN set -x; buildDeps='gcc libc6-dev make wget' \
    && apt-get update \
    && apt-get install -y $buildDeps \
    && wget -O redis.tar.gz "http://download.redis.io/releases/redis-5.0.3.tar.gz" \
    && mkdir -p /usr/src/redis \
    && tar -xzf redis.tar.gz -C /usr/src/redis --strip-components=1 \
    && make -C /usr/src/redis \
    && make -C /usr/src/redis install \
    && rm -rf /var/lib/apt/lists/* \
    && rm redis.tar.gz \
    && rm -r /usr/src/redis \
    && apt-get purge -y --auto-remove $buildDeps
    
'首先,之前所有的命令只有一个目的,就是编译、安装redis可执行文件。因此没有必要建立很多层,这只是一层的事情。因此,这里没有使用很多个RUN---对应不同的命令,而是仅仅使用一个RUN指令,并使用&& 将各个所需命令串联起来。将之前的七层,简化为了一层。'

'此外,我们可以看到这一组命令的最后添加了清理工作的命令,删除了为了编译构建所需要的软件,清理了所有下载、展开的文件,并且还清理了apt缓存文件。这是很重要的一步,因为镜像时多层存储,每一层的东西并不会在下一层被删除,会一直跟随者镜像。因此镜像构建时,一定要确保每一层只添加真正需要添加的东西,任何无关的东西应该清理掉'

2.4 构建镜像

'接着我们的简单定制nginx镜像'

# 1. 在Dockerfile文件所在目录执行
[root@docker mynginx]# docker build -t nginx:v1 .
Sending build context to Docker daemon  2.048kB
Step 1/2 : FROM nginx
latest: Pulling from library/nginx
b380bbd43752: Pull complete 
fca7e12d1754: Pull complete 
... ...
858292fd2e56: Pull complete 
Digest: sha256:644a70516a26004c97d0d85c7fe1d0c3a67ea8ab7ddf4aff193d9f301670cf36
Status: Downloaded newer image for nginx:latest
 ---> 87a94228f133
Step 2/2 : RUN echo "hello,docker!" > /usr/share/nginx/html/index.html
 ---> Running in ebad8c4b482d
Removing intermediate container ebad8c4b482d
 ---> 969d6e5f47df
Successfully built 969d6e5f47df
Successfully tagged nginx:v1

# 2. Docker执行Dockerfile的大致流程
	(1)	docker从基础镜像运行一个容器
	(2) 执行一条指令并对容器做出修改
	(3) 执行类似docker commit的操作提交一个新的镜像层
	(4) docker再基于刚提交的镜像运行一个新容器
	(5) 执行Dockerfile中的下一条指令直到所有指令都执行完成
	
# 3. 我们上面构建镜像了使用了命令  docker build ;格式如下
'在这里我们指定了最终镜像的名称 -t nginx:v1,构建成功后,我们可以像之前运行nginx那样来运行这个镜像'
docker build [选项] <上下文路径/URL/->

# 4. 使用-f参数指定某个文件作为Dockerfile
[root@docker docker]# docker build -f Dockerfile.html -t nginx:v2 .

2.5 镜像构建上下文(Context)

	# 如果注意,我们会看到 docker build命令最后有一个"." 。此个"."表示当前目录,而Dockerfile就在当前目录,但是我们不能以为这个路径就是在指定Dockerfile所在路径,如果对应上面的命令格式,其实这就是在指定---->上下文路径
	
# 1. 什么是上下文呢?
	首先我们要理解docker build的工作原理。Docker在运行时分为Docker引擎(也就是服务端守护进程)和客户端工具。
	Docker的引擎提供了一组REST API,被称为Docker Remote API,而如Docker命令这样的客户端工具,则是通过这组API与Docker引擎交互,从而完成各种功能。因此,虽然表面上我们好像是在本机执行各种docker功能,但实际上,一切都是使用的远程调用形式在服务端(docker引擎)完成。也因为这种C/S设计,让我们操作远程服务器的Docker引擎变得轻而易举。
	
	当我们进行镜像构建的时候,并非所有定制都会通过RUN指令完成,经常会需要将一些本地文件复制进镜像,比如通过COPY指令、ADD指令等。而docker build命令构建镜像,其实并非在本地构建,而是在服务端,也就是Docker引擎中构建的。那么在这种客户端/服务端的架构中,如何才能让服务端获得本地文件?
	
	这就引入了上下文的概念。当构建的时候,用户会指定构建镜像上下文的路径,docker build命令得知这个路径后,会将路径下的所有内容打包,然后上传给Docker引擎。这样Docker 引擎收到这个上下文包后,展开就会获得构建镜像所需的一切文件。
	
# 2. 如果在Dockerfile中这么写:
COPY ./package.json /app/

'首先说明,这并不是要复制执行docker build 命令所在的目录下的package.json,也不是复制Dockerfile所在目录下的package.json,而是复制上下文(context)目录下的package.json'

因此,COPY这类指令中的源文件的路径都是相对路径,之所有会出现 COPY ../package.json 或者 COPY /opt/xxx /app 无法工作的原因,因为这些路径已经超出了上下文的范围,Docker 引擎无法获得这些位置的文件。如果真的需要那些文件,应该将他们复制到上下文目录中去。

现在就可以理解刚才的命令 docker build -t nginx:v3 . 中的这个 ".",实际上是在指定上下文的目录,docker build命令会将该目录下的内容打包交给Docker引擎以帮助构建镜像

'还有一点需要注意,我们不能将Dockerfile放到硬盘根目录去构建,因为 docker build执行后,会发送几十GB的东西给Docker引擎,极为缓慢而且很容易构建失败,因为这种做法是让docker  build打包整个硬盘'

# 一般来说,应该会将Dockerfile置于一个空目录下,或者项目根目录下。如果该目录下没有所需文件,那么应该吧所需文件复制一份过来。如果目录下有些东西确实不希望构建时传给Docker引擎,那么可以用.gitignore一样的语法写一个.dockerignore,该文件是用于剔除不需要作为上下文传递给Docker引擎的


# 3. 理解docker build构建镜像的工作流程
	(1) 执行docker build -t <imagename:imagetag> .;
	(2) Docker客户端会将构建命令后面指定的路径"."下的所有文件打包成一个tar包,发送给Docker服务端
	(3) Docker服务端收到客户端发送的tar包,然后解压,根据Dockerfile里面的指令进行分层构建

2.6 其他docker build的用法

1) 用给定的tar压缩包构建

$ docker build http://server/context.tar.gz

'如果锁给出的URL不是个GIT repo,而是个tar 压缩包,那么Docker引擎会下载这个包,并自动解压缩,以其作为上下文,开始构建'

2) 从标准输入中读取Dockerfile进行构建

docker build - < Dockerfile
'or'
cat Dockerfile | docker build -

# 如果标准输入传入的是文本文件,则将其视为Dockerfile,并开始构建。这种形式由于直接从标准输入中读取Dockerfile的内容,它没有上下文,因此不可以像其他方法那样可以将本地文件COPY进镜像之类的事情

3) 从标准输入中读取上下文压缩包进行构建

$ docker build - < context.tar.gz

# 如果发现标准输入的文件格式是gzip、bzip2以及xz的话,将会使其为上下文压缩包,直接将其展开,将里面视为上下文,并开始构建。

2.7 COPY复制文件

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

和RUN指令一样,也有两种格式,一种类似于命令行,一种类似于函数调用。

# COPY指令将从构建上下文目录中<源路径>的文件/目录复制到新的一层的镜像内的<目标路径>位置,如下所示
COPY package.json /usr/src/app/

'其中,源路径可以是多个,甚至可以是通配符,其通配符规则要满足Go语言的filepath.Match规则'
'目标路径可以是容器内的绝对路径,也可以是相对于工作目录的相对路径(工作目录可以用WORKDIR指令来制定)。目标路径不需要事先创建,如果目录不存在会在复制文件前先行创建缺失目录'


# 此外还需要注意一点,使用COPY指令,源文件的各种元数据都会保留。比如读、写、执行权限、文件变更时间等。这个特性对于镜像定制很有用。特别是构建相关文件都在使用git进行管理的时候

COPY --chown=55:mygroup files* /mydir/
COPY --chown=bin files* /mydir/
COPY --chown=1 files* /mydir/
COPY --chown=10:11 files* /mydir/

'如果源文件为文件夹,复制的时候不是直接复制该文件夹,而是将文件夹中的内容复制到目标路径'

2.8 ADD更高级的复制文件

# ADD 指令和 COPY 的格式和性质基本一致,但是在COPY的基础上增加了一些功能,比如<源路径>可以是一个URL,在这种情况下,Docker引擎会试图去下载这个链接的文件放到<目标路径>去。下载后的文件权限自动设置为600,如果这并不是想要的权限,那么还需要增加额外的一层RUN进行权限调整
'另外,如果下载的是个压缩包。需要解压缩,也一样还需要额外的一层RUN指令进行解压缩。所以不如直接使用RUN指令,然后使用wget或者curl工具下载,处理权限、解压缩、然后清理无用文件更合理'

'需要注意一点,ADD指令会令镜像构建缓存失效,从而可能会令镜像构建变得比较缓慢'
因此在COPY和ADD指令中选择的时候,可以遵循这样的原则,所有的文件复制均使用COPY指令,仅在需要自动解压缩的场合使用ADD

# 在使用该指令的时候还可以加上 --chown=<user>:<group>选项来改变文件的所属用户及所属组
ADD --chown=55:mygroup files* /mydir/
ADD --chown=bin files* /mydir/
ADD --chown=1 files* /mydir/
ADD --chown=10:11 files* /mydir/

2.9 CMD容器启动命令

CMD指令的格式和RUN相似,也是两种格式

# shell格式:	CMD <命令>
# exec格式:	CMD ["可执行文件","参数1","参数2"...]
# 参数列表格式:	CMD ["参数1","参数2"...]。 在指定了ENTRYPOINT指令后,用CMD指定具体的参数

------------------------'CMD'------------------------
之前介绍容器的时候就讲过,Docker不是虚拟机,容器就是进程。既然是进程,那么在启动容器的时候,需要指定所运行的程序及参数。CMD指令就是用于指定默认的容器主进程的启动命令的。

在运行时可以指定新的命令来替代镜像设置中的这个默认命令,比如,ubuntu镜像默认的CMD是"/bin/bash",如果我们直接" docker run -it ubuntu" 的话,会直接进入bash。我们也可以在运行时指定别的命令,如 "docker run -it ubuntu cat /etc/os-release".这就是用"cat /etc/os-release"命令替换了默认的/bin/bash命令

# 在指令格式上,一般推荐使用exec格式,这类格式在解析时会被解析为JSON数组,因此一定要使用双引号,而不是使用单引号


------------------------'shell'------------------------
如果使用shell格式的话,实际的命令会被包装成 sh -c的参数形式进行执行。比如:
CMD echo $HOME

# 在实际执行中,会将其变成
CMD [ "sh","-c","echo $HOME"]

这就是我们为什么可以使用环境变量的原因,因为这些环境变量会被shell进行解析处理。。


------------------------'前台执行and后台执行'------------------------

'Dokcer不是虚拟机,容器中的应用都应该以前台执行,而不是像虚拟机、物理机里面那样,用systemd去启动后台服务,容器里面没有后台服务的概念'

对于容器而言,其启动程序就是容器应用程序,容器就是为了主进程而存在的,主进程退出,容器就失去了存在的意义,从而退出,其他辅助进程不是它需要关心的问题

所以当我们启动nginx服务的时候不能以传统的systemd进行启动
# 正确的做法是直接执行nginx可执行文件,并且要求以前台形式运行。比如:
CMD ["nginx","-g","daemon off;"]

2.10 ENTRYPOINT 入口点

1. ENTRYPOINT作为启动命令是无法被docker run覆盖(如果docker run指定命令,会被认为ENTRYPOINT的参数)

2. 如果ENTRYPOINT和CMD指令同时存在,则CMD的相关内容会被设置成ENTRYPOINT的参数

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

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

'当指定了 ENTRYPOINT后,CMD的含义就发生了改变,不再是直接的运行其命令,而是将CMD的内容作为参数传给了ENTRYPOINT指令,将变为'
<ENTRYPOINT> "<CMD>"


'ENTRYPOINT使用场景'
--------------------------"场景1: 让镜像变成像命令一样使用"--------------------------

[root@docker docker]# vim Dockerfile 
FROM ubuntu:18.04
RUN apt-get update \
    && apt-get install -y curl \
    && rm -rf /var/lib/apt/lists/*
ENTRYPOINT [ "curl", "-s", "http://myip.ipip.net" ]

'我们之前使用CMD的时候,会发现如果运行容器的时候加上 -i 参数会报错,主要是因为跟在镜像名后面的是命令,运行时会替换CMD的默认值。因此这里的 -i 是替换了原来的CMD,而 -i 根本不是命令,所以自然找不到   现在重新尝试一下'
[root@docker docker]# docker run myip:v1 -i
docker: Error response from daemon: OCI runtime create failed: container_linux.go:380: starting container process caused: exec: "-i": executable file not found in $PATH: unknown.
[root@docker docker]# docker run myip:v2 -i
HTTP/1.1 200 OK
Date: Wed, 27 Oct 2021 02:00:18 GMT
Content-Type: text/plain; charset=utf-8
Content-Length: 68
Connection: keep-alive
X-Via-JSL: 2633f0d,-
Set-Cookie: __jsluid_h=9156e56ea90a6ec350af55d5f53bdcd2; max-age=31536000; path=/; HttpOnly
X-Cache: bypass

当前 IP:58.246.15.250  来自于:中国 上海 上海  联通

# 可以看到,这次加上-i参数执行成功了,因为当存在ENTRYPOINT后,CMD的内容将会作为参数传给ENTRYPOINT,而这里的-i就是新的CMD,因此会作为参数传给curl,从而达到我们预期的效果


--------------------------"场景2: 应用运行前的准备工作"--------------------------

启动容器就是启动主进程,但有些时候,启动主进程前,需要一些准备工作

比如mysql类的数据库,可能需要一些数据库配置、初始化的工作,这些工作要在最总的mysql服务器运行之前解决,

此外,可能希望避免使用root用户去启动服务,从而提高安全性,而在启动服务前还需要以root身份执行一些必要的准备工作,最后切换到服务用户身份启动服务。或者除了服务外,其他命令以及可以使用root身份运行,方便调试等

2.11 ENV设置环境变量

# 格式依旧有两种
1. ENV <key> <value>
2. ENV <key1>=<value1> <key2>=<value2>...

这个指令很简单,就是设置环境变量而已,无论是后面的其他指令,如RUN,还是运行时的应用,都可以直接使用这里定义的环境变量

2.12 ARG构建参数

# 格式如下
ARG <参数名>[=<默认值>]

构建参数和ENV的效果一样,都是设置环境变量。所不同的是,ARG所设置的构建环境的环境变量,在将来容器运行时是不会存在这些环境变量的。但是不要因此就是用ARG保存密码之类的信息,因为docker history还是可以看到所有值的。

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

'ARG指令有生效范围,如果在FROM指令之前指定,那么只能用于FROM指令'

'对于多阶段构建,ARG如果在FROM指令之前指定,那么这个变量在每个FROM中都生效'

2.13 VOLUEME定义匿名卷

# 格式如下
1. VOLUME ["<路径1>","<路径2>"...]
2. VOLUME <路径>

之前我们说过,容器运行时应该尽量保持容器存储层不发生写操作,对于数据库类需要保存动态数据的应用,其数据库文件应该保存于卷(volume)中。 在Dockerfile中,我们可以实现指定某些目录挂载为匿名卷,这样在运行时,如果用户不指定挂载,其应用也可以正常运行,不会像容器存储层写入大量数据


VOLUME /data
# 这里的/data目录就会在容器运行时自动挂载为匿名卷,任何向/data中写入的信息都不会记录进容器存储层,从而保证了容器存储层的无状态变化。当然,运行容器时可以覆盖这个挂载设置。比如

docker run -d -v mydata:/data xxxx
# 在这行命令中,就是用了mydata这个命名卷挂载到了/data这个位置替代了Dockerfile中定义的匿名卷的挂载配置

2.14 EXPOSE 暴露端口

# 格式如下
EXPOSE <端口1> [<端口2>...]

EXPOSE指令是声明容器运行时提供服务的端口,这只是一个声明,在容器运行时并不会因为这个声明应用就会开启这个端口的服务。
在Dockerfile中写入这样的声明有两个好处

1. 帮助镜像使用者理解这个镜像服务的守护端口,以方便配置映射
2. 运行时使用随机端口映射时,也就是 docker run -P时,会自动随机映射EXPOSE的端口

# 要将EXPOSE和运行时使用 -p <宿主端口>:<容器端口> 区分开来。
# -p,是映射宿主端口和容器端口,换句话说,就是将容器的对应端口公开给外界访问
# 而EXPOSE仅仅是声明容器打算使用什么端口而已,并不会自动在宿主进行端口映射

2.15 WORKDIR 指定工作目录

# 格式如下
WORKDIR <工作目录路径>

使用WORKDIR指令可以指定工作目录(或称之为当前目录),以后各层的当前目录就被改为指定目录,如该目录不存在,WORKDIR会帮你建立目录


'可能出现的错误'
vim Dockerfile
RUN cd /app
RUN echo "hello" > world.txt

# 如果将上面的Dockerfile进行构建镜像运行后,会发现找不到 /app/world.txt 文件,或者其内容不是hello。
# 原因其实很简单,在shell中,连续两行是同一个进程执行环境,因此前一个命令修改的内存状态会直接影响后一个命令;
# 而在Dockerfile中,这两行RUN命令的执行环境根本不同,是两个完全不同的容器。这就是对Dockerfile构建分层存储的概念不了解所导致错误

之前说过每一个RUN都是启动一个容器、执行命令、然后提交存储层文件变更。第一层RUN cd /app的执行仅仅是当前进程的工作目录变更,一个内存上的变化而已,其结果不会造成任何文件变更。而到第二层的时候,启动的是一个全新的容器,跟第一层的容器更完全没关系,自然不可能继承前一层构建过程中的内存变化

'因此,如果需要改变以后各层的工作目录的位置,那么应该使用WORKDIR指令'
[root@docker docker]# cat Dockerfile 
FROM ubuntu:18.04

WORKDIR /root

RUN echo "hello" > world.txt


# 如果你的WORKDIR指令使用的相对路径,那么所切换的路径与之前的WORKDIR有关
WORKDIR /a
WORKDIR b
WORKDIR c
RUN pwd			# RUN pwd的工作目录就为/a/b/c

2.16 USER 指定当前用户

# 格式如下
USER <用户名>[:<用户组>]

USER指令和WORKDIR相似,都是改变环境状态并影响以后的层
# WORKDIR是改变工作目录,USER则是改变之后 层的执行RUN,CMD以及ENTRYPOINT这类命令的身份

'需要注意得是,USER只是帮助你切换到指定用户而已,这个用户必须是事先建立好的,否则无法切换'

2.17 ONBUILD 触发器

# 格式如下
ONBUILD <其他指令>

ONBUILD是一个特殊的指令,它后面跟的是其他指令,比如RUN,COPY等,而这些指令,在当前镜像构建时并不会被执行。只有当以当前镜像为基础镜像,去构建下一级镜像的时候才会被执行

# Dockerfile中的其他指令都是为了定制当前镜像而准备的,唯有ONBUILD是为了帮助别人定制自己而准备的

三、使用Dockerfile构建项目

3.1 编辑MySQL的Dockerfile

# 1. 新建MySQL目录并创建Dockerfile
[root@docker docker]# mkdir mysql
[root@docker docker]# cd mysql/
[root@docker mysql]# touch Dockerfile 

# 2. 修改Dockerfile文件
[root@docker mysql]# vim Dockerfile 

FROM centos:7

ADD mysql-5.7.35-linux-glibc2.12-x86_64.tar.gz /usr/local/

RUN useradd -M -s /sbin/nologin -r mysql \
    && yum install -y ncurses-devel libaio-devel gcc gcc-c++ numactl libaio glibc cmake autoconf \
    && ln -s /usr/local/mysql-5.7.35-linux-glibc2.12-x86_64 /usr/local/mysql \
    && chown -R mysql.mysql /usr/local/mysql \
    && mkdir /mysql_data \
    && chown -R mysql.mysql /mysql_data \
    && /usr/local/mysql/bin/mysqld --initialize-insecure --user=mysql --basedir=/usr/local/mysql --datadir=/mysql_data

ADD my.cnf /etc/

WORKDIR /usr/local/mysql

EXPOSE 3306

CMD /usr/local/mysql/bin/mysqld --defaults-file=/etc/my.cnf

# 3. 加入需要的压缩包到/root/docker/mysql目录下
[root@docker ~]# mv mysql-5.7.35-linux-glibc2.12-x86_64.tar.gz docker/mysql/
[root@docker ~]# vim docker/mysql/my.cnf

[mysqld]
user=mysql
basedir=/usr/local/mysql
datadir=/mysql_data
port=3306
socket=/usr/local/mysql/mysql.sock
character-set-server=utf8
log-error=/mysql_data/mysqld.log
pid-file=/tmp/mysqld.pid
[mysql]
socket=/usr/local/mysql/mysql.sock
[client]
socket=/usr/local/mysql/mysql.sock

# 4. 创建网桥
[root@docker mysql]# docker network create discuz
3b0b4ac99a475274e3d01180da57821483777dafa596efcd6d662273b05a1e05

# 5. 由于我们生成的mysql容器没有可以用于远程的用户以及没有所需要的数据库,所以可以通过脚本来实现
'创建用户所需的脚本'
[root@docker mysql]# cat create_user.sh 
#! /bin/bash
# 进行循环
while true;do
# 测试登录mysql并执行sql语句,查看语句是否执行成功
    /usr/local/mysql/bin/mysql -uroot -p'' -e 'show databases;' &>/dev/null
# 如果执行成功,则再次判断使用我们所设置的远程用户是否可以进行登录并执行sql语句,如果可以执行成功则退出
    if [ $? -eq 0 ];then
        /usr/local/mysql/bin/mysql -uroot -p${MYSQL_ROOT_PASSWORD} -h${HOSTNAME} -e 'show databases;' &>/dev/null
        if [ $? -eq 0 ];then
	    exit
	    fi

# 如果用社会的远程用户无法登录,继续执行脚本执行sql语句进行创建远程用户,并创建数据库
	/usr/local/mysql/bin/mysql -uroot -p'' -e "GRANT ALL PRIVILEGES ON *.* TO 'root'@'%' IDENTIFIED BY '${MYSQL_ROOT_PASSWORD}' WITH GRANT OPTION;FLUSH PRIVILEGES;CREATE TABLE discuz;" &>/dev/null

	exit

    fi
    sleep 1;
done



[root@docker mysql]# cat start.sh 
#!/bin/bash

# 判断我们的变量是否为空,如果为空则输出提示信息并退出
if [ ! $MYSQL_ROOT_PASSWORD ];then
    echo "远程root密码必填!!!"
    exit;
fi

# 如果不为空,则在容器后台运行create_user.sh脚本
nohup ./create_user.sh &

# 然后前台启动mysql
/usr/local/mysql/bin/mysqld --defaults-file=/etc/my.cnf


# 6. 此时又需要修改一下Dockerfile里面的内容
'把Dockerfile里面的最后一个CMD指令进行修改'

[root@docker mysql]# cat Dockerfile
FROM centos:7

ADD mysql-5.7.35-linux-glibc2.12-x86_64.tar.gz /usr/local/ 

RUN useradd -M -s /sbin/nologin -r mysql \
    && yum install -y ncurses-devel libaio-devel gcc gcc-c++ numactl libaio glibc cmake autoconf \
    && ln -s /usr/local/mysql-5.7.35-linux-glibc2.12-x86_64 /usr/local/mysql \
    && chown -R mysql.mysql /usr/local/mysql/ \
    && mkdir /mysql_data \
    && chown -R mysql.mysql /mysql_data/ \
    && /usr/local/mysql/bin/mysqld --initialize-insecure --user=mysql --basedir=/usr/local/mysql --datadir=/mysql_data
    
ADD my.cnf /etc/

WORKDIR /usr/local/mysql

EXPOSE 3306

ADD start.sh /usr/local/mysql 

ADD create_user.sh /usr/local/mysql

CMD ./start.sh


# 7. 生成mysql镜像文件
[root@docker mysql]# docker build -t mysql:last .

# 8. 运行此mysql镜像
[root@docker mysql]# docker run -d -P --network discuz --name mysql -e MYSQL_ROOT_PASSWORD=123456 mysql:last
0b8fea48baa41f9357b536dc183594c1b59ad812c5ef71b042550cf482e461ed

# 9. 测试mysql服务器是否正常
[root@docker mysql]# docker exec -it mysql /usr/local/mysql/bin/mysql -uroot -p123456 -hmysql -e 'show databases;'
mysql: [Warning] Using a password on the command line interface can be insecure.
+--------------------+
| Database           |
+--------------------+
| information_schema |
| discuz             |
| mysql              |
| performance_schema |
| sys                |
+--------------------+

3.2 编辑nginx+php的Dockerfile

# 1. 复制代码文件到/root/docker/nginx
[root@docker opt]# cp -r upload/ /root/docker/nginx/


# 2. 编辑nginx官方源
[root@docker opt]# cd /root/docker/nginx/
[root@docker nginx]# cat nginx.repo 
[nginx-stable]
name=nginx stable repo
baseurl=http://nginx.org/packages/centos/7/$basearch/
gpgcheck=1
enabled=1
gpgkey=https://nginx.org/keys/nginx_signing.key
module_hotfixes=true

# 3. 编辑nginx的网站配置文件
[root@docker nginx]# cat default.conf 
server {

	server_name _;
	listen 80;
	
	root /usr/share/nginx/html;
	
	location / {
		index index.php ;
	}
	
	location ~* \.php$ {
		fastcgi_pass 127.0.0.1:9000;
		fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
		include fastcgi_params;
	}

}


# 4. 编辑Dockerfile
[root@docker nginx]# cat Dockerfile 
FROM centos:7

RUN useradd www -r -M -s /sbin/nologin

ADD nginx.repo /etc/yum.repos.d/

RUN yum -y install nginx \
    && sed -i 's#user nginx#user www#g' /etc/nginx/nginx.conf

ADD default.conf /etc/nginx/conf.d

RUN rpm -Uvh https://dl.fedoraproject.org/pub/epel/epel-release-latest-7.noarch.rpm \
    && rpm -Uvh https://mirror.webtatic.com/yum/el7/webtatic-release.rpm \
    && yum remove php-mysql-5.4 php php-fpm php-common \
    && yum -y install php71w php71w-cli php71w-common php71w-devel php71w-embedded php71w-gd php71w-mcrypt php71w-mbstring php71w-pdo php71w-xml php71w-fpm php71w-mysqlnd php71w-opcache php71w-pecl-memcached php71w-pecl-redis php71w-pecl-mongodb \
    && sed -i 's#apache#www#g' /etc/php-fpm.d/www.conf

EXPOSE 80 443

CMD php-fpm && nginx -g 'daemon off;'


# 5. 生成镜像文件
[root@docker nginx]# docker build -t nginx:v1 .

# 6. 生成容器
[root@docker nginx]# docker run -d --name nginx -v /root/docker/nginx/upload:/usr/share/nginx/html -P --network discuz nginx:v1

# 7. 最后通过docker ps 来查看生成的端口,并使用浏览器访问