[docker]3.dockerfile的使用以及通过maven插件构建Docker镜像

本文的目的

据我们所知,docker除了官方提供的镜像以外(通常都是一些基础环境的镜像,或者像WordPress这样著名的程序),也有很多自己的自定义的镜像,主要用来实现我们自己需要的定制需求。

Dockerfile就像是maven构建程序的pom.xml一样,通过定义一系列镜像(maven中的项目)所需要的条件,就可以轻松构建一个镜像(项目)。那么这篇文章就是为了掌握Dockerfile常规的几个语法,以便能够让我们通过定义的一系列指令构建出我们自己的定制镜像。

本文使用到的Java示例代码地址:https://github.com/WeidanLi/p-docker-demo

1. Java的HelloWorld

首先先来一个例子,我们编写一个最简单的Java程序HelloWorld,然后把这个HelloWorld打包进去Docker的jdk8镜像。尝试运行。

1.1 首先需要一个jdk8的容器

因为官方已经有了,所以直接从官方仓库里面拉取,然后使用Dockerfile进行镜像的构建。

通过docker search jdk8我找到了alljoynsville/oracle-jdk8这个镜像挺符合我的要求的,所以我把他拉取下来作为我后面项目所需要的环境。

docker pull alljoynsville/oracle-jdk8

1.2 开始构建

这时候在项目的根目录创建一个Dockerfile文件,开始编写我们的Dockerfile。

# 指定当前构建镜像的基础镜像
FROM alljoynsville/oracle-jdk8
# 镜像启动的时候需要执行的命令,其实这里也是镜像持续运行的命令,如运行一个Tomcat
ENTRYPOINT ["/usr/bin/java", "-cp", "/usr/share/app/service.jar", "cn.liweidan.docker.HelloWorld"]
# 构建镜像的时候,需要拷贝的jar包
ADD target/docker-java-hello-world-1.0.0-SNAPSHOT.jar /usr/share/app/service.jar

然后我们来到Dockerfile的目录,执行以下命令:

docker build -t my-java-hello-world .
Sending build context to Docker daemon  34.82kB
Step 1/4 : FROM alljoynsville/oracle-jdk8
 ---> de3413c1cdce
Step 2/4 : MAINTAINER WeidanLi <toweidan@126.com>
 ---> Using cache
 ---> a0c1f7b3ca12
Step 3/4 : ENTRYPOINT ["/usr/bin/java", "-cp", "/usr/share/app/service.jar", "cn.liweidan.docker.HelloWorld"]
 ---> Using cache
 ---> a0ef831ca2fc
Step 4/4 : ADD target/docker-java-hello-world-1.0.0-SNAPSHOT.jar /usr/share/app/service.jar
 ---> Using cache
 ---> de501901fb22
Successfully built de501901fb22
Successfully tagged my-java-hello-world:latest

看到控制台Successfully built XXX[images-id]字样表示镜像已经构建成功。

1.3 运行Docker镜像

➜  docker-java-hello-world docker run my-java-hello-world:latest
Hello World!

可以看到我们的镜像已经开始成功运行起来了。当然因为这个程序运行输出完就已经结束了,如果我们把他写成while(true){}的形式,那么就可以看到Docker容器像MySQL一样一直在进程中执行。

2. Dockerfile指令集

其实我不知道怎么才能够比较简单的传达怎么去写一个Dockerfile,像上面的例子已经是最简单的例子了,在项目中其实只要上面的指令也就够了,不过还是需要整理以下指令集,所以我整理出来一个表格,这些指令集都可以被添加到Dockerfile中,以便实现我们的需求。

2.1 Dockerfile的格式

Dockerfile必须以FROM指令初识,后面指定基础镜像。

Dockerfile中#开头的后面是注释。

2.2 常用指令

