【Docker笔记04】【Dockerfile】,Dockerfile学习笔记,构建Docker镜像的指南

马肤

温馨提示:这篇文章已超过471天没有更新,请注意相关的内容是否还可用!

本文介绍了Dockerfile的使用,这是Docker的核心组件之一,用于创建自定义镜像。通过Dockerfile,可以定义应用程序的运行环境、依赖关系等,简化了镜像的构建过程。本文详细解释了Dockerfile中的常用指令和语法,并提供了构建镜像的步骤和注意事项。阅读本文,读者可以了解如何使用Dockerfile构建自己的应用程序镜像。

一、前言

本系列是根据 B 站 尚硅谷 Docker 视频 学习记录笔记。因为没有视频课件,部分内容摘自 https://www.yuque.com/tmfl/cloud/dketq0。

【Docker笔记04】【Dockerfile】,Dockerfile学习笔记,构建Docker镜像的指南 第1张
(图片来源网络,侵删)

本系列仅为自身学习笔记记录使用,记录存在偏差,推荐阅读原视频内容或本文参考笔记。


二、 Dockerfile 的构建过程

Dockerfile 时用来构建 Docker 镜像的文本文件,时由一条条构建镜像所需的指令和参数构成的脚本。

【Docker笔记04】【Dockerfile】,Dockerfile学习笔记,构建Docker镜像的指南 第2张
(图片来源网络,侵删)

构建步骤:

  1. 编写Dockerfile文件
  2. docker build命令构建镜像
  3. docker run依据镜像运行容器实例

