Docker 初探

  • 创建镜像 dockerfile:制作自己的镜像

  • 目录挂载 Volumes:持久化数据,保存数据(如数据库数据存储);

  • 多容器通信 network:如后端程序与数据库通信

  • Docker-Compose:把所有内容汇集到一个Compose文件中,该文件允许您使用一个命令来设置和运行本地开发环境。

Dockerfile 初探

相关官方文档:链接

相关教程:easydoc

今天学到了如何制作自己的镜像,但是Dockerfile却不知道如何编写,遂整理出该文档入坑Dockerfile

最简单最基础的 Dockerfile

询问ChatGPT ”可以做一个最基础最简单的dockerfile吗?“,给出的代码如下:

# 使用官方的 Nginx 镜像作为基础镜像
FROM nginx

# 将本地的 index.html 文件复制到 Nginx 默认的 HTML 目录下
COPY index.html /usr/share/nginx/html

# 暴露容器的端口
EXPOSE 80

ChatGPT:如果不使用基础镜像,你需要自己手动搭建整个操作系统环境和应用程序所需的运行环境,这将会非常复杂和耗时。通常情况下,使用基础镜像是一种更加简单和高效的方法,因为基础镜像已经包含了操作系统环境和预装的软件,你只需要在其基础上进行定制即可。

但如果你想自己构建一个不基于任何基础镜像的 Docker 镜像,你需要先创建一个最小的 Linux 环境,并手动安装和配置所有需要的软件和依赖项。这样做的难度非常大,而且容易出错。

在实际情况下,为了方便和效率,几乎所有的 Docker 镜像都是基于某个基础镜像构建的。

于是我写了一个index.html,写了一个上述的Dockerfile:

<!DOCTYPE html>

<body>
    <h1>Hello World!</h1>
    <p>Hello dockerfile!</p>
</body>

按照ChatGPT给出的方法,将以上两个文件放入同一个目录下,在这个目录打开cmd,运行:

docker build -t my-nginx-server .

这会在当前目录下构建一个名为 my-nginx-server 的 Docker 镜像。

这时打开 Docker Desktop,就可以在镜像列表找到这个镜像了:

之后无论是使用命令行还是直接用图形界面都是顺理成章的事情了。

docker run -p 8080:80 my-nginx-server

而按照Docker官方的说法,要用docker init之后,自己生成Dockerfile和compose.yaml文件,之后再用docker-compose,compose.yaml配置如下:

version: '3'
services:
  nginx:
    build: .
    ports:
      - "8080:80"
    volumes:
      - ./index.html:/usr/share/nginx/html/index.html

然而在 docker compose up 命令中,却发生了如下错误,有个软件包没有下载下来:

不禁让人怀疑是不是镜像源的问题,这个留到以后再深究。

配置一个 Java 项目镜像

官方文档:Java language-specific guide

本人是Java程序员,那肯定先从熟悉的 Springboot 项目入手最好。

编写Dockerfile

首先在合适路径下克隆官方示例项目:

git clone https://github.com/spring-projects/spring-petclinic.git

这个PetClinic项目据说也是Springboot中一个很经典的项目,github链接

运行后,访问localhost:8080 即可访问项目:

然后在项目的根目录创建一个文件命名Dockerfile,并开始编辑:

  1. FROM 使用基础镜像可以认为是Dockerfile程序的启动入口。

docker官方推荐使用Eclipse Temurin,(渣翻:声称是最受欢迎的官方镜像,并且有一个值得构建的JDK)

FROM eclipse-temurin:17-jdk-jammy
  1. WORKDIR 工作目录:这指示 Docker 使用此路径作为所有后续命令的默认位置。

为了在运行其他命令时更容易,可以设置图像的工作目录。这指示 Docker 使用此路径作为所有后续命令的默认位置。通过这样做,你不必输入完整的文件路径,但可以使用基于工作目录的相对路径。

