img
月光下有两个影子,一个是我的,另一个也是我的。

Dockerfile概念

Dockerfile就是一个文本文件,它是由一条一条指令构成的,每一条指令构建一层,所以,每一条指令都是在描述该层是如何构建的。

比如我这里定制一个nginx镜像

FROM nginx
RUN echo '<h1>Hello, world</h1>' > /usr/share/nginx/html/index.html

这个dockerfile涉及的指令只有两个,FROMRUN

FROM 指定基础镜像

既然要定制镜像,那么必须得以一个基础镜像为基础,然后进行雕琢,美化。像上面的哪个例子,就是以nginx为基础来创建的镜像。所以在Dockerfile中FROM必须是第一条指令,且必须存在。

在docker hub上有很多官方镜像

  • 服务类: nginx、redis、mongo、mysql、https、php、tomcat等等
  • 语言应用类: python、ruby、golang、openjdk、node等等

除去这些之外,如果还是没有找到你想要的镜像,官方还提供了一些更加基础的操作系统镜像。如: Ubuntu、debian、centos、fedora、apline等等

RUN 执行命令

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

  • shell格式:RUN <命令>,就像是直接在命令行输入的命令一样,上面的例子中的RUN就是shell格式。
  • exec格式:RUN ["可执行文件", "参数一", "参数二"],这更像是函数调用中的格式。

比如:

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 -zxf redis.tar.gz -C /usr/src/redis --strip-components=1
RUN make -C /usr/src/redis
RUN make -C /usr/src/redis install

每一个指令都会建立一层,RUN也是,每次RUN都会建立一层,就像上面这种写法,是建立了七层。结果就是产生很多臃肿的镜像,毫无意义,比如在运行时不需要的内容,像编译环境,更新软件包等。这些增加构建层数,只会增加构建部署镜像的时间,且容易出错。

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

FROM debian:stretch

RUN 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

/是命令换行方式,&&命令连接,将前后命令串联起来,#注释符,一般在文件的首行进行编写,其它地方不再进行注释,否则可能会影响流程运行。

这里最后还清理了一下缓存apt-get purge -y --auto-remove $buildDeps,清理掉无关的东西。

COPY 复制文件

格式

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

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

COPY指令将从旁那个构建上下文的目录中<源路径>的文件/目录复制到新的一层镜像中的<目标路径>的位置。如:

COPY flag.php /var/www/html/

源路径可以是很多个,甚至可以是通配符。

COPY hom* /mydir/
COPY hom?.txt /mydir/

目标路径可以是容器内的绝对路径,也可以是相对工作目录的相对路径,工作目录可以使用WORDDIR来指定,目标目录不需要事先创建,如果不存在会在复制文件之前自动创建缺失目录

使用COPY指令,源文件的各种元数据都会保留。比如读写执行权限,文件变更时间,这个特性对于镜像指定很有用。尤其是在使用Git进行管理的时候。

在使用该指令的时候,还可以加上--chown=<user>:<group>选项来改变文件的所属用户及用户组

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

ADD 更高级的复制文件

这个也是复制的,它与COPY不同的一点是,它可以自动解压。并且它的源路径可以是一个url,如果源路径是个url的话,那么ADD指令就会调用Docker引擎去下载该链接的文件,然后将它放入目标路径中。

需要注意的是下载后的文件权限默认设置为600,如果不想要这个权限可以再加一层RUN指令进行修改权限。

如果下载的是个压缩包,需要解压缩,也同样是需要再加一层RUN来进行解压缩,所以有时候不如直接是同RUN指令,调用wget或者curl工具下载,处理权限,解压缩,然后清理文件更加合理。所以这个命令一般不常用。

如果源路径为一个tar压缩文件的话,压缩格式为gzipbzip2xz的情况下,ADD指令就会自动解压缩这个压缩文件到目标路径中去。

FROM scratch
ADD ubuntu-xenial-core-cloudimg-amd64-root.tar.gz /
...

如果在某些情况下,我们不想解压缩,想直接复制压缩文件,这个时候就不能使用ADD命令了。

所以可以考虑,所有文件的复制均使用COPY指令来执行,仅在需要自动解压缩的时候再使用ADD

CMD 容器启动命令

CMD指令的格式与RUN指令相似,同样为两种格式

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

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格式
CMD echo $HOME   ===   CMD [ "sh", "-c", "echo $HOME" ]

在编写指令nginx服务开启命令时,需要使用命令service nginx start

但是直接使用

CMD service nginx start

会出现一个问题,就是会被进程理解为

CMD [ "sh", "-c", "service nginx start" ]

这样的命令结束后,sh进程也就结束了,主进程结束推出,那么容器自然也就退出了。而且在容器中使用systemctl命令的话,会发现无法执行,正确的方法应该是

CMD [ "nginx", "-g", "daemon off;" ]

ENV 设置环境变量

格式有两种

ENV <key> <value>
ENV <key1>=<value1> <key2>=<value2>...

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

ENV VERSION=1.0 DEBUG=on \
	NAME="Happy m0re"

比如在node镜像的dockerfile

ENV NODE_VERSION 7.2.0

RUN curl -SLO "https://nodejs.org/dist/v$NODE_VERSION/node-v$NODE_VERSION-linux-x64.tar.xz" \
  && curl -SLO "https://nodejs.org/dist/v$NODE_VERSION/SHASUMS256.txt.asc" \
  && gpg --batch --decrypt --output SHASUMS256.txt SHASUMS256.txt.asc \
  && grep " node-v$NODE_VERSION-linux-x64.tar.xz\$" SHASUMS256.txt | sha256sum -c - \
  && tar -xJf "node-v$NODE_VERSION-linux-x64.tar.xz" -C /usr/local --strip-components=1 \
  && rm "node-v$NODE_VERSION-linux-x64.tar.xz" SHASUMS256.txt.asc SHASUMS256.txt \
  && ln -s /usr/local/bin/node /usr/local/bin/nodejs

一开始设置了node的版本,下面直接使用$NODE_VERSION代表的就是7.2.0,这样做的好处就是更新的话,直接更改环境变量中定义的版本信息,也就是7.2.0即可。

CTF出题docker编写

FROM nickistre/centos-lamp:latest

COPY src/ /var/www/html/

RUN /etc/init.d/mysqld start \
    && mysqladmin -uroot password 'ctfhub' \
    && mysql -e "CREATE DATABASE ctfhub DEFAULT CHARACTER SET utf8;" -uroot -pctfhub \
    && mysql -e "use ctfhub;source /var/www/html/ctfhub.sql;" -uroot -pctfhub \
    && rm /var/www/html/ctfhub.sql \
    && rm /var/www/html/phpinfo.php \
    && chmod -R 655 /var/www/html/

解析:

首先目录

- src
	- index.php
	- ctfhub.sql
- Dockerfile

拉去一个lamp镜像,然后将src目录下的文件(ctfrhub.sql&&index.php)复制到容器中的网站根目录下

随后开启mysql服务,进入数据库,使用mysqladmin来设置初始用户和密码root:ctfhub

然后使用命令创建对应数据库ctfhub并且导入数据库文件ctfhub.sql

然后删除数据库文件ctfhub.sql防止信息泄露,删除phpinfo.php防止php探针泄露危险可利用函数

最后赋予网站根目录对应权限为655,题目就部署完成了。