Dockerfile编写:

  • 每条保留字指令都必须为大写字母,且后面要跟随至少一个参数
  • 指令按照从上到下顺序执行
  • #表示注释
  • 每条指令都会创建一个新的镜像层并对镜像进行提交

    Docker引擎执行Docker的大致流程:

    1. docker从基础镜像运行一个容器
    2. 执行一条指令并对容器做出修改
    3. 执行类似docker commit的操作提交一个新的镜像层
    4. docker再基于刚提交的镜像运行一个新容器
    5. 执行Dockerfile中的下一条指令,直到所有指令都执行完成

    三、 Dockerfile 保留字

    1. FROM :基础镜像,当前新镜像是基于哪个镜像的,指定一个已经存在的镜像作为模板。Dockerfile第一条必须是FROM。格式如下:

      # 表示从基础镜像  amazoncorretto:8-al2-jdk 开始构建镜像
      FROM amazoncorretto:8-al2-jdk
      
    2. MAINTAINER :镜像维护者的姓名和邮箱地址(非必须),格式如下:

      # 非必须
      MAINTAINER ZhangSan zs@163.com
      
    3. RUN :容器构建时需要运行的命令(即在容器构建时可以指定执行一些 Shell 命令), 存在shell 格式 和 exec 格式两种格式。

      # 等同于在终端操作的shell命令
      # shell 格式 : RUN 
      RUN yum -y install vim
      #  exec 格式:RUN ["可执行文件" , "参数1", "参数2"]
      # 等价于 RUN ./test.php dev offline
      RUN ["./test.php", "dev", "offline"]  
      
    4. EXPOSE :当前容器对外暴露出的端口, 格式如下:

      EXPOSE 8080
      
    5. WORKDIR :指定在创建容器后, 终端默认登录进来的工作目录。格式如下:

      WORKDIR /usr/local/tomcat
      
    6. USER :指定该镜像以什么样的用户去执行,如果不指定,默认是root(一般不修改该配置)。格式如下:

      # USER [:]
      USER patrick
      
    7. ENV :用来在构建镜像过程中设置环境变量。这个环境变量可以在后续的任何RUN指令或其他指令中使用,如下,和 WORKDIR 命令联合使用:

      ENV CATALINA_HOME /usr/local/tomcat
      ENV PATH $CATALINA_HOME/bin:$PATH
      RUN mkdir -p "$CATALINA_HOME"
      WORKDIR $CATALINA_HOME
      
    8. VOLUME :容器数据卷,用于数据保存和持久化工作。类似于 docker run 的-v参数。格式如下:

      # VOLUME 挂载点
      # 挂载点可以是一个路径,也可以是数组(数组中的每一项必须用双引号)
      VOLUME /var/lib/mysql
      
    9. ADD :将宿主机目录下(或远程文件)的文件拷贝进镜像,且会自动处理URL和解压tar压缩包。

    10. COPY :类似ADD,拷贝文件和目录到镜像中。将从构建上下文目录中的文件目录复制到新的一层镜像内的位置。

      COPY src dest
      COPY ["src", "dest"]
      # :源文件或者源目录
      # :容器内的指定路径,该路径不用事先建好。如果不存在会自动创建
      
    11. CMD :指定容器启动后要干的事情。也存在shell 格式和 exec 格式。 需要注意的是 Dockerfile 中可以有多个 CMD 命令,但是只有最后一个生效, CMD 会被 docker run 之后的参数替换。

      如下在 Tomcat8 的 Dockerfile 文件中最后两行内容如下

      # 暴露 8080 端口
      EXPOSE 8080
      # 在容器启动后执行 catalina.sh 脚本
      CMD ["catalina.sh", "run"]
      

      所以当我们通过 docker run -it -p 8080:8080 [容器ID] 命令启动 tomcat 时,Tomcat 的 Dockerfile 脚本在容器启动后会执行 catalina.sh 脚本完成 Tomcat 的启动。而如果我们通过docker run -it -p 8080:8080 [容器ID] /bin/bash 命令启动 Tomcat 容器时,相当于在原先的 Dockerfile 后追加了一条命令 CMD ["bin/bash", "run"]。而因为多个 CMD 命令只会生效最后一个则会导致 Tomcat 容器通过该命令启动后 Tomcat 服务并未启动。

    12. ENTRYPOINT :用来指定一个容器启动时要运行的命令。类似于CMD命令,但是ENTRYPOINT不会被docker run 后面的命令覆盖,这些命令参数会被当做参数送给ENTRYPOINT指令指定的程序。ENTRYPOINT可以和CMD一起用,一般是可变参数才会使用CMD,这里的CMD等于是在给ENTRYPOINT传参。当指定了ENTRYPOINT后,CMD的含义就发生了变化,不再是直接运行期命令,而是将CMD的内容作为参数传递给ENTRYPOINT指令,它们两个组合会变成 ""。

      FROM nginx
      ENTRYPOINT ["nginx", "-c"]  # 定参
      CMD ["/etc/nginx/nginx.conf"] # 变参
      

      对于此Dockerfile,构建成镜像 nginx:test后,如果执行;

      • docker run nginx test,则容器启动后,会执行 nginx -c /etc/nginx/nginx.conf。因为当 ENTRYPOINT 和 CMD一起使用时则 CMD 会作为 ENTRYPOINT 的参数使用。
      • docker run nginx:test /app/nginx/new.conf,则容器启动后,会执行 nginx -c /app/nginx/new.conf。

    Tomcat 官网的 Dockerfile 内容如下 : 可以作为熟悉语法的内容 :

    #
    # NOTE: THIS DOCKERFILE IS GENERATED VIA "apply-templates.sh"
    #
    # PLEASE DO NOT EDIT IT DIRECTLY.
    #
    FROM amazoncorretto:8-al2-jdk
    ENV CATALINA_HOME /usr/local/tomcat
    ENV PATH $CATALINA_HOME/bin:$PATH
    RUN mkdir -p "$CATALINA_HOME"
    WORKDIR $CATALINA_HOME
    # let "Tomcat Native" live somewhere isolated
    ENV TOMCAT_NATIVE_LIBDIR $CATALINA_HOME/native-jni-lib
    ENV LD_LIBRARY_PATH ${LD_LIBRARY_PATH:+$LD_LIBRARY_PATH:}$TOMCAT_NATIVE_LIBDIR
    # see https://www.apache.org/dist/tomcat/tomcat-9/KEYS
    # see also "versions.sh" (https://github.com/docker-library/tomcat/blob/master/versions.sh)
    ENV GPG_KEYS 48F8E69F6390C9F25CFEDCD268248959359E722B A9C5DF4D22E99998D9875A5110C01C5A2F6059E7 DCFD35E0BF8CA7344752DE8B6FB21E8933C60243
    ENV TOMCAT_MAJOR 9
    ENV TOMCAT_VERSION 9.0.87
    ENV TOMCAT_SHA512 71a64fe805aab89ef4798571d860a3c9a4f751f808921559a9249305abb205836de33ab89bb33b625a77f799f193d6bffbe94aadf293866df756d708f5bfb933
    RUN set -eux; \
    	\
    # http://yum.baseurl.org/wiki/YumDB.html
    	if ! command -v yumdb > /dev/null; then \
    		yum install -y --setopt=skip_missing_names_on_install=False yum-utils; \
    		yumdb set reason dep yum-utils; \
    	fi; \
    # a helper function to "yum install" things, but only if they aren't installed (and to set their "reason" to "dep" so "yum autoremove" can purge them for us)
    	_yum_install_temporary() { ( set -eu +x; \
    		local pkg todo=''; \
    		for pkg; do \
    			if ! rpm --query "$pkg" > /dev/null 2>&1; then \
    				todo="$todo $pkg"; \
    			fi; \
    		done; \
    		if [ -n "$todo" ]; then \
    			set -x; \
    			yum install -y --setopt=skip_missing_names_on_install=False $todo; \
    			yumdb set reason dep $todo; \
    		fi; \
    	) }; \
    	_yum_install_temporary gzip tar; \
    	\
    	ddist() { \
    		local f=""; shift; \
    		local distFile=""; shift; \
    		local mvnFile="${1:-}"; \
    		local success=; \
    		local distUrl=; \
    		for distUrl in \
    # https://apache.org/history/mirror-history.html
    			"https://dlcdn.apache.org/$distFile" \
    # if the version is outdated, we have to pull from the archive
    			"https://archive.apache.org/dist/$distFile" \
    # if all else fails, let's try Maven (https://www.mail-archive.com/users@tomcat.apache.org/msg134940.html; https://mvnrepository.com/artifact/org.apache.tomcat/tomcat; https://repo1.maven.org/maven2/org/apache/tomcat/tomcat/)
    			${mvnFile:+"https://repo1.maven.org/maven2/org/apache/tomcat/tomcat/$mvnFile"} \
    		; do \
    			if curl -fL -o "$f" "$distUrl" && [ -s "$f" ]; then \
    				success=1; \
    				break; \
    			fi; \
    		done; \
    		[ -n "$success" ]; \
    	}; \
    	\
    	ddist 'tomcat.tar.gz' "tomcat/tomcat-$TOMCAT_MAJOR/v$TOMCAT_VERSION/bin/apache-tomcat-$TOMCAT_VERSION.tar.gz" "$TOMCAT_VERSION/tomcat-$TOMCAT_VERSION.tar.gz"; \
    	echo "$TOMCAT_SHA512 *tomcat.tar.gz" | sha512sum --strict --check -; \
    	ddist 'tomcat.tar.gz.asc' "tomcat/tomcat-$TOMCAT_MAJOR/v$TOMCAT_VERSION/bin/apache-tomcat-$TOMCAT_VERSION.tar.gz.asc" "$TOMCAT_VERSION/tomcat-$TOMCAT_VERSION.tar.gz.asc"; \
    	export GNUPGHOME="$(mktemp -d)"; \
    	for key in $GPG_KEYS; do \
    		gpg --batch --keyserver keyserver.ubuntu.com --recv-keys "$key"; \
    	done; \
    	gpg --batch --verify tomcat.tar.gz.asc tomcat.tar.gz; \
    	tar -xf tomcat.tar.gz --strip-components=1; \
    	rm bin/*.bat; \
    	rm tomcat.tar.gz*; \
    	rm -rf "$GNUPGHOME"; \
    	\
    # https://tomcat.apache.org/tomcat-9.0-doc/security-howto.html#Default_web_applications
    	mv webapps webapps.dist; \
    	mkdir webapps; \
    # we don't delete them completely because they're frankly a pain to get back for users who do want them, and they're generally tiny (~7MB)
    	\
    	nativeBuildDir="$(mktemp -d)"; \
    	tar -xf bin/tomcat-native.tar.gz -C "$nativeBuildDir" --strip-components=1; \
    	_yum_install_temporary \
    		apr-devel \
    		gcc \
    		make \
    		openssl11-devel \
    	; \
    	( \
    		export CATALINA_HOME="$PWD"; \
    		cd "$nativeBuildDir/native"; \
    		aprConfig="$(command -v apr-1-config)"; \
    		./configure \
    			--libdir="$TOMCAT_NATIVE_LIBDIR" \
    			--prefix="$CATALINA_HOME" \
    			--with-apr="$aprConfig" \
    			--with-java-home="$JAVA_HOME" \
    			--with-ssl \
    		; \
    		nproc="$(nproc)"; \
    		make -j "$nproc"; \
    		make install; \
    	); \
    	rm -rf "$nativeBuildDir"; \
    	rm bin/tomcat-native.tar.gz; \
    	\
    # mark any explicit dependencies as manually installed
    	find "$TOMCAT_NATIVE_LIBDIR" -type f -executable -exec ldd '{}' ';' \
    		| awk '/=>/ && $(NF-1) != "=>" { print $(NF-1) }' \
    		| xargs -rt readlink -e \
    		| sort -u \
    		| xargs -rt rpm --query --whatprovides \
    		| sort -u \
    		| tee "$TOMCAT_NATIVE_LIBDIR/.dependencies.txt" \
    		| xargs -r yumdb set reason user \
    	; \
    	\
    # clean up anything added temporarily and not later marked as necessary
    	yum autoremove -y; \
    	yum clean all; \
    	rm -rf /var/cache/yum; \
    	\
    # sh removes env vars it doesn't support (ones with periods)
    # https://github.com/docker-library/tomcat/issues/77
    	find ./bin/ -name '*.sh' -exec sed -ri 's|^#!/bin/sh$|#!/usr/bin/env bash|' '{}' +; \
    	\
    # fix permissions (especially for running as non-root)
    # https://github.com/docker-library/tomcat/issues/35
    	chmod -R +rX .; \
    	chmod 1777 logs temp work; \
    	\
    # smoke test
    	catalina.sh version
    # verify Tomcat Native is working properly
    RUN set -eux; \
    	nativeLines="$(catalina.sh configtest 2>&1)"; \
    	nativeLines="$(echo "$nativeLines" | grep 'Apache Tomcat Native')"; \
    	nativeLines="$(echo "$nativeLines" | sort -u)"; \
    	if ! echo "$nativeLines" | grep -E 'INFO: Loaded( APR based)? Apache Tomcat Native library' >&2; then \
    		echo >&2 "$nativeLines"; \
    		exit 1; \
    	fi
    EXPOSE 8080
    CMD ["catalina.sh", "run"]
    

    四、Dockerfile 构建

    1. 基础构建

    Dockerfile 的构建通过 docker build 命令实现,如下:

    # 注意:定义的TAG后面有个空格,空格后面有个点
    # docker build -t 新镜像名字:TAG .
    docker build -t ubuntu:1.0.1 .
    

    如下例子:通过 Dockerfile 基于 centos 7 镜像新建一个支持 vim、 net-tools 以及 JDK 的镜像。

    # 在 jdk 相对路径下建立
    [root@localhost jdk]# ll
    总用量 189604
    drwxr-xr-x. 7   10  143       245 10月  5 2019 jdk1.8
    -rw-r--r--. 1 root root 194151339 10月 14 13:55 jdk-8u231-linux-x64.tar.gz
    # 新建编辑 Dockerfile 文件
    [root@localhost jdk]# vim Dockerfile
    # 查看当前存在镜像
    [root@localhost jdk]# docker images 
    REPOSITORY   TAG       IMAGE ID       CREATED       SIZE
    tomcat       latest    fb5657adc892   2 years ago   680MB
    mysql        5.7       c20987f18b13   2 years ago   448MB
    ubuntu       latest    ba6acccedd29   2 years ago   72.8MB
    centos       7         eeb6ee3f44bd   2 years ago   204MB
    redis        6.0.8     16ecd2772934   3 years ago   104MB
    # 构建新镜像
    [root@localhost jdk]# docker build -t ubuntu-plus:1.0.0 .
    [+] Building 0.0s (11/11) FINISHED             docker:default
     => [internal] load build definition from Dockerfile     0.0s
     => => transferring dockerfile: 830B                     0.0s
     => [internal] load metadata for docker.io/library/centos:7                                             0.0s
     => [internal] load .dockerignore                        0.0s
     => => transferring context: 2B                          0.0s
     => [1/6] FROM docker.io/library/centos:7                0.0s
     => [internal] load build context                        0.0s
     => => transferring context: 107B                        0.0s
     => CACHED [2/6] WORKDIR /usr/local                      0.0s
     => CACHED [3/6] RUN yum -y install vim                  0.0s
     => CACHED [4/6] RUN yum -y install net-tools            0.0s
     => CACHED [5/6] RUN mkdir /usr/local/java               0.0s
     => CACHED [6/6] ADD jdk-8u231-linux-x64.tar.gz /usr/local/java                                         0.0s
     => exporting to image                                   0.0s
     => => exporting layers                                  0.0s
     => => writing image sha256:83f22e20f9477993e092720eee52962161c2a30e32294e61d3fe251b3ad7eebb            0.0s
     => => naming to docker.io/library/ubuntu-plus:1.0.0     0.0s
    # 构建成功,可以看到新镜像
    [root@localhost jdk]# docker images
    REPOSITORY    TAG       IMAGE ID       CREATED         SIZE
    ubuntu-plus   1.0.0     83f22e20f947   5 minutes ago   1.12GB
    tomcat        latest    fb5657adc892   2 years ago     680MB
    mysql         5.7       c20987f18b13   2 years ago     448MB
    ubuntu        latest    ba6acccedd29   2 years ago     72.8MB
    centos        7         eeb6ee3f44bd   2 years ago     204MB
    redis         6.0.8     16ecd2772934   3 years ago     104MB
    # 可以正常进,并且支持上述新增功能
    [root@localhost jdk]# docker run -it ubuntu-plus:1.0.0 /bin/bash
    

    上面例子中的 Dockerfile 内容如下:

    FROM centos:7
    # 执行作者和邮箱
    MAINTAINER kingfish
    # 设置环境变量
    ENV MYPATH /usr/local
    # 设置容器进入后的默认路径
    WORKDIR $MYPATH
    # 安装 net-tools 和 vim
    RUN yum -y install vim
    RUN yum -y install net-tools
    # 安装 JDK8
    # RUN yum -y install glibc.i868
    RUN mkdir /usr/local/java
    # 将 宿主机的 jdk 文件拷贝到容器内部. Add 是相对路径 jar, 安装包必须要和 Dockerfile 放同一文件夹下
    ADD jdk-8u231-linux-x64.tar.gz /usr/local/java
    # 配置环境变量
    ENV JAVA_HOME /usr/local/java/jdk1.8.0_231
    ENV JRE_HOME $JAVA_HOME/jre
    ENV CLASSPATH $JAVA_HOME/lib/dt.jar:$JAVA_HOME/lib/tools.jar:$JRE_HOME/lib:$CLASSPATH
    ENV PATH $JAVA_HOME/bin:$PATH
    EXPOSE 80
    CMD echo $MYPATH
    CMD echo "install ifconfig cmd into ubuntu success ....."
    CMD /bin/bash
    

    2. 微服务打包成 Dockerfile

    1. 新建SpringBoot服务并打包为 spring-simple-demo.jar,移动到宿主机上,本文不再展示具体服务搭建过程

    2. 编写 Dockerfile,文件内容如下:

      # 依赖于 JDK 8
      FROM jdk:8
      MAINTAINER kingfish
      # 在主机 /var/lib/docker目录下创建一个临时文件,并链接到容器的 /tmp
      VOLUME /tmp
      # 将jar包添加到容器中,并命名为 spring-simple-demo.jar
      ADD spring-simple-demo.jar /spring-simple-demo.jar
      # 运行jar包
      RUN bash -c 'touch /spring-simple-demo.jar'
      ENTRYPOINT ["java", "-jar", "/spring-simple-demo.jar"]
      # 暴露服务端口(服务本身暴露9090端口)
      EXPOSE 9090
      
    3. 构建镜像

      [root@192 spring-demo]# vim Dockerfile 
      # 构建镜像,指定镜像名和版本号
      [root@192 spring-demo]#  docker build -t simple_demo_docker:1.0 .
      [+] Building 15.8s (8/8) FINISHED                                       docker:default
       => [internal] load build definition from Dockerfile                              0.0s
       => => transferring dockerfile: 588B                                              0.0s
       => [internal] load metadata for docker.io/library/java:8                        15.2s
       => [internal] load .dockerignore  0.0s
       => => transferring context: 2B    0.0s
       => [internal] load build context  0.0s
       => => transferring context: 103B  0.0s
       => CACHED [1/3] FROM docker.io/library/java:8@sha256:c1ff613e8ba25833d2e1940da0940c3824f03f802c449f3d1815a66b7f8c0e9d           0.0s
       => [2/3] ADD spring-simple-demo.jar /spring-simple-demo.jar                      0.1s
       => [3/3] RUN bash -c 'touch /spring-simple-demo.jar'                             0.3s
       => exporting to image             0.1s
       => => exporting layers            0.1s
       => => writing image sha256:bb3a29e0a820244956ee052546902f06837b2395deb84bab2f4215b838e8fada                                     0.0s
       => => naming to docker.io/library/simple_demo_docker:1.0                         0.0s
       # 查看镜像
      [root@192 spring-demo]# docker images
      REPOSITORY           TAG       IMAGE ID       CREATED         SIZE
      simple_demo_docker   1.0       bb3a29e0a820   6 seconds ago   749MB
      
    4. 启动镜像并测试

      # 启动容器
      [root@192 spring-demo]# docker run -p 9090:9090 -it simple_demo_docker:1.0  /bin/bash
      # 调用服务暴露接口,可以正常返回结果
      [root@192 spring-demo]# curl 127.0.0.1:9090/simple-demo/demo/getByDb
      [{"id":232,"name":null,"password":null,"roleId":null,"userId":"1"},{"id":233,"name":null,"password":null,"roleId":null,"userId":"0"},{"id":234,"name":null,"password":null,"roleId":null,"userId":"2"},{"id":235,"name":null,"password":null,"roleId":null,"userId":"3"},{"id":236,"name":null,"password":null,"roleId":null,"userId":"4"},{"id":237,"name":null,"password":null,"roleId":null,"userId":"5"},{"id":238,"name":null,"password":null,"roleId":null,"userId":"7"},{"id":239,"name":null,"password":null,"roleId":null,"userId":"6"},{"id":240,"name":null,"password":null,"roleId":null,"userId":"8"},{"id":241,"name":null,"password":null,"roleId":null,"userId":"9"}][root@192 spring-demo]# 
      

    五、虚悬镜像

    虚悬镜像:仓库名、标签名都是 的镜像,称为 dangling images(虚悬镜像)。

    在构建或者删除镜像时可能由于一些错误导致出现虚悬镜像。如:

    构建时候没有镜像名、tag

    docker build .
    

    列出docker中的虚悬镜像:

    docker image ls -f dangling=true
    

    虚悬镜像一般是因为一些错误而出现的,没有存在价值,可以删除:

    # 删除所有的虚悬镜像
    docker image prune
    

    六、参考内容

    B 站 尚硅谷 Docker 视频

    https://www.yuque.com/tmfl/cloud/dketq0


0
收藏0
文章版权声明:除非注明,否则均为VPS857原创文章,转载或复制请以超链接形式并注明出处。

相关阅读

  • 【研发日记】Matlab/Simulink自动生成代码(二)——五种选择结构实现方法,Matlab/Simulink自动生成代码的五种选择结构实现方法(二),Matlab/Simulink自动生成代码的五种选择结构实现方法详解(二)
  • 超级好用的C++实用库之跨平台实用方法,跨平台实用方法的C++实用库超好用指南,C++跨平台实用库使用指南,超好用实用方法集合,C++跨平台实用库超好用指南,方法与技巧集合
  • 【动态规划】斐波那契数列模型(C++),斐波那契数列模型(C++实现与动态规划解析),斐波那契数列模型解析与C++实现(动态规划)
  • 【C++】,string类底层的模拟实现,C++中string类的模拟底层实现探究
  • uniapp 小程序实现微信授权登录(前端和后端),Uniapp小程序实现微信授权登录全流程(前端后端全攻略),Uniapp小程序微信授权登录全流程攻略,前端后端全指南
  • Vue脚手架的安装(保姆级教程),Vue脚手架保姆级安装教程,Vue脚手架保姆级安装指南,Vue脚手架保姆级安装指南,从零开始教你如何安装Vue脚手架
  • 如何在树莓派 Raspberry Pi中本地部署一个web站点并实现无公网IP远程访问,树莓派上本地部署Web站点及无公网IP远程访问指南,树莓派部署Web站点及无公网IP远程访问指南,本地部署与远程访问实践,树莓派部署Web站点及无公网IP远程访问实践指南,树莓派部署Web站点及无公网IP远程访问实践指南,本地部署与远程访问详解,树莓派部署Web站点及无公网IP远程访问实践详解,本地部署与远程访问指南,树莓派部署Web站点及无公网IP远程访问实践详解,本地部署与远程访问指南。
  • vue2技术栈实现AI问答机器人功能(流式与非流式两种接口方法),Vue2技术栈实现AI问答机器人功能,流式与非流式接口方法探究,Vue2技术栈实现AI问答机器人功能,流式与非流式接口方法详解
  • 发表评论

    快捷回复:表情:
    评论列表 (暂无评论,0人围观)

    还没有评论,来说两句吧...

    目录[+]

    取消
    微信二维码
    微信二维码
    支付宝二维码