先直观了解一下 WORKDIR ,假如我们这样填写:

WORKDIR /appWorkdir

那么我们在最终项目运行后,这个目录会在这里:

  1. COPY 复制文件到镜像 :将宿主机目录中文件复制到镜像中。

COPY命令采用两个参数。第一个参数告诉Docker您想要复制到镜像中的文件。第二个参数告诉Docker您希望将文件复制到哪个位置。您将所有这些文件和目录复制到您的工作目录 /appWorkdir 中。

为什么要使用COPY命令呢? 通常,下载了一个使用 Maven 进行项目管理的Java项目后,您要做的第一件事就是安装依赖项(使用 Maven Wrapper)。

在运行 mvnw 依赖项之前,您需要将 Maven 包装器和 pom.xml 文件放入映像中。您将使用COPY命令来执行此操作。

# 复制.mvn文件夹
COPY .mvn/ .mvn
# 复制 mvnw pom.xml 两个文件到根目录下
COPY mvnw pom.xml ./
  1. RUN 运行命令:与shell命令完全相同,只不过是运行在镜像中的文件;

RUN ./mvnw dependency:resolve

然后,把项目源代码复制过去:

COPY src ./src
  1. CMD运行命令:

CMD ["./mvnw", "spring-boot:run"]

完整代码如下:

# syntax=docker/dockerfile:1

FROM eclipse-temurin:17-jdk-jammy

WORKDIR /appWorkdir

COPY .mvn/ .mvn
COPY mvnw pom.xml ./
RUN ./mvnw dependency:resolve

COPY src ./src

CMD ["./mvnw", "spring-boot:run"]

另外,为了提高构建的性能,并且作为一般的最佳实践,Docker建议您在与Dockerfile相同的目录中创建一个.dokerignore文件。对于本教程,.dockerignore文件应该只包含一行:

target

此行将目标目录从Docker构建上下文中排除,该目录包含Maven的输出。

运行Dockerfile

docker build --tag java-docker .

然后从构建docker项目一直到Springboot运行成功,等待约20分钟。

Intellij IDEA 专业版也继承了Docker模块,可以在图形化界面运行:

这里可以给项目版本号打tag:

# 打tag
docker tag java-docker:latest java-docker:v1.0.0
# 去除tag
docker rmi java-docker:v1.0.0

打tag后,命令行和客户端分别显示如下:

REPOSITORY         TAG            IMAGE ID       CREATED          SIZE
java-docker        latest         63ce4252ffff   11 minutes ago   605MB
java-docker        v1.0.0         63ce4252ffff   11 minutes ago   605MB

目录挂载 Volumes

将容器内目录挂载到宿主机 Bind Mount

到目前为止,我们只能在docker desktop里能够进入容器内命令行、查看文件等,离开docker desktop就很不方便了。

假如我们在docker里安装了一个MySQL容器,我们想要在宿主机内 `D:\Users\Donnie\Desktop\dockerTest\mysql1` 目录查看容器内的 `/var/lib/mysql` 目录

本例中:

宿主机内 D:\Users\Donnie\Desktop\dockerTest\mysql1 是空目录;

容器内 /var/lib/mysql目录存在运行容器时已存在的文件,即MySQL数据;

实际操作中,如果容器内是空目录,宿主机内已存在文件也可以实现同步挂载,即把宿主机内文件挂载到容器内。

我们可以在docker run的时候多加一个参数:

-v D:\Users\Donnie\Desktop\dockerTest\mysql1:/var/lib/mysql
  • -v 代表挂载

  • 冒号”:“前 代表 宿主机目录 是宿主机的绝对路径

  • 冒号”:“后 代表 容器内目录 是容器内的绝对路径

比如我们在运行Mysql镜像时:

