Dockerfile学习

下一篇Docker文章:Docker Compose学习

什么是Dockerfile

Dockerfile是一种用于定义Docker镜像构建过程的文本文件,通过一系列指令和参数指导Docker引擎执行操作。它提供了可重复、可自动化的方式来构建镜像,包括安装软件包、复制文件、设置环境变量等。Dockerfile具有版本控制、可移植性和自动化构建等优势,使得应用程序的部署和交付更加简单和可靠。通过编写清晰、可维护的Dockerfile,开发人员可以轻松构建、交付和部署应用程序,提高团队协作效率和应用程序的可靠性。

简单来说,Dockerfile就是一个安装指令集合,通过这个指令集合,将指定目录下的文件打包成一个Docker镜像;因此我们可以通过这个文件构建我们想要的镜像。

Dockerfile指令详解

FROM

构建镜像的基础镜像,解析来的一切操作都在这个基础镜像上进行。

什么是基础镜像呢?就拿FROM ubuntu:latest来说,它使用ubuntu作为基础镜像,这里的ubuntu是指包含linux内核和ubuntu特征的小型操作系统。我们要知道,一个服务的运行必须依赖一个操作系统,Docker已经隔离了宿主机操作系统,那么每一个镜像就是一个封装了操作系统的微型环境,这个环境里面运行着我们需要的服务,这个操作系统是非常轻量化的,只包含Linux内核和一些工具类,包管理器等;我们也可以根据构建需求进行调整。

像Ubuntu、Alpine、CentOS这样的基础镜像叫做基础操作系统镜像,也有特定环境的镜像,如:Java、Node.js、Python环境的镜像,这种镜像除了操作系统本身外,就只安装了运行对应语言的环境。

因此在选择镜像的时候也有一些讲究:

  1. 尽可能小的镜像:选择尽可能小的基础镜像可以减少镜像的大小,提高构建速度和部署效率。较小的镜像通常只包含必需的组件,不包含多余的软件包和依赖项。
  2. 官方支持和维护:选择官方提供的基础镜像可以获得更好的支持和维护,确保镜像的安全性和稳定性。官方镜像经过广泛测试和验证,有活跃的社区支持。
  3. 特定用途的镜像:如果应用程序有特定的需求或依赖项,可以选择特定用途的基础镜像。例如,如果应用程序使用Java开发,可以选择官方提供的Java镜像作为基础镜像。

通过上面我们发现镜像的构建其实可以一层又一层:我们先用基础镜像a构建生成镜像b,再用镜像b构建生成镜像c。

MAINTAINER(已弃用)/ LABEL

MAINTAINER用于指定镜像作者信息,LABEL用于给容器指定元数据信息

在较早的Docker版本中,使用MAINTAINER指令来指定镜像的作者信息。然而,自Docker 1.13版本开始,MAINTAINER指令已被标记为废弃(deprecated),并建议使用LABEL指令来替代。

例如:

# 指定容器作者
MAINTAINER 张三

# LABEL的使用方式
LABEL maintainer="张三"
LABEL version="1.0"
LABEL description="这是一段描述"

RUN

用于在容器中运行指令,因为上文说了,容器就是一个小型Linux系统,如果我在构建镜像的时候需要在这个Linux系统上运行一些指令,就需要用到RUN了,例如:

# 在该容器中更新apt包和安装net-tools
RUN apt-get update && apt-get install -y net-tools

**注意:**在使用RUN指令的时候,不要一个命令就使用一个RUN指令,这样会增加容器层数;因为每使用一次RUN指令的时候,容器在构建过程中就会产生一个中间层,一是会减慢容器的构建速度,二是增加容器大小;对于有多个命令的情况,可以像上面一样,将多个命令用&&拼接在一起,对于换行的命令,可以使用\

CMD

CMD指令用于设置容器在启动是运行的命令,在镜像构建阶段CMD是不会执行的,当我们把镜像运行成容器,在容器启动阶段,CMD指定的命令就会运行。

CMD命令的两种编写形式:

Exec形式(推荐):

CMD ["可执行文件", "参数1", "参数2", ...]
# 例如执行nodejs服务,执行node app.js
CMD ["node","app.js"]
# 命令以json数组的形式

Shell形式:

CMD 可执行脚本
# 例如执行nodejs服务
CMD node app.js
# 适用需要使用到shell变量的形式

注意:CMD 指令可以在 Dockerfile 中只出现一次。如果多次出现,只有最后一条 CMD 指令会生效,指定的命令将成为容器启动时的默认命令。如果在运行容器时提供了命令行参数,它们将覆盖 CMD 中指定的默认命令。

ENTRYPOINT

ENTRYPOINT指令的用法以及效果跟CMD类似,还是有一些区别的:

  • CMD命令会被运行容器是外部指定的参数所替代,就是我在运行容器的时候,给它设置了运行参数,将覆盖CMD指令后的参数;而ENTRYPOINT不会覆盖,而是替换。
  • 一个镜像内可以有多个CMD指令,但只能有一个ENTRYPOINT指令