指令 作用
FROM 指定一个镜像的基础镜像,必须位于第一行,如FROM alljoynsville/oracle-jdk8
RUN 运行指令,在相对应的系统运行以后的结果作为Dockerfile中下一步的输入。
CMD 为容器提供默认运行的指令。RUN是在镜像构建的时候进行调用,而CMD则是容器运行时调用的命令。一个Dockerfile只能有一个CMD。
LABEL 指定容器的原信息,以便可以在docker inspect中查看
EXPOSE 用于指定暴露的接口,如果使用-P指令,Docker会为每个指定的端口进行随机映射。
ENV 用于指定环境参数,可以在运行的时候通过-e env1=value1进行覆盖,如果没有指定则使用默认Dockerfile指定的值
ADD 用于指定文件拷贝到镜像中,如果源路径是个url,则会下载指定文件到镜像中(我可以把小电影打包到Docker中?)
COPY 拷贝,同上,但不支持URL
ENTRYPOINT 配置容器运行的时候执行的指令,多用于调用命令运行我们的程序
VOLUME 映射文件卷,如果我们需要用到外部的文件或者生成的文件想防御外部系统,可以通过此参数指定
USER 指定以一个什么样的用户角色运行,如果没有指定,Linux下默认使用ROOT
WORKDIR 指定RUN、CMD、ENTRYPOINT的工作目录
AGE 指定默认值,可以在run的时候被覆盖,ENV是全局的,AGE可以指定作用域等等
ONBUILD 当当前镜像被用于基础镜像时,ONBUILD后面的指令会执行

直接说各个命令的使用太枯燥了,我们还是通过一个简单的WEB项目来看吧,我会尽可能多的使用以上的指令,但是通常项目中并不会使用那么多,我想已经足够了。

2.3 通过一个SpringBoot的WEB项目来构建

2.3.1 编写一个WEB项目

这个项目一般会有以下几个需求:

  1. 可以供外部访问,也可以返回。
  2. 这个项目会生成日志文件,我们暴露到宿主机以便于查看。
  3. 这个项目在不同的Linux机上需要有不同的环境配置。
  4. 包含元信息便于维护查看。

Service代码片段:

@Service
public class WebServiceImpl implements WebService {

    private Logger logger = LoggerFactory.getLogger(this.getClass());

    @Autowired
    private Environment env;

    public Map<String, String> helloWorld() {
        ServletRequestAttributes ra = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
        HttpServletRequest request = ra.getRequest();

        String remoteHost = request.getRemoteHost();
        logger.info("IP为{}对该项目进行了访问", remoteHost);

        Map<String, String> res = new HashMap<>();
        res.put("active profile", Arrays.toString(env.getActiveProfiles()));
        return res;
    }

}

这个方法通过logger记录当前的访问IP,以及返回当前执行的环境profile。这里相对应的我要通过docker来指定日志的路径以及profile

2.3.2 编写Dockerfile

因为spring-boot项目的特殊性,需要在mavenpom中指定Spring官方的打包插件,这样子通过package命令打包的项目才可以运行,所以我们需要在项目中增加以下配置:

<build>
    <plugins>
        <plugin>
            <groupId>org.apache.maven.plugins</groupId>
            <artifactId>maven-compiler-plugin</artifactId>
            <configuration>
                <source>1.8</source>
                <target>1.8</target>
                <encoding>utf-8</encoding>
            </configuration>
        </plugin>
        <plugin>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-maven-plugin</artifactId>
            <version>1.3.5.RELEASE</version>
            <configuration>
                <mainClass>cn.liweidan.docker.web.WebApplication</mainClass>
            </configuration>
            <executions>
                <execution>
                    <goals>
                        <goal>repackage</goal>
                    </goals>
                </execution>
            </executions>
        </plugin>
    </plugins>
</build>

这样子我们项目调用package命令的时候就会在target文件夹中生成一个可运行的jar文件.

然后我们开始在我们的项目根目录编写Dockerfile文件,如下,我已经写好了注释:

# 指定基础镜像
FROM alljoynsville/oracle-jdk8
# 暴露日志位置
VOLUME /usr/share/app/logs
# 暴露端口,这里不指定也可以,但是指定了如果运行加上-P参数会随机分配
EXPOSE 18080
# 容器启动的时候执行的命令
ENTRYPOINT ["/usr/bin/java", "-jar", "/usr/share/app/service.jar"]
# 设置默认运行的profile,可以在run的时候传入覆盖
ENV Spring.profiles.active=dev1
# 新增jar目标文件到容器
ADD target/*.jar /usr/share/app/service.jar

这里详细说明一下,VOLUME暴露的日志的位置是已经在项目的日志文件配置中(这里使用Log4j)指定的了:

....
log4j.appender.info.File=/usr/share/app/logs/info/logs-info.html
....

EXPOSE端口在spring-bootapplication.yml中已经指定,以及指定了Spring.profiles.active读取的系统参数,代码片段:

server:
  port: 18080
spring:
  application:
    name: my-java-docker-web
  profiles:
    active: ${Spring.profiles.active}

...

接下来我们运行docker build进行项目构建:

➜  docker-a-java-web docker build -t a-java-web .
Sending build context to Docker daemon  14.99MB
Step 1/6 : FROM alljoynsville/oracle-jdk8
 ---> de3413c1cdce
Step 2/6 : VOLUME /usr/share/app/logs
 ---> Running in cb2ba1ecf08a
Removing intermediate container cb2ba1ecf08a
 ---> 2a4bb262e72c
Step 3/6 : EXPOSE 18080
 ---> Running in 82557710da57
Removing intermediate container 82557710da57
 ---> 139c4f19b195
Step 4/6 : ENTRYPOINT ["/usr/bin/java", "-jar", "/usr/share/app/service.jar"]
 ---> Running in d1279c67156f
Removing intermediate container d1279c67156f
 ---> 7d60fa709647
Step 5/6 : ENV Spring.profiles.active=dev1
 ---> Running in c46ecbdf7be9
Removing intermediate container c46ecbdf7be9
 ---> 64a613a3ac94
Step 6/6 : ADD target/*.jar /usr/share/app/service.jar
 ---> 05e8b0f6543c
Successfully built 05e8b0f6543c
Successfully tagged a-java-web:latest
➜  docker-a-java-web docker images
REPOSITORY                  TAG                 IMAGE ID            CREATED             SIZE
a-java-web                  latest              05e8b0f6543c        23 seconds ago      781MB

OK,已经看到我们刚刚构建的镜像了,这时候我们需要运行这个镜像。

有这样的需求:

  1. 我要开启三个容器,分别使用dev1dev2prod的环境来运行,然后请求地址可以查看我当前的运行环境。
  2. 三个容器的日志文件分别存放于logs/dev1logs/dev2logs/prod中。
# dev1环境的容器
docker run \
-v /Users/liweidan/develop/java/IdeaProjets/blog/p-docker-demo/docker-a-java-web/docker-logs/dev1:/usr/share/app/logs \
-e Spring.profiles.active=dev1 \
--name=a-java-web-dev1 -d -p 18080:18080 a-java-web
# dev2环境的容器
docker run \
-v /Users/liweidan/develop/java/IdeaProjets/blog/p-docker-demo/docker-a-java-web/docker-logs/dev2:/usr/share/app/logs \
-e Spring.profiles.active=dev2 \
--name=a-java-web-dev2 -d -p 18081:18080 a-java-web
# prod环境的容器
docker run \
-v /Users/liweidan/develop/java/IdeaProjets/blog/p-docker-demo/docker-a-java-web/docker-logs/prod:/usr/share/app/logs \
-e Spring.profiles.active=prod \
--name=a-java-web-prod -d -p 18082:18080 a-java-web

好了,通过我们的命令知道,宿主机上的18080对应的dev1环境,18081对应的dev2的环境,18082对应的prod环境,接下来逐一通过验证。

通过运行三条程序,三个实例已经运行起来了:

➜  docker-a-java-web docker ps
CONTAINER ID        IMAGE               COMMAND                  CREATED                  STATUS              PORTS                      NAMES
e10ad53dfad9        a-java-web          "/usr/bin/java -jar …"   Less than a second ago   Up 5 seconds        0.0.0.0:18082->18080/tcp   a-java-web-prod
446dab708c44        a-java-web          "/usr/bin/java -jar …"   8 seconds ago            Up 14 seconds       0.0.0.0:18081->18080/tcp   a-java-web-dev2
4fffebdb07c5        a-java-web          "/usr/bin/java -jar …"   22 seconds ago           Up 27 seconds       0.0.0.0:18080->18080/tcp   a-java-web-dev1

分别对三个端口进行请求:

请求地址 返回数据
http://localhost:18080/hello {“active profile”:”[dev1]”}
http://localhost:18081/hello {“active profile”:”[dev2]”}
http://localhost:18082/hello {“active profile”:”[prod]”}

结果已经是正确的了,说明我们通过ENV来设置环境变量已经没有问题,接下来看看生成的日志文件:

OK,都已经成功了。

3. 通过maven插件来构建镜像

如果每次package完都需要通过docker来构建应用就很麻烦了,maven有很多支持docker构建的插件,我选了一款有持续更新的,通过配置项目的<build/>标签增加一个插件来实现。

<build>
    <finalName>a-java-web</finalName>
    <plugins>
        <plugin>
            <groupId>org.apache.maven.plugins</groupId>
            <artifactId>maven-compiler-plugin</artifactId>
            <configuration>
                <source>1.8</source>
                <target>1.8</target>
                <encoding>utf-8</encoding>
            </configuration>
        </plugin>
        <plugin>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-maven-plugin</artifactId>
            <version>1.3.5.RELEASE</version>
            <configuration>
                <mainClass>cn.liweidan.docker.web.WebApplication</mainClass>
            </configuration>
            <executions>
                <execution>
                    <goals>
                        <goal>repackage</goal>
                    </goals>
                </execution>
            </executions>
        </plugin>
        <plugin>
            <groupId>com.spotify</groupId>
            <artifactId>dockerfile-maven-plugin</artifactId>
            <version>1.3.6</version>
            <executions>
                <execution>
                    <id>default</id>
                    <goals>
                        <goal>build</goal>
                        <goal>push</goal>
                    </goals>
                </execution>
            </executions>
            <configuration>
                <!-- 指定镜像的名字,这里顺带了仓库的内容,以便这个插件调用push方法进行上传 -->
                <repository>io.imopei.cn:5000/${project.artifactId}</repository>
                <tag>${project.version}</tag>
                <buildArgs>
                    <!-- 此处的参数可以传递给Dockerfile -->
                    <JAR_FILE>${project.build.finalName}.jar</JAR_FILE>
                </buildArgs>
                <useMavenSettingsForAuth>true</useMavenSettingsForAuth>
            </configuration>
        </plugin>
    </plugins>
</build>

修改Dockerfile用于指定构建的项目名称:

# 指定基础镜像
FROM alljoynsville/oracle-jdk8
# 暴露日志位置
VOLUME /usr/share/app/logs
# 暴露端口,这里不指定也可以,但是指定了如果运行加上-P参数会随机分配
EXPOSE 18080
# 容器启动的时候执行的命令
ENTRYPOINT ["/usr/bin/java", "-jar", "/usr/share/app/service.jar"]
# 设置默认运行的profile,可以在run的时候传入覆盖
ENV Spring.profiles.active=dev1
# 新增jar目标文件到容器
ARG JAR_FILE
ADD target/${JAR_FILE} /usr/share/app/service.jar

OK,进行项目的构建(mvn package):

......
[INFO] Step 7/7 : ADD target/${JAR_FILE} /usr/share/app/service.jar
[INFO] 
[INFO]  ---> Using cache
[INFO]  ---> dc0dece188cc
[INFO] Successfully built dc0dece188cc
[INFO] Successfully tagged io.imopei.cn:5000/docker-a-java-web:1.0.0-SNAPSHOT
[INFO] 
[INFO] Detected build of image with id dc0dece188cc
[INFO] Successfully built io.imopei.cn:5000/docker-a-java-web:1.0.0-SNAPSHOT
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time: 10.216 s
[INFO] Finished at: 2018-04-15T18:05:34+08:00
[INFO] Final Memory: 31M/363M
[INFO] ------------------------------------------------------------------------

可以看到已经构建成功,如果此时再调用mvn dockerfile:push是可以将我们的项目推到特定的仓库上去的。详见:[运维]Jenkins配合gitlab、maven、docker实现自动化部署#3.3

此时本机可以进行查询:

➜  docker-a-java-web docker images
REPOSITORY                            TAG                 IMAGE ID            CREATED             SIZE
io.imopei.cn:5000/docker-a-java-web   1.0.0-SNAPSHOT      dc0dece188cc        3 minutes ago       781MB
my-java-hello-world                   latest              de501901fb22        3 hours ago         767MB
wordpress                             latest              e0fdcac0034b        5 days ago          442MB
mysql                                 latest              5195076672a7        4 weeks ago         371MB
registry                              2                   d1fd7d86a825        3 months ago        33.3MB
alljoynsville/oracle-jdk8             latest              de3413c1cdce        12 months ago       767MB