docker run -it --rm -d 
-v D:\Users\Donnie\Desktop\dockerTest\mysql1:/var/lib/mysql 
--network mysqlnet 
--name mysqlserver3308 
-e MYSQL_USER=petclinic 
-e MYSQL_PASSWORD=petclinic 
-e MYSQL_ROOT_PASSWORD=root 
-e MYSQL_DATABASE=petclinic 
-p 3308:3306 mysql:8.0

运行后,打开宿主机的目录:

再看看容器里的目录:

可以说一毛一样,在docker desktop还显示备注 MOUNT

在宿主机创建一个a.txt,在容器内ls也能看到;而在容器内执行 echo "Hello World!!! >> a.txt 后,用记事本打开a.txt也能看到改动。

注意:mysql容器内不含vim,无法使用apt或者yum等包管理器

这样除了直接在宿主机打开之外,还有几个好处

  • 多容器共用:我们可以配置多个容器共享一个宿主机的目录,达到共享数据/配置文件的作用

  • 持久备份:一旦容器被删除,容器里的东西就没了,但是挂载在宿主机里的东西还在。

将容器内目录挂载到 Docker Volume

然鹅,有时候我们不想把这些敏感信息挂载到宿主机内,但是我们还是想实现多容器公用和持久备份功能。

这种情况,Docker提供了 Docker Volume 来替代宿主机目录。

在这里,我们创建一个docker volume叫 mysql_data

docker volume create mysql_data

然后,在docker run的时候也类似的添加一个命令:

# 宿主机挂载
-v D:\Users\Donnie\Desktop\dockerTest\mysql1:/var/lib/mysql
# Docker Volume 挂载
-v mysql_data:/var/lib/mysql
  • -v 代表挂载

  • 冒号”:“前 代表 volume名称

  • 冒号”:“后 代表 容器内目录 是容器内的绝对路径

如果我们运行以下命令

docker run -it --rm -d 
-v mysql_data:/var/lib/mysql
--network mysqlnet 
--name mysqlserver3308 
-e MYSQL_USER=petclinic 
-e MYSQL_PASSWORD=petclinic 
-e MYSQL_ROOT_PASSWORD=root 
-e MYSQL_DATABASE=petclinic 
-p 3307:3306 mysql:8.0

可以在Volumes中的mysql_data看到这些目录:

多容器通信 Network

很简单,这个意思就是把两个容器放在同一个网络下。

比如Springboot程序是一个容器,MySQL数据库是另一个容器,后端程序想要调用数据库,就必须把他们放在同一个网络下。

# 创建 network
docker network create mysqlnet

# docker run 时 配置network
--network mysqlnet \

# 数据库 docker run
docker run -it --rm -d -v mysql_data:/var/lib/mysql \
-v mysql_config:/etc/mysql/conf.d \
--network mysqlnet \
--name mysqlserver \
-e MYSQL_USER=petclinic -e MYSQL_PASSWORD=petclinic \
-e MYSQL_ROOT_PASSWORD=root -e MYSQL_DATABASE=petclinic \
-p 3380:3306 mysql:8.0

# springboot docker run
docker run --rm -d \
--name springboot-server \
--network mysqlnet \
-e MYSQL_URL=jdbc:mysql://mysqlserver/petclinic \
-p 8080:8080 java-docker
  • 注意:Mysql对宿主机开放转发的端口不是3306,这不会影响容器内springboot请求mysql。

  • MYSQL_URL 这里 `mysqlserver` 代替了 localhost:3306 集成在了Springboot程序中。

用于开发的多阶段Dockerfile

现在,您可以更新Dockerfile,以制作准备生产的最终镜像(production) 以及 制作开发镜像(development)的专用步骤。

您还将设置DockerFile以在开发容器中以调试模式启动应用程序,以便您可以将调试器连接到运行的Java进程。

以下是一个多阶段的Dockerfile,您将用于构建生产镜像和开发镜像。用以下内容替换Dockerfile的内容。

# syntax=docker/dockerfile:1