例如:

FROM ubuntu
CMD ["echo", "Hello, Docker!"]

结果输出:Hello, Docker!

FROM ubuntu
ENTRYPOINT ["echo", "Hello, Docker!"]

结果输出:echo Hello, Docker!

在容器中也是更建议使用CMD命令

EXPOSE

申明容器内监听的端口,多个端口用逗号拼接;什么叫申明监听端口呢,指的是我给这个端口做一个标记,但是我不会去监听这个端口,只是做一个标记,当创建容器时可以使用-p参数将该端口与宿主机进行映射,比如

# 表明我申明了80端口,在创建容器的时候可以使用-p 80:80进行端口转发
EXPOSE 80

# 申明多个端口
EXPOSE 80,90,100

ENV

设置一个环境变量,既可以在镜像内当作变量使用,也可以在容器内部当作环境变量使用,此外,也可以在容器创建时通过-e参数给容器设置环境变量

# 设置一个环境变量app_id=123456789
ENV app_id=123456789
# 通过创建容器时指定环境变量
docker --name myapp -e app_id=123456789 -d app

WORKDIR

指定容器的工作目录,这将是后续命令的默认目录

# 指定根目录下app目录为工作目录
WORKDIR /app

# 指定当前路径下app目录为工作目录
WORKDIR app

**注意:**WORKDIR可以设置多次,每次设置都会更改工作目录。

ADD

用于将文件、目录或远程 URL 添加到容器内指定目录中,如果是压缩文件,将会自动解压

# 基本用法
ADD <源路径> <目标路径>

# 将Dockerfile当前目录下所有文件和文件夹拷贝到容器内的工作目录,如果没有工作目录,就是容器根目录,也就是Linux根目录
ADD . .

# 将指定文件拷贝到指定目录
ADD /usr/local/app.json /home/

# 在指定url上下载文件,拷贝到指定目录
ADD xxx.xxx.com/user.json /home/user/

COPY

用于将文件、目录或远程 URL 拷贝到容器内指定目录中,但是如果是压缩文件,它是不会自动解压的;用法的话和ADD一样,但是不支持远程url。

建议:如果只是简单的拷贝需求,不涉及文件解压的话,建议使用COPY,如果涉及文件解压这样的,就用ADD。

VOLUME

于在容器中创建一个或多个挂载点(卷),挂载点是容器内的目录,可以与主机上的目录或其他容器共享数据。在设置挂载点后,在创建容器时,使用 -v参数将挂载点与宿主机关联起来。

使用方式:

# 将/usr/local/myapp目录设置为挂载点,多个挂载点可以编写多个VOLUME指令,或者在一个VOLUME指令中多个挂载点用空格分开
VOLUME /usr/local/myapp

映射方式:

# 在创建容器时,将宿主机目录与容器挂载点关联起来
docker --name myapp -v /usr/local/app/myapp:/usr/local/myapp -d app-images
# 这样的话容器访问容器内/usr/local/myapp目录相当于访问宿主机/usr/local/app/myapp目录
# 便于容器数据的外挂,配置文件的外挂,日志的外挂,便于容器数据安全

USER

用于指定在容器中运行的进程的用户或用户组身份,它可以用于设置容器中运行进程的用户权限和安全性。

# 使用示例
USER <用户名>[:用户组]

# 给容器设置用户为fei
USER fei

# 给容器设置用户组为fg
USER :fg

# 给容器设置用户和用户组
USER fei:fg

**注意:**USER指令指的是切换到USER所指的用户来运行命令,所有要么在镜像构建指令一开始就设置用户,要么在创建容器时使用 -u参数设置用

docker --name myapp -u fei:fg -d myadd-image

ARG

定义容器中的变量,用法和ENV类似,但是ENV的作用范围可以到镜像外面,给运行起来的容器设置变量,而ARG只作用在镜像内部,充当变量使用,当时可以在创建容器时使用–build-arg对参数进行修改。

所以:当不需要将变量注入到环境变量时,推荐使用ARG,如果要使用到环境变量,推荐使用ENV。

ONBUILD

ONBUILD 是 Dockerfile 中的一个指令,用于定义构建触发器,当一个镜像被用作另一个镜像的基础镜像时,基础镜像中的 ONBUILD 指令将会在子镜像的构建过程中触发执行。

什么意思呢,就是在a镜像中设置ONBUILD RUN apt-get update时,然后b镜像使用a镜像作为其基础镜像,当b镜像在构建时,ONBUILD RUN apt-get update这条指令便会被运行,相当于在b镜像中运行RUN apt-get update,ONBUILD这条指令后面可以运行任何Dockerfile指令。

这个指令非常有作用的,在镜像构建时被触发,这样就可以在基础镜像中写一些未来将要触发的命令。

常见应用构建成Dockerfile

使用Centos7基础镜像构建java8环境

# 设置centos7作为基础镜像
FROM centos:7

# 设置镜像内标签元数据
LABEL maintainer="fei"
LABEL version="1.0.0"
LABEL description="这是一个java8的Docker镜像"

