• docker学习笔记(3)- 镜像


    简介

    docker学习笔记(1)- 架构概述一节中可以看到镜像是docker三大组件之一,可以将Docker镜像类比为虚拟机的模版。

    1. 镜像由多个层组成,每层叠加之后从外部看就像一个独立的对象,镜像的内部包括操作系统、应用程序、应用运行时所必须的依赖包等。
    2. 使用镜像时从仓库中拉取镜像到Docker主机,然后使用该镜像可以启动一个或多个容器,也可以将容器构建为镜像。

    具体的概念与实现在后续实现Docker的基础技术中记录,先整理镜像的用法

    相关命令

    镜像加速

    国内访问Docker hub有速度缓慢甚至会无法的情况,换用国内云厂商提供的加速服务,可以添加多个源,在/etc/docker/daemon.json文件中添加如下json内容,没有daemon.json可以自己新建

    复制代码
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    bash
    { "registry-mirror": [ "https://hub-mirror.c.163.com/", "https://reg-mirror.qiniu.com" ] } # 重启docker systemctl daemon-reload systemctl restart docker # 查看是否生效,在使用docker pull时会快很多 docker info > Registry: https://index.docker.io/v1/ Labels: Experimental: false Insecure Registries: 127.0.0.0/8 Registry Mirrors: https://hub-mirror.c.163.com/ https://reg-mirror.qiniu.com/

    搜索镜像

    复制代码
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    bash
    docker search centos --filter=stars=20 > NAME DESCRIPTION STARS OFFICIAL AUTOMATED centos The official build of CentOS. 7066 [OK] centos/systemd systemd enabled base container. 105 [OK] centos/mysql-57-centos7 MySQL 5.7 SQL database server 92 centos/postgresql-96-centos7 PostgreSQL is an advanced Object-Relational … 45 centos/httpd-24-centos7 Platform for running Apache httpd 2.4 or bui… 43 centos/python-35-centos7 Platform for building and running Python 3.5… 39 centos/php-56-centos7 Platform for building and running PHP 5.6 ap… 34 centos/mysql-56-centos7 MySQL 5.6 SQL database server 22
    • NAME:镜像名字

    • DESCRIPTION:镜像描述信息,默认会被截断,可使用--no-trunc取消截断

    • STARS:收藏数,--filter=starts=20,搜索收藏数大于20的镜像

    • OFFICIAL:由docker官方维护支持的镜像,最好使用官方镜像作为基础镜像

    • AUTOMATED:该镜像由docker hub的自动构建流程创建的

    拉取镜像

    复制代码
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    bash
    # 拉取镜像 docker pull <镜像名称> # 不提供仓库名默认为docker.io,tag默认为最新的tag,用户名默认为官方镜像 docker pull ubuntu > Using default tag: latest latest: Pulling from library/ubuntu 7c3b88808835: Pull complete Digest: sha256:8ae9bafbb64f63a50caab98fd3a5e37b3eb837a3e0780b78e5218e63193961f9 Status: Downloaded newer image for ubuntu:latest docker.io/library/ubuntu:latest # 最后一行显示完整的镜像名称 # 指定仓库名和tag docker image pull docker.io/library/ubuntu:16.04 > 16.04: Pulling from library/ubuntu 58690f9b18fc: Pull complete b51569e7c507: Pull complete da8ef40b9eca: Pull complete fb15d46c38dc: Pull complete Digest: sha256:0f71fa8d4d2d4292c3c617fda2b36f6dabe5c8b6e34c3dc5b0d17d4e704bd39c Status: Downloaded newer image for ubuntu:16.04 docker.io/library/ubuntu:16.04
    • 镜像名称的格式为:Docker仓库地址/用户名/软件名:tag
    • 上面提到镜像是分层存储的,可以看到pull时也是一层一层进行,给出每层ID的前12位,拉取完成后给出给出一个sha256的摘要,用来确保下载一致性

    推送镜像

    使用docker push推送镜像到仓库,也可以推送到私有仓库,在docker学习笔记(2)- 仓库一节中有记录

    列出镜像

    复制代码
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    bash
    # 列出本地镜像 docker image ls > REPOSITORY TAG IMAGE ID CREATED SIZE registry 2 8948869ebfee 5 days ago 24.2MB ubuntu latest 2b4cba85892a 10 days ago 72.8MB portainer/portainer-ce latest ed396c816a75 4 weeks ago 280MB joxit/docker-registry-ui latest c4f5113ae220 4 months ago 24.8MB centos 7 eeb6ee3f44bd 5 months ago 204MB centos latest 5d0da3dc9764 5 months ago 231MB ubuntu 16.04 b6f507652425 6 months ago 135MB radial/busyboxplus latest fffcfdfce622 7 years ago 12.9MB # 列出所有镜像,包括中间层镜像 docker image ls -a # 查看镜像、容器、存储卷等实际消耗空间 docker system df > TYPE TOTAL ACTIVE SIZE RECLAIMABLE Images 8 2 984.2MB 680.5MB (69%) Containers 2 2 0B 0B Local Volumes 5 0 184.3MB 184.3MB (100%) Build Cache 0 0 0B 0B # 列出镜像sha256摘要 docker image ls --digests REPOSITORY TAG DIGEST IMAGE ID CREATED SIZE busybox latest sha256:caa382c432891547782ce7140fb3b7304613d3b0438834dce1cad68896ab110a 2fb6fc2d97e1 2 days ago 1.24MB www.codemachine.in/busybox latest sha256:14d4f50961544fdb669075c442509f194bdc4c0e344bde06e35dbd55af842a38 2fb6fc2d97e1 2 days ago 1.24MB
    • 一个镜像可以对应多个标签,IMAGE ID是镜像的唯一标识

    • Docker Hub中显示的镜像体积是网络传输即压缩后的体积,而下载到本地后会解压缩,所以本地看到的镜像SIZE更大

    • 通过image ls 列出的镜像体积并不是本地实际消耗的空间,镜像是多层存储结构并且可以继承复用,因此不同的镜像可能会使用相同的基础镜像,Union FS使得相同的层只需保存一份

    format展示

    复制代码
    • 1
    • 2
    • 3
    • 4
    bash
    # 仅仅显示image ID docker image ls -q # 删除所有列出的镜像 docker image rm $(docker image ls -q)

    使用go模版语法

    复制代码
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    bash
    # 列出镜像ID和仓库名 docker image ls --format "{{.ID}}: {{.Repository}}" > 2fb6fc2d97e1: busybox 2fb6fc2d97e1: www.codemachine.in/busybox 5d0da3dc9764: 172.17.73.129:6000/centos 5d0da3dc9764: centos 5d0da3dc9764: www.codemachine.in/centos 5d0da3dc9764: www.codemachine.in/centos 32b8411b497a: dockersamples/atseasampleshopapp_reverse_proxy 8dbf7c60cf88: dockersamples/visualizer # 自定义列显示 docker image ls --format "table {{.ID}}\t{{.Repository}}\t{{.Tag}}" > IMAGE ID REPOSITORY TAG 2fb6fc2d97e1 busybox latest 2fb6fc2d97e1 www.codemachine.in/busybox latest 5d0da3dc9764 172.17.73.129:6000/centos latest 5d0da3dc9764 centos latest 5d0da3dc9764 www.codemachine.in/centos galen 5d0da3dc9764 www.codemachine.in/centos latest 32b8411b497a dockersamples/atseasampleshopapp_reverse_proxy <none> 8dbf7c60cf88 dockersamples/visualizer <none>

    dangling镜像

    这类镜像没有标签和仓库名,在pull或者build了新版本镜像后,新旧镜像同名,旧的镜像名称与tag被取消,产生了dangling镜像

    复制代码
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    er-hljs
    # 查看dangling镜像 docker image ls -f dangling=true > REPOSITORY TAG IMAGE ID CREATED SIZE <none> <none> 00285df0df87 5 days ago 342 MB # -f后还可以跟since,before,label等参数过滤

    删除镜像

    可以使用镜像ID、镜像名、sha256摘要来删除镜像

    复制代码
    • 1
    • 2
    • 3
    • 4
    bash
    # OPTION: -f 强制删除 docker image rm [OPTION] <NAME> # 删除未使用的镜像(清理多余镜像) docker image prune
    复制代码
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    bash
    [docker@docker1 ~]$ docker images REPOSITORY TAG IMAGE ID CREATED SIZE busybox latest 2fb6fc2d97e1 2 days ago 1.24MB www.codemachine.in/busybox latest 2fb6fc2d97e1 2 days ago 1.24MB [docker@docker1 ~]$ docker image rm busybox Untagged: busybox:latest Untagged: busybox@sha256:caa382c432891547782ce7140fb3b7304613d3b0438834dce1cad68896ab110a [docker@docker1 ~]$ docker images REPOSITORY TAG IMAGE ID CREATED SIZE www.codemachine.in/busybox latest 2fb6fc2d97e1 2 days ago 1.24MB [docker@docker1 ~]$ docker image rm 2fb6fc2d97e1 Untagged: www.codemachine.in/busybox:latest Untagged: www.codemachine.in/busybox@sha256:14d4f50961544fdb669075c442509f194bdc4c0e344bde06e35dbd55af842a38 Deleted: sha256:2fb6fc2d97e10c79983aa10e013824cc7fc8bae50630e32159821197dda95fe3 Deleted: sha256:797ac4999b67d8c38a596919efa5b7b6a4a8fd5814cb8564efa482c5d8403e6d

    几种不会删除镜像的情况:

    1. Untagged:一个镜像可能有多个标签标签指向,可以看到下面两个镜像的IMAGE ID是一样的,因此只删除一个并没有真正delete镜像,而是删除了标签,所有标签都Untagged后才会真正删除镜像,Deleted
    2. 从上层向基础层方向依次查找,如果有其他镜像依赖当前镜像也无法真正Deleted
    3. 如果有容器以此镜像为基础启动,不管容器是否运行,该镜像都不可删除

    Dockerfile

    构建镜像实际上是在每一层添加配置、文件等。将每一层修改、安装、构建、操作等命令写入dockerfile脚本中,这样在构建镜像时使用了什么命令,做了什么操作,同时可以配合多阶段构建来精简镜像体积和降低部署复杂度。

    docker build用法

    复制代码
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    bash
    # 指定镜像名,使用当前目录下的Dockerfile docker build -t shykes/myapp:1.0.2 -t shykes/myapp:latest . # 指定Dockerfile路径 docker build -f /path/to/a/Dockerfile . # 从标准输入中读取Dockerfile进行构建 docker build - < Dockerfile cat Dockerfile | docker build - # 读取压缩包构建 docker build - < context.tar.gz
    • 构建上下文(Context):docker采用的是C/S架构,在运行时docker engine提供了一组REST API,在使用客户端时其实是通过API与docker engine交互,那么就算我们是在本机执行docker命令,诸如ADD、COPY这类这令时,实际上还是使用远程调用的方式在服务端完成(docker engine),docker build -t <NAME> .的意思是将当前目录作为构建镜像上下文的路径,然后将该路径的所有内容打包上传到docker engine,之后docker engine用收到的文件构建镜像。

    • 一般将dockerfile置于项目根目录,如果该目录下有些东西不希望在构建时传给docker engine,可以添加到.dockerignore文件中。

    指令

    书写Dockerfile的常用指令,详细参考见Dockerfile reference

    FROM

    指定基础镜像

    复制代码
    • 1
    • 2
    • 3
    bash
    FROM [--platform=<platform>] <image>[:<tag>] [AS <name>] # --platform:提供镜像使用平台,linux/amd64, linux/arm64, or windows/amd64 # AS <name>: 指定此构建阶段的别名,供后面的FROM和COPY引用
    • 特殊镜像scratch是一个空白的镜像,执行的指令会在镜像第一层开始写,静态编译适用

    RUN

    执行命令行命令

    复制代码
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    bash
    # shell格式 RUN <command> RUN /bin/bash -c "echo hello" # exec格式 RUN ["executable", "param1", "param2"] RUN ["/bin/bash", "-c", "echo hello"]
    • 每一行RUN执行就会使镜像新增一层,过多使用RUN使得镜像臃肿很容易就达到Union FS限制的最大层数,利用 && 和换行 的方式执行多条命令在这一层将所有的事情做完是个不错的选择
    • 通常用于安装软件包

    COPY

    从Context目录中的文件复制到镜像新一层的目录下

    复制代码
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    bash
    COPY [--chown=<user>:<group>] <源路径>... <目标路径> or COPY [--chown=<user>:<group>] ["<源路径1>",... "<目标路径>"] # 源路径可以是多个或满足Go语言filepath.Match规则的通配符 COPY package.json /usr/src/app/ COPY hom* /mydir/ COPY hom?.txt /mydir/
    • <目标路径> 可以是容器内的绝对路径,也可以是相对于工作目录的相对路径(工作目录可以用 WORKDIR 指令来指定)
    • 使用 COPY 指令,源文件的各种元数据都会保留

    CMD

    Docker 不是虚拟机,容器就是进程。既然是进程,那么在启动容器的时候,需要指定所运行的程序及参数。CMD 指令就是用于指定默认的容器主进程的启动命令的。

    复制代码
    • 1
    • 2
    • 3
    bash
    CMD echo $HOME or CMD [ "sh", "-c", "echo $HOME" ]
    • 可被替换:比如centos默认CMD /bin/bash,那么使用docker run -it centos就会进入/bin/bash下,如果使用docker run -it centos cat /etc/redhat-release就会变成输出release号后停止

    ENTRYPOINT

    与CMD功能相似,只不过CMD容器运行时若添加了参数(如上所说),那么默认CMD的参数就会被替换掉,而ENTRYPOINT会将添加的参数跟在原有参数的后边,这样就可以像使用命令一样使用容器

    复制代码
    • 1
    bash
    ENTRYPOINT [ "curl", "-s", "http://myip.ipip.net" ]

    也可以做启动容器前的准备工作,以下为redis创建用户,然后为ENTRYPOINT指定脚本,该脚本判断CMD参数是否是启动redis-server,如果是使用redis用户启动,如果是其他操作则继续使用root,这样既保证了服务运行的安全性,又不妨碍使用root用户做一些调试和信息获取等操作

    复制代码
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    bash
    FROM alpine:3.4 ... RUN addgroup -S redis && adduser -S -G redis redis ... ENTRYPOINT ["docker-entrypoint.sh"] EXPOSE 6379 CMD [ "redis-server" ] # docker-entrypoint.sh #!/bin/sh ... # allow the container to be started with `--user` if [ "$1" = 'redis-server' -a "$(id -u)" = '0' ]; then find . \! -user redis -exec chown redis '{}' + exec gosu redis "$0" "$@" fi exec "$@"

    ENV

    设置环境变量,在后面的指令中引用

    复制代码
    • 1
    er-hljs
    ENV <key>=<value> ...

    VOLUME

    在镜像中创建挂载点,但是无法指定创建在主机的对应目录,可以通过docker inspect <CONTAINER NAME>查看Source挂载目录是哪个

    复制代码
    • 1
    • 2
    • 3
    bash
    VOLUME ["<路径1>", "<路径2>"...] or VOLUME <路径>

    EXPOSE

    声明容器运行时打算用什么端口,并不会自动在宿主机和容器进行端口映射。可以使用docker run -p <主机端口:容器端口>进行端口映射,也可以使用docker run -P随机映射EXPOSE的端口

    复制代码
    • 1
    • 2
    bash
    EXPOSE <port> [<port>/<protocol>...] EXPOSE 80/tcp

    WORKDIR

    指定当前工作目录,后面各层(RUN,CMD,ENTRYPOINT,COPY,ADD)的当前目录就被改为WORKDIR目录,如果该目录不存在则会自动创建

    复制代码
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    bash
    WORKDIR /path/to/workdir # 示例,pwd的路径为/a/b/c WORKDIR /a WORKDIR b WORKDIR c RUN pwd

    USER

    指定用户身份,影响后面各层操作的用户,用户必须事先创建好

    复制代码
    • 1
    bash
    USER <用户名>[:<用户组>]

    如果是执行SHELL时候要改变身份,不要使用 su 或者 sudo,这些都需要比较麻烦的配置,而且在 TTY 缺失的环境下经常出错。建议使用 ``gosu`

    复制代码
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    bash
    # 建立 redis 用户,并使用 gosu 换另一个用户执行命令 RUN groupadd -r redis && useradd -r -g redis redis # 下载 gosu RUN wget -O /usr/local/bin/gosu "https://github.com/tianon/gosu/releases/download/1.12/gosu-amd64" \ && chmod +x /usr/local/bin/gosu \ && gosu nobody true # 设置 CMD,并以另外的用户执行 CMD [ "exec", "gosu", "redis", "redis-server" ]

    LABEL

    为镜像添加元数据

    复制代码
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    bash
    LABEL <key>=<value> <key>=<value> <key>=<value> ... LABEL "com.example.vendor"="ACME Incorporated" LABEL com.example.label-with-value="foo" LABEL version="1.0" LABEL description="This text illustrates \ that label-values can span multiple lines."

    SHELL

    用来指定RUN ENTRYPOINT CMD 指令的 shell,Linux 中默认为 ["/bin/sh", "-c"]

    复制代码
    • 1
    bash
    SHELL ["executable", "parameters"]

    ONBUILD

    一般作为基础镜像时使用,该指令在构建当前镜像时不会执行,当其他镜像以此为基础镜像时才会执行

    复制代码
    • 1
    bash
    ONBUILD <其它指令>

    使用git仓库构建

    复制代码
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    bash
    docker build -t hello-world git://github.com/docker-library/hello-world.git\#master:amd64/hello-world > Sending build context to Docker daemon 22.02kB Step 1/3 : FROM scratch ---> Step 2/3 : COPY hello / ---> e0499e772bd9 Step 3/3 : CMD ["/hello"] ---> Running in 1eeb706f26e2 Removing intermediate container 1eeb706f26e2 ---> 6abb50a2e5cc Successfully built 6abb50a2e5cc Successfully tagged hello-world:latest # 查看镜像 docker images REPOSITORY TAG IMAGE ID CREATED SIZE hello-world latest 6abb50a2e5cc 47 seconds ago 13.3kB

    指定要构建的git仓库地址,切换到master分支,进入amd64/hello-world目录开始构建

    使用tar压缩包构建

    docker engine下载该tar包并自动解压,以解压后的文件夹作为上下文开始构建

    复制代码
    • 1
    bash
    docker build http://server/context.tar.gz

    Dockerfile多阶段构建

    Docker v17.05开始支持多阶段构建 (multistage builds),解决了以下问题:

    • 如果使用一个Dockerfile,镜像体积过大使得部署时间过长(比如编译依赖组件繁多,但实际运行中并不需要),而且容易泄露源码
    • 如果使用多个Dockerfile(比如编译和运行分开进行),中间需要脚本整合不同构建阶段内容是个比较复杂的工作,容易出现问题

    下面对比单个Dockerfile构建和多阶段构建一个go helloworld程序的区别。

    复制代码
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    bash
    # app.go package main import "fmt" func main(){ fmt.Printf("Hello World!"); }

    使用单个文件

    Dockerfile

    复制代码
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    bash
    FROM golang:alpine RUN sed -i 's/dl-cdn.alpinelinux.org/mirrors.aliyun.com/g' /etc/apk/repositories RUN apk --no-cache add git ca-certificates WORKDIR /go/src/github.com/go/helloworld/ COPY app.go . RUN go mod init RUN GOPROXY="https://goproxy.io" GO111MODULE=on go get -d -v github.com/go-sql-driver/mysql \ && CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o app . \ && cp /go/src/github.com/go/helloworld/app /root WORKDIR /root/ CMD ["./app"]

    build

    复制代码
    • 1
    • 2
    • 3
    • 4
    • 5
    bash
    docker build -t go/helloworld:1 . # 运行容器 docker container run go/helloworld:1 > Hello World!%

    多阶段构建

    Dockerfile

    复制代码
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    bash
    # 将此阶段命名为builder FROM golang:alpine as builder # 解决下载go慢的问题 RUN sed -i 's/dl-cdn.alpinelinux.org/mirrors.aliyun.com/g' /etc/apk/repositories RUN apk --no-cache add git WORKDIR /go/src/github.com/go/helloworld/ RUN GOPROXY="https://goproxy.io" GO111MODULE=on go get -d -v github.com/go-sql-driver/mysql COPY app.go . # 处理go.mod缺失问题 RUN go mod init # 编译app.go RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o app . # 制作应用镜像,此阶段命名为prod FROM alpine:latest as prod RUN apk --no-cache add ca-certificates WORKDIR /root/ COPY --from=0 /go/src/github.com/go/helloworld/app . CMD ["./app"]

    build

    复制代码
    • 1
    • 2
    • 3
    • 4
    • 5
    bash
    docker build -t go/helloworld:2 . # 运行容器 docker container run go/helloworld:2 > Hello World!%

    对比两个镜像的大小,可以看到通过多阶段构建的方法,摒弃编译所需环境依赖,最后的应用镜像要精简很多很多

    复制代码
    • 1
    • 2
    • 3
    • 4
    bash
    docker image ls |grep go/hello > go/helloworld 2 38f137a75add 6 minutes ago 7.86MB go/helloworld 1 e7606d3c0921 17 minutes ago 353MB

    构建到某一阶段

    依据上面的Dockerfile,如果我们只想构建到Build阶段的镜像时,可以用--targe参数指定此阶段别名来实现

    复制代码
    • 1
    bash
    docker build --target builder -t username/imagename:tag .

    结束

    本篇主要汇总go镜像相关操作指令等,镜像原理等架构技术会在后面深入学习docker底层实现时分析

    学习自:
    《Docker技术入门与实战(第3版)》Nigel,Poulton(奈吉尔·波尔顿) 著,李瑞丰,刘康 译
    《深入浅出Docker》杨保华,戴王剑,曹亚仑 著
    https://docs.docker.com/

  • 相关阅读:
    阿里P8熬了一个月肝出这份32W字Java面试手册,在Github标星31K+
    Swoole 的异步 Task 任务详解
    编写高效的消息传递代码-对消息进行降维
    基于SSM实现智慧幼儿园信息管理系统
    第三方登录功能的实现之 QQ登录 - 已绑定账号
    用《斗破苍穹》的视角打开C#委托2 委托链 / 泛型委托 / GetInvocationList
    MyBatis反射和类型转换
    day05 51单片机-外部中断、定时器
    单片机是不是嵌入式呢,老生常谈了
    InnoDB和MyISAM的区别
  • 原文地址:https://www.cnblogs.com/tongh/p/16015361.html