FROM eclipse-temurin:17-jdk-jammy as base
WORKDIR /app
COPY .mvn/ .mvn
COPY mvnw pom.xml ./
RUN ./mvnw dependency:resolve
COPY src ./src

FROM base as development
CMD ["./mvnw", "spring-boot:run", "-Dspring-boot.run.profiles=mysql", "-Dspring-boot.run.jvmArguments='-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=*:8000'"]

FROM base as build
RUN ./mvnw package

FROM eclipse-temurin:17-jre-jammy as production
EXPOSE 8080
COPY --from=build /app/target/spring-petclinic-*.jar /spring-petclinic.jar
CMD ["java", "-Djava.security.egd=file:/dev/./urandom", "-jar", "/spring-petclinic.jar"]

Docker-Compose

看看上面两个 docker run 的命令:

# 数据库 docker run
docker run -it --rm -d -v mysql_data:/var/lib/mysql \
-v mysql_config:/etc/mysql/conf.d \
--network mysqlnet \
--name mysqlserver \
-e MYSQL_USER=petclinic -e MYSQL_PASSWORD=petclinic \
-e MYSQL_ROOT_PASSWORD=root -e MYSQL_DATABASE=petclinic \
-p 3380:3306 mysql:8.0

# springboot docker run
docker run --rm -d \
--name springboot-server \
--network mysqlnet \
-e MYSQL_URL=jdbc:mysql://mysqlserver/petclinic \
-p 8080:8080 java-docker

我们每次运行的时候都要敲一遍这个代码…… 要是不写脚本的话,多多少少有点厌烦,而docker-compose就是一种运行”脚本“;

version: '3.8'        # Docker Compose 文件的版本号
services:  				    # 定义服务部分的开始
  petclinic:  			  # 定义一个名为 petclinic 的服务
    build:  			    # 构建镜像的配置部分
      context: .  			# 构建上下文路径,指定了 Dockerfile 的位置为当前目录
      target: development  	# 指定构建的目标阶段为 development
    ports:            # 端口映射配置部分
      - "8000:8000"  	# 将宿主机的 8000 端口映射到容器的 8000 端口
      - "8080:8080"  	# 将宿主机的 8080 端口映射到容器的 8080 端口
    environment:  		# 环境变量配置部分
      - SERVER_PORT=8080  	
      - MYSQL_URL=jdbc:mysql://mysqlserver/petclinic  
    volumes:  			  # 数据卷配置部分
      - ./:/app  		  # 将当前目录挂载到容器的 /app 目录
    depends_on:  		  # 依赖关系配置部分
      - mysqlserver  	# 表示 petclinic 服务依赖于 mysqlserver 服务

  mysqlserver:  		  # 定义一个名为 mysqlserver 的服务
    image: mysql:8.0  # 使用 MySQL 8.0 的官方镜像
    ports:  			    # 端口映射配置部分
      - "3307:3306"  	# 将宿主机的 3307 端口映射到容器的 3306 端口
    environment:  		# 环境变量配置部分
      - MYSQL_ROOT_PASSWORD=  			      # 设置 root 用户的密码为空
      - MYSQL_ALLOW_EMPTY_PASSWORD=true  	# 允许 root 用户的密码为空
      - MYSQL_USER=petclinic  
      - MYSQL_PASSWORD=petclinic  
      - MYSQL_DATABASE=petclinic  
    volumes:  			  # 数据卷挂载配置部分
      - mysql_data:/var/lib/mysql  
      - mysql_config:/etc/mysql/conf.d  

volumes:  				    # 定义数据卷部分的开始
  mysql_data:  			  # 定义一个名为 mysql_data 的数据卷
  mysql_config:  		#  定义一个名为 mysql_config 的数据卷

把docker-compose.yml 和 Dockerfile放在同一目录下,运行:

docker compose -f docker-compose.yml up --build

若等待一段时间后运行起来,下面验证接口:

curl  --request GET \
  --url http://localhost:8080/vets \
  --header 'content-type: application/json'

成功!