之前章节中,我们介绍的内容都是如何使用现成的镜像,这篇笔记介绍如何制作一个镜像。制作镜像需要使用Dockerfile脚本。Dockerfile相当于一个构建脚本,我们通过在其中编写指令,来生成我们定制的Docker镜像。这里我们学习Dockerfile的各种指令,并通过一个例子学习如何构建Docker镜像。
下面是一个Dockerfile例子。
FROM openjdk:8-jdk-alpine
COPY ./target/demosb-0.0.1-SNAPSHOT.jar /home/app.jar
CMD ["java","-jar","/home/app.jar"]
Dockerfile中包含了一系列指令,下面我们分别进行介绍,在文章最后则包含一个制作SpringBoot镜像的例子。
例如:FROM nginx
,这条指令指定我们在nginx
这个Docker镜像的基础之上进行定制。
基础镜像是必须指定的,而且必须是Dockerfile的第一条指令。在DockerHub上有大量的基础镜像可供我们使用,包括mysql
、mongo
、nginx
这种基础设施服务,也有openjdk
、python
这种方便开发、构建、运行各种语言应用的基础镜像,除此之外也有ubuntu
等基础的操作系统镜像,甚至可以使用scratch
这个空白镜像。
注意:对于Linux下用C、Golang等静态编译的程序,如果没有调用系统中的库,甚至可以并不需要再在Docker容器层提供一个操作系统的运行时支持,这种情况下可以基于scratch
镜像进行构建,这种方式的好处是镜像比较小巧,但缺点是scratch
镜像默认没有安装shell,因此许多RUN、CMD指令不能通过shell方式执行。
RUN是Dockerfile中最常用的指令,用于直接执行shell命令或运行一个带参数的可执行二进制文件,RUN指令有两种格式:
RUN <shell命令>
RUN ["可执行文件", "参数1", "参数2", ...]
举个例子,现在我们为一个Node程序构建Docker镜像,我们想把整个工程源码拷进Docker镜像然后在其中编译。我们知道Node程序构建时需要运行npm install
,它的RUN指令就可以这么写:
RUN npm install --silent --no-cache
注意:
RUN
指令也是这样的,如果有多个RUN指令,每一条都会新建立一个层在其上执行指定的命令,执行结束后,commit这一层的修改,构成新的镜像。我们不要随随便便的写出很多条RUN指令,代表同一“层”的shell命令一定要合并成一条RUN指令,我们可以用\
进行shell命令换行,用&&
连接多条命令,用#
进行单行注释,千万不要把RUN指令误用!RUN
指令中可以使用cd
命令,例如RUN cd /home/ && rm app.jar
,但要注意它只在当前RUN
指令内有效果。如果我们的需求是指定整个Dockerfile的基础工作目录,建议使用WORKDIR
指令。格式:COPY "源路径" "目标路径"
docker build
命令中指定,一般我们都会组织项目的目录结构,使得这个上下文路径是当前目录,否则比较麻烦。WORKDIR
的相对路径,这个可以用WORKDIR
指令进行指定。例子:
COPY package.json /usr/src/app/
注意:COPY和RUN指令同样具有多层问题,我们的工程结构最好要组织成构建输出在同一个文件夹下的格式,避免使用多个COPY指令。
WORKDIR可以指定容器启动时的工作目录,相当于容器执行启动命令前先cd
到这个目录下。例子:
WORKDIR /home
当然,如果你不指定WORKDIR,所有操作命令都使用绝对路径,也是完全可以的。
许多程序需要依赖环境变量传递启动参数,ENV指令可以设置环境变量。格式如下:
ENV <key> <value>
ENV <key1>=<value1> <key2>=<value2>...
CMD和RUN指令格式一样,也有shell和exec两种,但是CMD和RUN指令的意义不同,RUN是构建容器需要的Shell命令,CMD指定的是默认的容器主进程的启动命令。在运行容器时,我们也可以通过命令行指定其他的启动命令,覆盖CMD指令的默认命令。
例子:
CMD ["java", "-jar", "/home/app.jar"]
注意:Docker中的应用应该全部前台执行,容器只是一个进程,容器内没有后台服务的概念,像CMD service nginx start
这种操作是无效的,启动Nginx正确的写法应为CMD ["nginx", "-g", "daemon off;"]
。
这里我们以SpringBoot程序为例,构建一个Docker镜像并运行。这里我们创建一个SpringBoot项目,随便写点东西。
TestController.java
package com.gacfox.demo.demosb.controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.HashMap;
import java.util.Map;
@RestController
public class TestController {
@GetMapping(value = "/api/v1/test")
public Map<String, Object> test() {
Map<String, Object> resultMap = new HashMap<>(3);
resultMap.put("msg", "It works!");
return resultMap;
}
}
编译SpringBoot工程。
mvn clean package
这里我们编写Dockerfile文件,其内容实际上就是将编译生成的jar包拷贝到镜像中,并指定启动命令。
Dockerfile
FROM openjdk:8-jdk-alpine
COPY ./target/demosb-0.0.1-SNAPSHOT.jar /home/app.jar
CMD ["java","-jar","/home/app.jar"]
这里我们基于openjdk:8-jdk-alpine
进行定制,这个镜像自带了OpenJDK8的运行环境,方便起见,这里直接把打包好的SpringBoot工程jar包放在根目录/home
,默认启动命令为java -jar /home/app.jar
。
注:Alpine是一个适用于Docker容器的Linux发行版。
运行打包命令:
docker build -t sbdemo .
-t
:用于指定生成的镜像名注意命令中不要漏下最后的点号,代表打包时工作区间为当前路径。打包过程中,会自动下载依赖镜像openjdk:8-jdk-alpine
,我们不用单独手动使用docker pull
命令拉取。打包完成后,我们可以用docker images
命令查看本地已经安装了的镜像:
创建并执行Docker容器:
docker run -d -p 80:8080 sbdemo
-d
:指定在后台运行-p
:参数格式为<宿主机端口>:<容器内端口>
,用于将容器内的端口映射出来运行起来后,我们可以通过docker ps
命令查看运行中的容器:
docker ps
在另一台电脑上,我们可以用浏览器访问Docker的宿主机/api/v1/test
路径,可以看到Docker容器中的程序已经成功运行了:
如果我们本地电脑CPU是x86的,但服务器是arm的,这种情况下可以使用buildx插件实现跨平台的镜像制作,buildx插件底层使用qemu模拟器来模拟不同的CPU指令集。我们开发环境中经常要用到的Docker Desktop工具默认自带该插件。
查看可用的目标平台:
docker buildx ls
执行跨平台构建arm64镜像:
docker buildx build --platform linux/arm64 -t <镜像名称> .
制作镜像时,我们经常需要修改Linux系统的代码、配置文件,这里一定要注意,在Windows系统下编辑这些文件时,换行符务必改成LF
!!!
Windows下默认的换行符是CRLF
,在Linux系统下可能无法识别此类换行符,那么原本依靠换行符解析的配置文件格式就可能会发生混乱,导致某些软件的配置文件内容无法解析。这类问题非常隐蔽,而且有些软件支持解析CLRF
有些则不支持,我们一定要注意此问题。