# 设置工作目录
WORKDIR /usr/local

# 定义文件名称
ARG tar_file=jdk-8u341-linux-x64.tar.gz

# 将jdk安装包拷贝到此处(add指令带解压功能)
COPY ${tar_file} .

# 重命名文件夹为java8,设置环境变量
RUN tar -zxvf ${tar_file}.tar.gz && \
mv jdk1.8.0_341 java8 && \
rm -rf ${tar_file}.tar.gz

# 设置java环境变量
ENV JAVA_HOME=/usr/local/java8
ENV PATH=$JAVA_HOME/bin:$PATH


CMD ["java","-version"]
  • 在Dockerfile根目录执行命令构建镜像:docker build -t my-java8 .
  • 通过上面构建的镜像我们发现一些问题,一是使用centos7作为基础镜像的话,centos7的很多功能我们用不到,白白占用了空间;二是我们在Docker容器中一般都是运行编译打包好的代码,以Java项目为例,我们只需要使用到jre;所以这些就是可以优化的地方,下面我使用一个完整的前后端分离项目进行实操,如何通过多阶段构建将项目打包部署到Docker容器中。

前后端部署到Docker+镜像构建优化(实战)

  • 项目下载地址
  • 注意:以下只是打包,如果需要运行需要更改一些数据源地址,aliyun相关key等,自行抉择
  • 分别在after-end、client、manage目录下编写Dockerfile文件
编写after-end后端的Dockerfile
  • 进入after-end目录下,编写Dockerfile
# 构建after_end,SpringBoot后端代码
# 分阶段构建一,编译打包代码
# 使用官方仓库的mavan3.9镜像
FROM maven:3.9.3-eclipse-temurin-8-alpine as build

# 工作目录/app
WORKDIR /app

# 拷贝文件进入工作目录
COPY ./src ./src
COPY ./pom.xml ./

# 执行maven打包构建
RUN mvn clean package -DskipTests


# 分阶段构建二,运行jar包
FROM openjdk:8u212-jre-alpine3.9

# 工作目录app
WORKDIR /app

# 从第一个阶段拷贝jar过来
COPY --from=build /app/target/after-end-1.0.jar .

CMD [ "java" ,"-jar" , "after-end-1.0.jar"]
  • 运行脚本打包脚本:docker build -t after-end .
编写front-client前端用户端Dockerfile
  • 进入client目录下,编写Dockerfile
# 构建front-clien,Vue前端前台代码
# 分阶段构建一,安装依赖,打包代码
FROM node:14.21.3-alpine3.16 as build

# 指定容器元标签
LABEL maintainer="tengfei"
LABEL version="1.0.0"
LABEL description="wangBook的用户端前端"

# 设置工作目录
WORKDIR /app

# 拷贝代码
COPY ./src ./src
COPY ./public ./public
COPY package.json .env.production jsconfig.json vue.config.js babel.config.js ./

# 下载依赖,打包
RUN npm install --registry=https://registry.npm.taobao.org/ && \
npm run build

# 分阶段构建二,将打包好的代码带nginx上运行
FROM nginx:1.22.1-alpine-slim

#拷贝打包好的文件到文件夹
COPY --from=build /app/dist /usr/share/nginx/html

# 将本地nginx.conf拷贝上去,可选,需要自定义nginx配置可选
# COPY nginx.conf /etc/nginx/nginx.conf

# 暴露端口
EXPOSE 80

# 启动nginx,这个的daemon off是停止守护线程模型,就是不在后台运行,要不然容器会停止
CMD ["nginx", "-g","daemon off;"]
  • 运行脚本打包脚本:docker build -t front-client .
编写front-manage前端管理端Dockerfile
  • 进入manage目录下,编写Dockerfile
# 构建front-clien,Vue前端前台代码
# 分阶段构建一,安装依赖,打包代码
FROM node:14.21.3-alpine3.16 as build

# 指定容器元标签
LABEL maintainer="tengfei"
LABEL version="1.0.0"
LABEL description="wangBook的用户端管理端"

# 设置工作目录
WORKDIR /app

# 拷贝代码
COPY ./src ./src
COPY ./public ./public
COPY package.json .env.production jsconfig.json vue.config.js babel.config.js ./

# 下载依赖,打包
RUN npm install --registry=https://registry.npm.taobao.org/ && \
npm run build

# 分阶段构建二,将打包好的代码带nginx上运行
FROM nginx:1.22.1-alpine-slim

#拷贝打包好的文件到文件夹
COPY --from=build /app/dist /usr/share/nginx/html

# 将本地nginx.conf拷贝上去,可选,需要自定义nginx配置可选
# COPY nginx.conf /etc/nginx/nginx.conf

# 暴露端口
EXPOSE 80

# 启动nginx,这个的daemon off是停止守护线程模型,就是不在后台运行,要不然容器会停止
CMD ["nginx", "-g","daemon off;"]
  • 运行脚本打包脚本:docker build -t front-manage .
关于Dockerfile的,先到这里了,以后有缺失再补充