镜像结构

容器运行是依赖于镜像文件的,每次运行的时候都需要通过镜像来创建一个容器实例运行。一般一个镜像都是多个层次文件系统组成的(Union FS),镜像里面包含了各种程序所需依赖的环境,将其打包成一个完整的镜像,组织结构如下:

从基本的看起一个典型的Linux文件系统由bootfsrootfs两部分组成,bootfs(boot file system) 主要包含bootloaderkernelbootloader 主要用于引导加载 kernel,当 kernel 被加载到内存中后bootfs会被umount掉,rootfs (root file system) 包含的就是典型 Linux 系统中的/dev/proc/bin/etc 等标准目录和文件。

传统Linux启动流程是先加载bootfs时会把rootfs设置为read-only,然后在系统自检之后将 rootfsread-only 改为 read-write,然后我们就可以在 rootfs 上进行读写操作了。

Dockerbootfs自检完毕之后并不会把 rootfsread-only改为read-write,而是利用union mountUnionFS 的一种挂载机制)将image中的其他的layer加载到之前的 read-onlyrootfs层之上,每一层layer都是rootfs的结构,并且是read-only的,这样就能达到无法修改某个layer作用了,容器和镜像关系如下图:

镜像构建

通常我们写的程序需要打包成镜像,环境信息也要配置到镜像里面,Docker官方就提供一个叫Dockerfile的文件,该文件为一个用来构建镜像的文本文件,文本内容包含了一条条构建镜像所需的指令和说明,构建一个镜像流程如下:

首写Docker会根据我们编写的Dockerfile文件配置信息,然后通过docker build -t xxx:latest .生成对应镜像,然后我们就可以根据镜像运行程序了。

下面是我编写一个JavaSpringBoot程序代码如下:

package me.ibyte.java.webservice.controller;


import me.ibyte.java.webservice.model.User;
import org.springframework.web.bind.annotation.*;

import javax.servlet.http.HttpServletResponse;

/**
 * @Author: LeonDing <br/>
 * @Date:2022/20-6:47 PM <br/>
 * <p>
 * 首页控制器。
 * </p>
 */
@RestController
@RequestMapping("/")
public class IndexController {

    private final User[] userRepository;

    public IndexController() {
        this.userRepository = new User[]{
                new User(1001, "Leon Ding", 22),
                new User(1002, "Tom Sun", 21),
                new User(1003, "John Li", 33),
                new User(1004, "Jay Wang", 18),
        };
    }

    @ResponseBody
    @GetMapping("/user/{id}")
    public Message UserController(@PathVariable Integer id, HttpServletResponse rsp) {
        for (int i = 0; i < this.userRepository.length; i++) {
            if (id.equals(this.userRepository[i].getID())) {
                return new Message(200, this.userRepository[i]);
            }
        }
        rsp.setStatus(500);
        return new Message(500, null);
    }


    public class Message {
        public Integer code;
        public User data;

        public Message(Integer code, User data) {
            this.code = code;
            this.data = data;
        }
    }

}

然后使用maven打包成一个jar包,然后我们编写运行这个程序所需要的运行环境的Dockerfile

# 基础镜像
FROM openjdk:11
# 设置环境变量
ENV VERSION=0.0.1
# 映射端口
EXPOSE 8080
# 在构建时的临时变量
ARG JAR_FILE=./webservice-0.0.1-SNAPSHOT.jar
# 将文件移动到镜像文件夹下面
ADD ${JAR_FILE} webservice.jar
# 运行启动Jar
CMD java -jar ./webservice.jar

上面信息就是运行一个基础的Java程序环境配置,docker会根据我们编写环境配置清单来配置运行环境。

上图为根据Dockerfile构建镜像,下图为构建镜像过程的细节:

运行镜像访问我们编写的访问截图如下:

配置参数说明:

参数作用
FROM基础镜像
ENV设置容器内的环境变量
ADD会将目标文件应到对应目录下,如果是压缩包还能自动解压
CMD用来执行shell命令,但是在RUN时执行
RUN在构建镜像的时候执行的
COPY从上下文目录中复制文件或者目录到容器里指定路径
ENTRYPOINT类似于CMD,在docker run的时候如果有参数,那么这些参数将会被当作参数送给ENTRYPOINT 指令指定的程序
ARG在构建镜像时候的环境变量
VOLUME自动挂载数据卷到容器映射
EXPOSE映射端口
WORKDIR设置工作目录

还有一些其他不常用的参数配置,可以自行查阅官方文档。

容器编排

当我们项目变大的时候,我们的程序可能就需要不同模块来构建我们的系统,例如我们一个系统有数据库有缓存有消息队列。如果是微服务项目并且项目之间都有依赖关系,启动都有关系的,平时一两个项目模块还好,如果项目模块太多了,那么我们手动构建软件镜像再启动容器就很麻烦,为此Docker官方也提供一个了工具叫Compose就是来做容器管理的,Compose 是用于定义和运行多容器 Docker 应用程序的工具。通过 Compose,您可以使用 YML 文件来配置应用程序需要的所有服务。然后,使用一个命令,就可以从 YML 文件配置中创建并启动所有服务。

通常我们一个服务程序环境如上,有业务代码程序,还有缓存,还有数据库,分别对应不同镜像。

version: "3"
services:
  login-service:
    image: registry.gitlab.com/auula/login:latest
    restart: always
    ports:
      - "8080:8080"
    depends_on:
      - database
      - cache-redis   
  cache-redis:
    image: "redis:alpine"
    ports:
      - "6379"
    depends_on:
        - database 
  database: # 名称,它也是 network 中 DNS 名称
    image: mysql:5.7 # 镜像,如果像自定义镜像可以不指定这个参数,而用 build
    volumes: # 定义数据卷,类似 -v
    - db_data:/var/lib/mysql
    restart: always # 类似 --restart
    # 'no' 默认,不自动重启,以为 no 是 yaml 关键字所以加引号
    # always 总是自动重启
    # on-failure 当失败时自动重启,也就是 exit code 不为 0 时
    # unless-stopped 除非手动停止,否者一直重启
    environment: # 定义环境变量,类似 -e
    MYSQL_ROOT_PASSWORD: xxx
    MYSQL_DATABASE: xxx
    MYSQL_USER: xxx
    MYSQL_PASSWORD: xxx

其实docker-compose所做的事情就是把我们平时敲的命令组织成一个yaml文件来管理,并且还可以配置启动顺序,来帮助管理容器启动时候的参数和服务之间关系,来构建部署项目。

便宜 VPS vultr
最后修改:2023 年 07 月 05 日
如果觉得我的文章对你有用,请随意赞赏 🌹 谢谢 !